add restentpoint for login/logout, restructuring of modules and components, add flow usage

This commit is contained in:
Maren Süwer
2018-07-04 16:43:46 +02:00
parent 85902010ce
commit 3cc87ede73
22 changed files with 646 additions and 99 deletions

View File

@@ -35,3 +35,4 @@ scm-ui/yarn.lock
scm-ui/.gitignore
scm-ui/package-lock.json
node_modules
scm-ui/.flowconfig

View File

@@ -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
View 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;
}

View File

@@ -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
View 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>

View File

@@ -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
View 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();

View 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);

View File

@@ -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 = {

View File

@@ -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({

View File

@@ -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();

View File

@@ -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>
);
}
)
}

View File

@@ -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';

View File

@@ -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>
);
}
}
}

View File

@@ -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

View File

@@ -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;
}
}
}

View File

@@ -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))

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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"));
}
}