Add security scheme to controller for client generation
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Clients (at least javascript client) generated from the OpenAPI spec do add the bearer token to the request without specifying the bearer requirement.
This commit is contained in:
parent
39fd416b4a
commit
bcfbf349cd
7 changed files with 105 additions and 67 deletions
|
@ -20,6 +20,7 @@ import io.swagger.v3.oas.annotations.media.Content;
|
|||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import net.friedl.fling.model.dto.ArtifactDto;
|
||||
import net.friedl.fling.service.ArtifactService;
|
||||
|
@ -28,6 +29,7 @@ import net.friedl.fling.service.archive.ArchiveService;
|
|||
@RestController
|
||||
@RequestMapping("/api/artifacts")
|
||||
@Tag(name = "artifact", description = "Operations on /api/artifacts")
|
||||
@SecurityRequirement(name = "bearer")
|
||||
@Validated
|
||||
public class ArtifactController {
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import net.friedl.fling.model.dto.ArtifactDto;
|
||||
import net.friedl.fling.model.dto.FlingDto;
|
||||
|
@ -32,6 +33,7 @@ import net.friedl.fling.service.archive.ArchiveService;
|
|||
@RestController
|
||||
@RequestMapping("/api/fling")
|
||||
@Tag(name = "fling", description = "Operations on /api/fling")
|
||||
@SecurityRequirement(name = "bearer")
|
||||
public class FlingController {
|
||||
|
||||
private FlingService flingService;
|
||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||
|
||||
import {Switch, Route, Redirect} from "react-router-dom";
|
||||
|
||||
import request, {isOwner, isUser} from './util/request';
|
||||
import jwt from './util/jwt.js';
|
||||
|
||||
import Login from './components/admin/Login';
|
||||
import FlingAdmin from './components/admin/FlingAdmin';
|
||||
|
@ -13,53 +13,64 @@ import FlingUser from './components/user/FlingUser';
|
|||
import LandingPage from './components/LandingPage';
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact path="/" component={LandingPage} />
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact path="/" component={LandingPage} />
|
||||
|
||||
<Route exact path="/admin/login" component={Login} />
|
||||
<OwnerRoute exact path="/admin"><FlingAdmin /></OwnerRoute>
|
||||
<OwnerRoute path="/admin/:fling"><FlingAdmin /></OwnerRoute>
|
||||
<Route exact path="/admin/login" component={Login} />
|
||||
<OwnerRoute exact path="/admin"><FlingAdmin /></OwnerRoute>
|
||||
<OwnerRoute path="/admin/:fling"><FlingAdmin /></OwnerRoute>
|
||||
|
||||
<Route exact path="/unlock" component={Unlock} />
|
||||
<UserRoute path="/f/:shareId"><FlingUser /></UserRoute>
|
||||
<Route exact path="/unlock" component={Unlock} />
|
||||
<UserRoute path="/f/:shareId"><FlingUser /></UserRoute>
|
||||
|
||||
<Route match="*">Not implemented</Route>
|
||||
</Switch>
|
||||
);
|
||||
<Route match="*">Not implemented</Route>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
// A wrapper for <Route> that redirects to the login
|
||||
// screen if you're not yet authenticated.
|
||||
/*
|
||||
* A wrapper for <Route> that redirects to the login screen if no admin
|
||||
* authentication token was found.
|
||||
*
|
||||
* Note that the token check is purely client-side. It provides no actual
|
||||
* protection! It is hence possible to reach the admin site with some small
|
||||
* amount of trickery. Without a valid token no meaningful actions are possible
|
||||
* on the admin page though.
|
||||
*/
|
||||
function OwnerRoute({ children, ...rest }) {
|
||||
return (
|
||||
<Route
|
||||
{...rest}
|
||||
render={({ location }) => {
|
||||
log.info(request.defaults);
|
||||
if(isOwner()) { return children; }
|
||||
else { return <Redirect to={{pathname: "/admin/login", state: {from: location}}} />; }
|
||||
}}
|
||||
/>
|
||||
);
|
||||
log.info(`Routing request for ${rest['path']}`);
|
||||
return (
|
||||
<Route
|
||||
{...rest}
|
||||
render={({ location }) => {
|
||||
if (jwt.hasSubject("admin")) { return children; }
|
||||
else { return <Redirect to={{pathname: "/admin/login", state: {from: location}}} />; }
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// A wrapper for <Route> that redirects to the unlock
|
||||
// screen if the fling is protected
|
||||
/* A wrapper for <Route> that redirects to the unlock screen if no authorized token
|
||||
* was found.
|
||||
*
|
||||
* Note that the token check is purely client-side. It provides no actual
|
||||
* protection! It is hence possible to reach the target site with some small
|
||||
* amount of trickery. Without a valid token, no meaningful actions are possible
|
||||
* on the target page though - this must be checked server side.
|
||||
*/
|
||||
function UserRoute({ children, ...rest }) {
|
||||
return (
|
||||
<Route
|
||||
{...rest}
|
||||
render={({ match, location }) => {
|
||||
log.info(request.defaults);
|
||||
log.info(match);
|
||||
log.info(location);
|
||||
let x = {from: location, shareId: match.params.shareId};
|
||||
log.debug(`Routing request for ${rest['path']}`);
|
||||
return (
|
||||
<Route
|
||||
{...rest}
|
||||
render={({ match, location }) => {
|
||||
let state = {from: location, shareId: match.params.shareId};
|
||||
let authorized = jwt.hasSubject("admin") || (jwt.hasSubject("user") && jwt.hasClaim("id", state['shareId']));
|
||||
|
||||
if(isOwner()) { return children; }
|
||||
else if(isUser(match.params.shareId)) { return children; }
|
||||
else { return <Redirect to={ {pathname: "/unlock", state: x} } />; }
|
||||
}}
|
||||
/>
|
||||
);
|
||||
if (authorized) { return children; }
|
||||
else { return <Redirect to={ {pathname: "/unlock", state: state} } />; }
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,37 +1,34 @@
|
|||
import log from 'loglevel';
|
||||
import React, {useState, useEffect} from 'react';
|
||||
|
||||
import {flingClient} from '../../util/flingclient';
|
||||
import {FlingClient} from '../../util/fc';
|
||||
|
||||
import FlingTile from './FlingTile';
|
||||
|
||||
export default function FlingList(props) {
|
||||
const [flings, setFlings] = useState([]);
|
||||
useEffect(() => { (async () => {
|
||||
let flings = await flingClient.getFlings();
|
||||
const [flings, setFlings] = useState([]);
|
||||
useEffect(() => {
|
||||
let flingClient = new FlingClient();
|
||||
flingClient.getFlings()
|
||||
.then(flings => {
|
||||
let newFlings = [];
|
||||
|
||||
for (let fling of flings) {
|
||||
let flingTile = <FlingTile fling={fling}
|
||||
key={fling.id}
|
||||
refreshFlingListFn={refreshFlingList} />;
|
||||
newFlings.push(flingTile);
|
||||
let flingTile = <FlingTile fling={fling} key={fling.id} />;
|
||||
newFlings.push(flingTile);
|
||||
}
|
||||
setFlings(newFlings);
|
||||
})(); } , []);
|
||||
}).catch(log.error);
|
||||
}, []);
|
||||
|
||||
return(
|
||||
<div className="panel">
|
||||
{log.info(`Got active fling: ${props.activeFling}`)}
|
||||
<div className="panel-header p-2">
|
||||
<h5>My Flings</h5>
|
||||
</div>
|
||||
<div className="panel-body p-0">
|
||||
{flings}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
async function refreshFlingList() {
|
||||
}
|
||||
return(
|
||||
<div className="panel">
|
||||
{log.info(`Got active fling: ${props.activeFling}`)}
|
||||
<div className="panel-header p-2">
|
||||
<h5>My Flings</h5>
|
||||
</div>
|
||||
<div className="panel-body p-0">
|
||||
{flings}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -51,8 +51,8 @@ export default function Login() {
|
|||
authClient.authenticateOwner(opt)
|
||||
.then(response => {
|
||||
log.info("Login successful");
|
||||
sessionStorage.setItem['token'] = response;
|
||||
log.info("Returning back to", from);
|
||||
sessionStorage.setItem('token', response);
|
||||
log.debug("Returning back to", from);
|
||||
history.replace(from);
|
||||
}).catch(log.error);
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@ let clientConfig = (token) => {
|
|||
let config = new fc.ApiClient();
|
||||
config.basePath = process.env.REACT_APP_API.replace(/\/+$/, '');
|
||||
|
||||
token = token || sessionStorage.getItem['token'];
|
||||
token = token || sessionStorage.getItem('token');
|
||||
if(token) { config.authentications['bearer'].accessToken = token; }
|
||||
|
||||
return config;
|
||||
|
|
26
web/fling/src/util/jwt.js
Normal file
26
web/fling/src/util/jwt.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* Utilities for working with JWT tokens
|
||||
*/
|
||||
import jwtDecode from 'jwt-decode';
|
||||
|
||||
let jwt = {
|
||||
/**
|
||||
* Check the session store token for an arbitrary claim
|
||||
*/
|
||||
hasClaim: function (name, value) {
|
||||
if(!sessionStorage.getItem('token')) return false;
|
||||
let tokenPayload = jwtDecode(sessionStorage.getItem('token'));
|
||||
return tokenPayload[name] === value;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check the session store token for a subject
|
||||
*/
|
||||
hasSubject: function (value) {
|
||||
if(!sessionStorage.getItem('token')) return false;
|
||||
let tokenPayload = jwtDecode(sessionStorage.getItem('token'));
|
||||
return tokenPayload['sub'] === value;
|
||||
}
|
||||
};
|
||||
|
||||
export default jwt;
|
Loading…
Reference in a new issue