mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-09 23:15:43 +01:00
add restentpoint for login/logout, restructuring of modules and components, add flow usage
This commit is contained in:
@@ -35,3 +35,4 @@ scm-ui/yarn.lock
|
||||
scm-ui/.gitignore
|
||||
scm-ui/package-lock.json
|
||||
node_modules
|
||||
scm-ui/.flowconfig
|
||||
|
||||
1
pom.xml
1
pom.xml
@@ -70,6 +70,7 @@
|
||||
<module>scm-test</module>
|
||||
<module>scm-plugins</module>
|
||||
<module>scm-dao-xml</module>
|
||||
<module>scm-ui</module>
|
||||
<module>scm-webapp</module>
|
||||
<module>scm-server</module>
|
||||
<module>scm-clients</module>
|
||||
|
||||
128
scm-ui/flow-typed/npm/history_v4.x.x.js
vendored
Normal file
128
scm-ui/flow-typed/npm/history_v4.x.x.js
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
// flow-typed signature: eb8bd974b677b08dfca89de9ac05b60b
|
||||
// flow-typed version: 43b30482ac/history_v4.x.x/flow_>=v0.25.x
|
||||
|
||||
declare module "history/createBrowserHistory" {
|
||||
declare function Unblock(): void;
|
||||
|
||||
declare export type Action = "PUSH" | "REPLACE" | "POP";
|
||||
|
||||
declare export type BrowserLocation = {
|
||||
pathname: string,
|
||||
search: string,
|
||||
hash: string,
|
||||
// Browser and Memory specific
|
||||
state: string,
|
||||
key: string,
|
||||
};
|
||||
|
||||
declare export type BrowserHistory = {
|
||||
length: number,
|
||||
location: BrowserLocation,
|
||||
action: Action,
|
||||
push: (path: string, Array<mixed>) => void,
|
||||
replace: (path: string, Array<mixed>) => void,
|
||||
go: (n: number) => void,
|
||||
goBack: () => void,
|
||||
goForward: () => void,
|
||||
listen: Function,
|
||||
block: (message: string) => Unblock,
|
||||
block: ((location: BrowserLocation, action: Action) => string) => Unblock,
|
||||
push: (path: string) => void,
|
||||
replace: (path: string) => void,
|
||||
};
|
||||
|
||||
declare type HistoryOpts = {
|
||||
basename?: string,
|
||||
forceRefresh?: boolean,
|
||||
getUserConfirmation?: (
|
||||
message: string,
|
||||
callback: (willContinue: boolean) => void,
|
||||
) => void,
|
||||
};
|
||||
|
||||
declare export default (opts?: HistoryOpts) => BrowserHistory;
|
||||
}
|
||||
|
||||
declare module "history/createMemoryHistory" {
|
||||
declare function Unblock(): void;
|
||||
|
||||
declare export type Action = "PUSH" | "REPLACE" | "POP";
|
||||
|
||||
declare export type MemoryLocation = {
|
||||
pathname: string,
|
||||
search: string,
|
||||
hash: string,
|
||||
// Browser and Memory specific
|
||||
state: string,
|
||||
key: string,
|
||||
};
|
||||
|
||||
declare export type MemoryHistory = {
|
||||
length: number,
|
||||
location: MemoryLocation,
|
||||
action: Action,
|
||||
index: number,
|
||||
entries: Array<string>,
|
||||
push: (path: string, Array<mixed>) => void,
|
||||
replace: (path: string, Array<mixed>) => void,
|
||||
go: (n: number) => void,
|
||||
goBack: () => void,
|
||||
goForward: () => void,
|
||||
// Memory only
|
||||
canGo: (n: number) => boolean,
|
||||
listen: Function,
|
||||
block: (message: string) => Unblock,
|
||||
block: ((location: MemoryLocation, action: Action) => string) => Unblock,
|
||||
push: (path: string) => void,
|
||||
};
|
||||
|
||||
declare type HistoryOpts = {
|
||||
initialEntries?: Array<string>,
|
||||
initialIndex?: number,
|
||||
keyLength?: number,
|
||||
getUserConfirmation?: (
|
||||
message: string,
|
||||
callback: (willContinue: boolean) => void,
|
||||
) => void,
|
||||
};
|
||||
|
||||
declare export default (opts?: HistoryOpts) => MemoryHistory;
|
||||
}
|
||||
|
||||
declare module "history/createHashHistory" {
|
||||
declare function Unblock(): void;
|
||||
|
||||
declare export type Action = "PUSH" | "REPLACE" | "POP";
|
||||
|
||||
declare export type HashLocation = {
|
||||
pathname: string,
|
||||
search: string,
|
||||
hash: string,
|
||||
};
|
||||
|
||||
declare export type HashHistory = {
|
||||
length: number,
|
||||
location: HashLocation,
|
||||
action: Action,
|
||||
push: (path: string, Array<mixed>) => void,
|
||||
replace: (path: string, Array<mixed>) => void,
|
||||
go: (n: number) => void,
|
||||
goBack: () => void,
|
||||
goForward: () => void,
|
||||
listen: Function,
|
||||
block: (message: string) => Unblock,
|
||||
block: ((location: HashLocation, action: Action) => string) => Unblock,
|
||||
push: (path: string) => void,
|
||||
};
|
||||
|
||||
declare type HistoryOpts = {
|
||||
basename?: string,
|
||||
hashType: "slash" | "noslash" | "hashbang",
|
||||
getUserConfirmation?: (
|
||||
message: string,
|
||||
callback: (willContinue: boolean) => void,
|
||||
) => void,
|
||||
};
|
||||
|
||||
declare export default (opts?: HistoryOpts) => HashHistory;
|
||||
}
|
||||
@@ -3,25 +3,30 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"ces-theme": "https://github.com/cloudogu/ces-theme.git",
|
||||
"classnames": "^2.2.5",
|
||||
"flow-bin": "^0.75.0",
|
||||
"history": "^4.7.2",
|
||||
"react": "^16.4.1",
|
||||
"react-dom": "^16.4.1",
|
||||
"react-jss": "^8.6.0",
|
||||
"react-redux": "^5.0.7",
|
||||
"react-scripts": "1.1.4",
|
||||
"redux": "^4.0.0",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"history": "^4.7.2",
|
||||
"react-router-dom": "^4.3.1",
|
||||
"react-router-redux": "^5.0.0-alpha.9",
|
||||
"redux-devtools-extension": "^2.13.5"
|
||||
"react-scripts": "1.1.4",
|
||||
"redux": "^4.0.0",
|
||||
"redux-devtools-extension": "^2.13.5",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject"
|
||||
"eject": "react-scripts eject",
|
||||
"flow": "flow"
|
||||
},
|
||||
"proxy": "http://localhost:8081/scm",
|
||||
"devDependencies": {
|
||||
"prettier": "^1.13.7"
|
||||
}
|
||||
}
|
||||
|
||||
18
scm-ui/pom.xml
Normal file
18
scm-ui/pom.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>sonia.scm</groupId>
|
||||
<artifactId>scm</artifactId>
|
||||
<version>2.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<groupId>sonia.scm.clients</groupId>
|
||||
<artifactId>scm-ui</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<version>2.0.0-SNAPSHOT</version>
|
||||
<name>scm-ui</name>
|
||||
|
||||
</project>
|
||||
@@ -1,20 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import Navigation from './Navigation';
|
||||
import Main from './Main';
|
||||
import {withRouter} from 'react-router-dom';
|
||||
import 'ces-theme/dist/css/ces.css';
|
||||
|
||||
|
||||
|
||||
class App extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="App">
|
||||
<Navigation />
|
||||
<Main />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(App);
|
||||
64
scm-ui/src/apiclient.js
Normal file
64
scm-ui/src/apiclient.js
Normal file
@@ -0,0 +1,64 @@
|
||||
// @flow
|
||||
|
||||
// get api base url from environment
|
||||
const apiUrl = process.env.API_URL || process.env.PUBLIC_URL || "";
|
||||
|
||||
export const PAGE_NOT_FOUND_ERROR = Error("page not found");
|
||||
|
||||
// fetch does not send the X-Requested-With header (https://github.com/github/fetch/issues/17),
|
||||
// but we need the header to detect ajax request (AjaxAwareAuthenticationRedirectStrategy).
|
||||
const fetchOptions: RequestOptions = {
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest"
|
||||
}
|
||||
};
|
||||
|
||||
function handleStatusCode(response: Response) {
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
return response;
|
||||
}
|
||||
if (response.status === 404) {
|
||||
throw PAGE_NOT_FOUND_ERROR;
|
||||
}
|
||||
throw new Error("server returned status code " + response.status);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
function createUrl(url: string) {
|
||||
return `${apiUrl}/api/rest/v2/${url}`;
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
get(url: string) {
|
||||
return fetch(createUrl(url), fetchOptions).then(handleStatusCode);
|
||||
}
|
||||
|
||||
post(url: string, payload: any) {
|
||||
return this.httpRequestWithJSONBody(url, payload, "POST");
|
||||
}
|
||||
|
||||
delete(url: string, payload: any) {
|
||||
let options: RequestOptions = {
|
||||
method: "DELETE"
|
||||
};
|
||||
options = Object.assign(options, fetchOptions);
|
||||
return fetch(createUrl(url), options).then(handleStatusCode);
|
||||
}
|
||||
|
||||
httpRequestWithJSONBody(url: string, payload: any, method: string) {
|
||||
let options: RequestOptions = {
|
||||
method: method,
|
||||
body: JSON.stringify(payload)
|
||||
};
|
||||
options = Object.assign(options, fetchOptions);
|
||||
// $FlowFixMe
|
||||
options.headers["Content-Type"] = "application/json";
|
||||
|
||||
return fetch(createUrl(url), options).then(handleStatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
export let apiClient = new ApiClient();
|
||||
35
scm-ui/src/containers/App.js
Normal file
35
scm-ui/src/containers/App.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import React, { Component } from "react";
|
||||
import Navigation from "./Navigation";
|
||||
import Main from "./Main";
|
||||
import Login from "./Login";
|
||||
import { withRouter } from "react-router-dom";
|
||||
|
||||
type Props = {
|
||||
login: boolean
|
||||
}
|
||||
|
||||
class App extends Component {
|
||||
|
||||
render() {
|
||||
|
||||
const { login} = this.props;
|
||||
|
||||
if(login) {
|
||||
return (
|
||||
<div>
|
||||
<Login/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<div className="App">
|
||||
<Navigation />
|
||||
<Main />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(App);
|
||||
@@ -5,8 +5,8 @@ import classNames from 'classnames';
|
||||
|
||||
import { Route, withRouter } from 'react-router';
|
||||
|
||||
import Repositories from './containers/Repositories';
|
||||
import Users from './containers/Users';
|
||||
import Repositories from '../repositories/containers/Repositories';
|
||||
import Users from '../users/containers/Users';
|
||||
import {Switch} from 'react-router-dom';
|
||||
|
||||
const styles = {
|
||||
@@ -1,12 +1,15 @@
|
||||
import thunk from 'redux-thunk';
|
||||
import logger from 'redux-logger';
|
||||
import { createStore, compose, applyMiddleware, combineReducers } from 'redux';
|
||||
import { routerReducer, routerMiddleware } from 'react-router-redux';
|
||||
// @flow
|
||||
import thunk from "redux-thunk";
|
||||
import logger from "redux-logger";
|
||||
import { createStore, compose, applyMiddleware, combineReducers } from "redux";
|
||||
import { routerReducer, routerMiddleware } from "react-router-redux";
|
||||
|
||||
import repositories from './modules/repositories';
|
||||
import users from './modules/users';
|
||||
import repositories from "./repositories/modules/repositories";
|
||||
import users from "./users/modules/users";
|
||||
|
||||
function createReduxStore(history) {
|
||||
import type {BrowserHistory} from "history/createBrowserHistory";
|
||||
|
||||
function createReduxStore(history: BrowserHistory) {
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
|
||||
const reducer = combineReducers({
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
import registerServiceWorker from './registerServiceWorker';
|
||||
// @flow
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import App from "./containers/App";
|
||||
import registerServiceWorker from "./registerServiceWorker";
|
||||
|
||||
import { Provider } from 'react-redux';
|
||||
import createHistory from 'history/createBrowserHistory';
|
||||
import createReduxStore from './createReduxStore';
|
||||
import { ConnectedRouter } from 'react-router-redux';
|
||||
import { Provider } from "react-redux";
|
||||
import createHistory from "history/createBrowserHistory";
|
||||
|
||||
import type { BrowserHistory } from "history/createBrowserHistory";
|
||||
|
||||
import createReduxStore from "./createReduxStore";
|
||||
import { ConnectedRouter } from "react-router-redux";
|
||||
|
||||
const publicUrl: string = process.env.PUBLIC_URL || "";
|
||||
|
||||
// Create a history of your choosing (we're using a browser history in this case)
|
||||
const history = createHistory({
|
||||
basename: process.env.PUBLIC_URL
|
||||
const history: BrowserHistory = createHistory({
|
||||
basename: publicUrl
|
||||
});
|
||||
|
||||
window.appHistory = history;
|
||||
// Add the reducer to your store on the `router` key
|
||||
// Also apply our middleware for navigating
|
||||
const store = createReduxStore(history);
|
||||
|
||||
const root = document.getElementById("root");
|
||||
if (!root) {
|
||||
throw new Error("could not find root element");
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
{/* ConnectedRouter will use the store from Provider automatically */}
|
||||
@@ -26,7 +35,7 @@ ReactDOM.render(
|
||||
<App />
|
||||
</ConnectedRouter>
|
||||
</Provider>,
|
||||
document.getElementById('root')
|
||||
root
|
||||
);
|
||||
|
||||
registerServiceWorker();
|
||||
|
||||
@@ -3,12 +3,14 @@ import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { fetchRepositoriesIfNeeded } from '../modules/repositories';
|
||||
import Login from '../Login';
|
||||
import Login from '../../containers/Login';
|
||||
|
||||
|
||||
type Props = {
|
||||
login: boolean,
|
||||
error: any,
|
||||
error: Error,
|
||||
repositories: any,
|
||||
fetchRepositoriesIfNeeded: () => void
|
||||
}
|
||||
|
||||
class Repositories extends React.Component<Props> {
|
||||
@@ -21,15 +23,6 @@ class Repositories extends React.Component<Props> {
|
||||
const { login, error, repositories } = this.props;
|
||||
|
||||
|
||||
if(login) {
|
||||
return (
|
||||
<div>
|
||||
<h1>SCM</h1>
|
||||
<Login/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else if(!login){
|
||||
return (
|
||||
<div>
|
||||
<h1>SCM</h1>
|
||||
@@ -38,8 +31,7 @@ class Repositories extends React.Component<Props> {
|
||||
Users hier!
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
//@flow
|
||||
const FETCH_REPOSITORIES = 'scm/repositories/FETCH';
|
||||
const FETCH_REPOSITORIES_SUCCESS = 'scm/repositories/FETCH_SUCCESS';
|
||||
const FETCH_REPOSITORIES_FAILURE = 'scm/repositories/FETCH_FAILURE';
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { fetchUsersIfNeeded } from '../modules/users';
|
||||
import Login from '../Login';
|
||||
import Login from '../../containers/Login';
|
||||
|
||||
type Props = {
|
||||
login: boolean,
|
||||
@@ -22,23 +22,13 @@ class Users extends React.Component<Props> {
|
||||
const { login, error, users } = this.props;
|
||||
|
||||
|
||||
|
||||
if(login) {
|
||||
return (
|
||||
<div>
|
||||
<h1>SCM</h1>
|
||||
<Login/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else if(!login){
|
||||
return (
|
||||
<div>
|
||||
<h1>SCM</h1>
|
||||
<h2>Users</h2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
//@flow
|
||||
const FETCH_USERS = 'scm/users/FETCH';
|
||||
const FETCH_USERS_SUCCESS= 'scm/users/FETCH_SUCCESS';
|
||||
const FETCH_USERS_FAILURE = 'scm/users/FETCH_FAILURE';
|
||||
|
||||
const THRESHOLD_TIMESTAMP = 10000;
|
||||
|
||||
function requestUsers() {
|
||||
return {
|
||||
type: FETCH_USERS
|
||||
@@ -0,0 +1,269 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.inject.Inject;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.authc.AuthenticationException;
|
||||
import org.apache.shiro.authc.DisabledAccountException;
|
||||
import org.apache.shiro.authc.ExcessiveAttemptsException;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.api.rest.RestActionResult;
|
||||
import sonia.scm.security.*;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by masuewer on 04.07.18.
|
||||
*/
|
||||
@Path(AuthenticationResource.PATH)
|
||||
public class AuthenticationResource {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AuthenticationResource.class);
|
||||
|
||||
public static final String PATH = "v2/auth";
|
||||
|
||||
private final AccessTokenBuilderFactory tokenBuilderFactory;
|
||||
private final AccessTokenCookieIssuer cookieIssuer;
|
||||
|
||||
@Inject
|
||||
public AuthenticationResource(AccessTokenBuilderFactory tokenBuilderFactory, AccessTokenCookieIssuer cookieIssuer)
|
||||
{
|
||||
this.tokenBuilderFactory = tokenBuilderFactory;
|
||||
this.cookieIssuer = cookieIssuer;
|
||||
}
|
||||
|
||||
|
||||
@POST
|
||||
@Path("access_token")
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "bad request, required parameter is missing"),
|
||||
@ResponseCode(code = 401, condition = "unauthorized, the specified username or password is wrong"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response authenticateViaForm(
|
||||
@Context HttpServletRequest request,
|
||||
@Context HttpServletResponse response,
|
||||
@BeanParam AuthenticationRequest authentication
|
||||
) {
|
||||
return authenticate(request, response, authentication);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("access_token")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "bad request, required parameter is missing"),
|
||||
@ResponseCode(code = 401, condition = "unauthorized, the specified username or password is wrong"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response authenticateViaJSONBody(
|
||||
@Context HttpServletRequest request,
|
||||
@Context HttpServletResponse response,
|
||||
AuthenticationRequest authentication
|
||||
) {
|
||||
return authenticate(request, response, authentication);
|
||||
}
|
||||
|
||||
private Response authenticate(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
AuthenticationRequest authentication
|
||||
) {
|
||||
authentication.validate();
|
||||
|
||||
Response res;
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
|
||||
try
|
||||
{
|
||||
subject.login(Tokens.createAuthenticationToken(request, authentication.getUsername(), authentication.getPassword()));
|
||||
|
||||
AccessTokenBuilder tokenBuilder = tokenBuilderFactory.create();
|
||||
if ( authentication.getScope() != null ) {
|
||||
tokenBuilder.scope(Scope.valueOf(authentication.getScope()));
|
||||
}
|
||||
|
||||
AccessToken token = tokenBuilder.build();
|
||||
|
||||
if (authentication.isCookie()) {
|
||||
cookieIssuer.authenticate(request, response, token);
|
||||
res = Response.noContent().build();
|
||||
} else {
|
||||
res = Response.ok( token.compact() ).build();
|
||||
}
|
||||
}
|
||||
catch (DisabledAccountException ex)
|
||||
{
|
||||
if (LOG.isTraceEnabled())
|
||||
{
|
||||
LOG.trace(
|
||||
"authentication failed, account user ".concat(authentication.getUsername()).concat(
|
||||
" is locked"), ex);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("authentication failed, account {} is locked", authentication.getUsername());
|
||||
}
|
||||
|
||||
res = handleFailedAuthentication(request, ex, Response.Status.FORBIDDEN,
|
||||
WUIAuthenticationFailure.LOCKED);
|
||||
}
|
||||
catch (ExcessiveAttemptsException ex)
|
||||
{
|
||||
if (LOG.isTraceEnabled())
|
||||
{
|
||||
LOG.trace(
|
||||
"authentication failed, account user ".concat(authentication.getUsername()).concat(
|
||||
" is temporary locked"), ex);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("authentication failed, account {} is temporary locked", authentication.getUsername());
|
||||
}
|
||||
|
||||
res = handleFailedAuthentication(request, ex, Response.Status.FORBIDDEN,
|
||||
WUIAuthenticationFailure.TEMPORARY_LOCKED);
|
||||
}
|
||||
catch (AuthenticationException ex)
|
||||
{
|
||||
if (LOG.isTraceEnabled())
|
||||
{
|
||||
LOG.trace("authentication failed for user ".concat(authentication.getUsername()), ex);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("authentication failed for user {}", authentication.getUsername());
|
||||
}
|
||||
|
||||
res = handleFailedAuthentication(request, ex, Response.Status.UNAUTHORIZED,
|
||||
WUIAuthenticationFailure.WRONG_CREDENTIALS);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("access_token")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 204, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response logout(@Context HttpServletRequest request, @Context HttpServletResponse response)
|
||||
{
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
|
||||
subject.logout();
|
||||
|
||||
// remove authentication cookie
|
||||
cookieIssuer.invalidate(request, response);
|
||||
|
||||
// TODO anonymous access ??
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
public static class AuthenticationRequest {
|
||||
|
||||
@FormParam("grant_type")
|
||||
@JsonProperty("grant_type")
|
||||
private String grantType;
|
||||
|
||||
@FormParam("username")
|
||||
private String username;
|
||||
|
||||
@FormParam("password")
|
||||
private String password;
|
||||
|
||||
@FormParam("cookie")
|
||||
private boolean cookie;
|
||||
|
||||
@FormParam("scope")
|
||||
private List<String> scope;
|
||||
|
||||
public String getGrantType() {
|
||||
return grantType;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public boolean isCookie() {
|
||||
return cookie;
|
||||
}
|
||||
|
||||
public List<String> getScope() {
|
||||
return scope;
|
||||
}
|
||||
|
||||
public void validate() {
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(grantType), "grant_type parameter is required");
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(username), "username parameter is required");
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(password), "password parameter is required");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private Response handleFailedAuthentication(HttpServletRequest request,
|
||||
AuthenticationException ex, Response.Status status,
|
||||
WUIAuthenticationFailure failure) {
|
||||
Response response;
|
||||
|
||||
if (HttpUtil.isWUIRequest(request)) {
|
||||
response = Response.ok(new WUIAuthenticationFailedResult(failure,
|
||||
ex.getMessage())).build();
|
||||
} else {
|
||||
response = Response.status(status).build();
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private enum WUIAuthenticationFailure { LOCKED, TEMPORARY_LOCKED, WRONG_CREDENTIALS }
|
||||
|
||||
@XmlRootElement(name = "result")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
private static final class WUIAuthenticationFailedResult extends RestActionResult {
|
||||
|
||||
private final WUIAuthenticationFailure failure;
|
||||
private final String message;
|
||||
|
||||
public WUIAuthenticationFailedResult(WUIAuthenticationFailure failure, String message) {
|
||||
super(false);
|
||||
this.failure = failure;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public WUIAuthenticationFailure getFailure() {
|
||||
return failure;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import org.apache.shiro.subject.Subject;
|
||||
import sonia.scm.Priority;
|
||||
import sonia.scm.SCMContext;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.security.SecurityRequests;
|
||||
import sonia.scm.web.filter.HttpFilter;
|
||||
import sonia.scm.web.filter.SecurityHttpServletRequestWrapper;
|
||||
|
||||
@@ -72,6 +73,8 @@ public class SecurityFilter extends HttpFilter
|
||||
/** Field description */
|
||||
public static final String URL_AUTHENTICATION = "/api/rest/auth";
|
||||
|
||||
public static final String URLV2_AUTHENTICATION = "/api/rest/v2/auth";
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -104,10 +107,7 @@ public class SecurityFilter extends HttpFilter
|
||||
HttpServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException
|
||||
{
|
||||
String uri =
|
||||
request.getRequestURI().substring(request.getContextPath().length());
|
||||
|
||||
if (!uri.startsWith(URL_AUTHENTICATION))
|
||||
if (!SecurityRequests.isAuthenticationRequest(request))
|
||||
{
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
if (hasPermission(subject))
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package sonia.scm.security;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Created by masuewer on 04.07.18.
|
||||
*/
|
||||
public final class SecurityRequests {
|
||||
|
||||
private static final Pattern URI_LOGIN_PATTERN = Pattern.compile("/api/rest(?:/v2)?/auth/access_token");
|
||||
|
||||
private SecurityRequests() {}
|
||||
|
||||
public static boolean isAuthenticationRequest(HttpServletRequest request) {
|
||||
String uri = request.getRequestURI().substring(request.getContextPath().length());
|
||||
return isAuthenticationRequest(uri);
|
||||
}
|
||||
|
||||
public static boolean isAuthenticationRequest(String uri) {
|
||||
return URI_LOGIN_PATTERN.matcher(uri).matches();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -36,24 +36,22 @@ package sonia.scm.web.security;
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.inject.Inject;
|
||||
|
||||
import sonia.scm.Priority;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.filter.Filters;
|
||||
import sonia.scm.filter.WebElement;
|
||||
import sonia.scm.web.filter.AuthenticationFilter;
|
||||
import sonia.scm.security.SecurityRequests;
|
||||
import sonia.scm.web.WebTokenGenerator;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import java.util.Set;
|
||||
import sonia.scm.web.filter.AuthenticationFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Set;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Filter to handle authentication for the rest api of SCM-Manager.
|
||||
@@ -66,9 +64,6 @@ import javax.servlet.http.HttpServletResponse;
|
||||
public class ApiAuthenticationFilter extends AuthenticationFilter
|
||||
{
|
||||
|
||||
/** login uri */
|
||||
public static final String URI_LOGIN = "/api/rest/auth/access_token";
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -104,7 +99,7 @@ public class ApiAuthenticationFilter extends AuthenticationFilter
|
||||
throws IOException, ServletException
|
||||
{
|
||||
// skip filter on login resource
|
||||
if (request.getRequestURI().contains(URI_LOGIN))
|
||||
if (SecurityRequests.isAuthenticationRequest(request))
|
||||
{
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package sonia.scm.security;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Created by masuewer on 04.07.18.
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class SecurityRequestsTest {
|
||||
|
||||
@Mock
|
||||
private HttpServletRequest request;
|
||||
|
||||
@Test
|
||||
public void testIsAuthenticationRequestWithContextPath() {
|
||||
when(request.getRequestURI()).thenReturn("/scm/api/rest/auth/access_token");
|
||||
when(request.getContextPath()).thenReturn("/scm");
|
||||
|
||||
assertTrue(SecurityRequests.isAuthenticationRequest(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsAuthenticationRequest() throws Exception {
|
||||
assertTrue(SecurityRequests.isAuthenticationRequest("/api/rest/auth/access_token"));
|
||||
assertTrue(SecurityRequests.isAuthenticationRequest("/api/rest/v2/auth/access_token"));
|
||||
assertFalse(SecurityRequests.isAuthenticationRequest("/api/rest/repositories"));
|
||||
assertFalse(SecurityRequests.isAuthenticationRequest("/api/rest/v2/repositories"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user