add anonymous mode for webclient / change footer and redirects if user is anonymous / add login button if user is anonymous

This commit is contained in:
Eduard Heimbuch
2020-08-03 17:41:40 +02:00
parent b926238e03
commit 4c9e96f7e2
21 changed files with 324 additions and 151 deletions

View File

@@ -29,6 +29,8 @@ import sonia.scm.SCMContext;
public class Authentications {
private Authentications() {}
public static boolean isAuthenticatedSubjectAnonymous() {
return isSubjectAnonymous((String) SecurityUtils.getSubject().getPrincipal());
}

View File

@@ -59,16 +59,21 @@ import java.util.Set;
* @since 2.0.0
*/
@Singleton
public class AuthenticationFilter extends HttpFilter
{
public class AuthenticationFilter extends HttpFilter {
/** marker for failed authentication */
/**
* marker for failed authentication
*/
private static final String ATTRIBUTE_FAILED_AUTH = "sonia.scm.auth.failed";
/** Field description */
/**
* Field description
*/
private static final String HEADER_AUTHORIZATION = "Authorization";
/** the logger for AuthenticationFilter */
/**
* the logger for AuthenticationFilter
*/
private static final Logger logger =
LoggerFactory.getLogger(AuthenticationFilter.class);
@@ -95,38 +100,29 @@ public class AuthenticationFilter extends HttpFilter
* @param request servlet request
* @param response servlet response
* @param chain filter chain
*
* @throws IOException
* @throws ServletException
*/
@Override
protected void doFilter(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException
{
throws IOException, ServletException {
Subject subject = SecurityUtils.getSubject();
AuthenticationToken token = createToken(request);
if (token != null)
{
if (token != null) {
logger.trace(
"found authentication token on request, start authentication");
handleAuthentication(request, response, chain, subject, token);
}
else if (subject.isAuthenticated())
{
} else if (subject.isAuthenticated()) {
logger.trace("user is already authenticated");
processChain(request, response, chain, subject);
}
else if (isAnonymousAccessEnabled() && !HttpUtil.isWUIRequest(request))
{
} else if (isAnonymousAccessEnabled()) {
logger.trace("anonymous access granted");
subject.login(new AnonymousToken());
processChain(request, response, chain, subject);
}
else
{
} else {
logger.trace("could not find user send unauthorized");
handleUnauthorized(request, response, chain);
}
@@ -139,25 +135,19 @@ public class AuthenticationFilter extends HttpFilter
* @param request servlet request
* @param response servlet response
* @param chain filter chain
*
* @throws IOException
* @throws ServletException
*
* @since 1.8
*/
protected void handleUnauthorized(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException
{
throws IOException, ServletException {
// send only forbidden, if the authentication has failed.
// see https://bitbucket.org/sdorra/scm-manager/issue/545/git-clone-with-username-in-url-does-not
if (Boolean.TRUE.equals(request.getAttribute(ATTRIBUTE_FAILED_AUTH)))
{
if (Boolean.TRUE.equals(request.getAttribute(ATTRIBUTE_FAILED_AUTH))) {
sendFailedAuthenticationError(request, response);
}
else
{
} else {
sendUnauthorizedError(request, response);
}
}
@@ -165,16 +155,13 @@ public class AuthenticationFilter extends HttpFilter
/**
* Sends an error for a failed authentication back to client.
*
*
* @param request http request
* @param response http response
*
* @throws IOException
*/
protected void sendFailedAuthenticationError(HttpServletRequest request,
HttpServletResponse response)
throws IOException
{
throws IOException {
HttpUtil.sendUnauthorized(request, response,
configuration.getRealmDescription());
}
@@ -182,16 +169,13 @@ public class AuthenticationFilter extends HttpFilter
/**
* Sends an unauthorized error back to client.
*
*
* @param request http request
* @param response http response
*
* @throws IOException
*/
protected void sendUnauthorizedError(HttpServletRequest request,
HttpServletResponse response)
throws IOException
{
throws IOException {
HttpUtil.sendUnauthorized(request, response,
configuration.getRealmDescription());
}
@@ -200,20 +184,15 @@ public class AuthenticationFilter extends HttpFilter
* Iterates all {@link WebTokenGenerator} and creates an
* {@link AuthenticationToken} from the given request.
*
*
* @param request http servlet request
*
* @return authentication token of {@code null}
*/
private AuthenticationToken createToken(HttpServletRequest request)
{
private AuthenticationToken createToken(HttpServletRequest request) {
AuthenticationToken token = null;
for (WebTokenGenerator generator : tokenGenerators)
{
for (WebTokenGenerator generator : tokenGenerators) {
token = generator.createToken(request);
if (token != null)
{
if (token != null) {
logger.trace("generated web token {} from generator {}",
token.getClass(), generator.getClass());
@@ -227,30 +206,24 @@ public class AuthenticationFilter extends HttpFilter
/**
* Handle authentication with the given {@link AuthenticationToken}.
*
*
* @param request http servlet request
* @param response http servlet response
* @param chain filter chain
* @param subject subject
* @param token authentication token
*
* @throws IOException
* @throws ServletException
*/
private void handleAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Subject subject,
AuthenticationToken token)
throws IOException, ServletException
{
throws IOException, ServletException {
logger.trace("found basic authorization header, start authentication");
try
{
try {
subject.login(token);
processChain(request, response, chain, subject);
}
catch (AuthenticationException ex)
{
} catch (AuthenticationException ex) {
logger.warn("authentication failed", ex);
handleUnauthorized(request, response, chain);
}
@@ -259,33 +232,26 @@ public class AuthenticationFilter extends HttpFilter
/**
* Process the filter chain.
*
*
* @param request http servlet request
* @param response http servlet response
* @param chain filter chain
* @param subject subject
*
* @throws IOException
* @throws ServletException
*/
private void processChain(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Subject subject)
throws IOException, ServletException
{
throws IOException, ServletException {
String username = Util.EMPTY_STRING;
if (!subject.isAuthenticated())
{
if (!subject.isAuthenticated()) {
// anonymous access
username = SCMContext.USER_ANONYMOUS;
}
else
{
} else {
Object obj = subject.getPrincipal();
if (obj != null)
{
if (obj != null) {
username = obj.toString();
}
}
@@ -299,19 +265,21 @@ public class AuthenticationFilter extends HttpFilter
/**
* Returns {@code true} if anonymous access is enabled.
*
*
* @return {@code true} if anonymous access is enabled
*/
private boolean isAnonymousAccessEnabled()
{
private boolean isAnonymousAccessEnabled() {
return (configuration != null) && configuration.getAnonymousMode() != AnonymousMode.OFF;
}
//~--- fields ---------------------------------------------------------------
/** set of web token generators */
/**
* set of web token generators
*/
private final Set<WebTokenGenerator> tokenGenerators;
/** scm main configuration */
/**
* scm main configuration
*/
protected ScmConfiguration configuration;
}

View File

@@ -47,7 +47,7 @@
},
"dependencies": {
"@scm-manager/ui-extensions": "^2.1.0",
"@scm-manager/ui-types": "^2.3.0",
"@scm-manager/ui-types": "^2.4.0-SNAPSHOT",
"classnames": "^2.2.6",
"date-fns": "^2.4.1",
"gitdiff-parser": "^0.1.2",

View File

@@ -89,14 +89,24 @@ const Footer: FC<Props> = ({ me, version, links }) => {
meSectionTile = <TitleWithIcon title={me.displayName} icon="user-circle" />;
}
let meSectionBody = <div />;
{
if (me.name !== "_anonymous")
meSectionBody = (
<>
<NavLink to="/me/settings/password" label={t("profile.changePasswordNavLink")} />
<ExtensionPoint name="profile.setting" props={extensionProps} renderAll={true} />
</>
);
}
return (
<footer className="footer">
<section className="section container">
<div className="columns is-size-7">
<FooterSection title={meSectionTile}>
<NavLink to="/me" label={t("footer.user.profile")} />
<NavLink to="/me/settings/password" label={t("profile.changePasswordNavLink")} />
<ExtensionPoint name="profile.setting" props={extensionProps} renderAll={true} />
{meSectionBody}
</FooterSection>
<FooterSection title={<TitleWithIcon title={t("footer.information.title")} icon="info-circle" />}>
<ExternalNavLink to="https://www.scm-manager.org/" label={`SCM-Manager ${version}`} />

View File

@@ -63,6 +63,23 @@ class PrimaryNavigation extends React.Component<Props> {
}
};
appendLogin = (navigationItems: ReactNode[], append: Appender) => {
const { t, links } = this.props;
const props = {
links,
label: t("primary-navigation.login")
};
if (binder.hasExtension("primary-navigation.login", props)) {
navigationItems.push(
<ExtensionPoint key="primary-navigation.login" name="primary-navigation.login" props={props} />
);
} else {
append("/login", "/login", "primary-navigation.login", "login");
}
};
createNavigationItems = () => {
const navigationItems: ReactNode[] = [];
const { t, links } = this.props;
@@ -95,6 +112,7 @@ class PrimaryNavigation extends React.Component<Props> {
);
this.appendLogout(navigationItems, append);
this.appendLogin(navigationItems, append);
return navigationItems;
};

View File

@@ -25,7 +25,7 @@
"@scm-manager/tsconfig": "^2.1.0",
"@scm-manager/ui-scripts": "^2.1.0",
"@scm-manager/ui-tests": "^2.1.0",
"@scm-manager/ui-types": "^2.3.0",
"@scm-manager/ui-types": "^2.4.0-SNAPSHOT",
"@types/classnames": "^2.2.9",
"@types/enzyme": "^3.10.3",
"@types/fetch-mock": "^7.3.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@scm-manager/ui-types",
"version": "2.3.0",
"version": "2.4.0-SNAPSHOT",
"description": "Flow types for SCM-Manager related Objects",
"main": "src/index.ts",
"files": [

View File

@@ -48,6 +48,7 @@
"repositories": "Repositories",
"users": "Benutzer",
"logout": "Abmelden",
"login": "Anmelden",
"groups": "Gruppen",
"admin": "Administration"
},

View File

@@ -49,6 +49,7 @@
"repositories": "Repositories",
"users": "Users",
"logout": "Logout",
"login": "Login",
"groups": "Groups",
"admin": "Administration"
},

View File

@@ -26,7 +26,7 @@ import { connect } from "react-redux";
import { Redirect, withRouter } from "react-router-dom";
import { compose } from "redux";
import styled from "styled-components";
import { getLoginFailure, isAuthenticated, isLoginPending, login } from "../modules/auth";
import { getLoginFailure, isAnonymous, isLoginPending, login } from "../modules/auth";
import { getLoginInfoLink, getLoginLink } from "../modules/indexResource";
import LoginInfo from "../components/LoginInfo";
@@ -86,7 +86,7 @@ class Login extends React.Component<Props> {
}
const mapStateToProps = (state: any) => {
const authenticated = isAuthenticated(state);
const authenticated = state?.auth?.me && !isAnonymous(state.auth.me);
const loading = isLoginPending(state);
const error = getLoginFailure(state);
const link = getLoginLink(state);

View File

@@ -43,8 +43,10 @@ type Props = WithTranslation & {
class Logout extends React.Component<Props> {
componentDidMount() {
if (this.props.logoutLink) {
this.props.logout(this.props.logoutLink);
}
}
render() {
const { authenticated, redirecting, loading, error, t } = this.props;

View File

@@ -23,7 +23,7 @@
*/
import React from "react";
import { Route, RouteComponentProps, withRouter } from "react-router-dom";
import { getMe } from "../modules/auth";
import { getMe, isAnonymous } from "../modules/auth";
import { compose } from "redux";
import { connect } from "react-redux";
import { WithTranslation, withTranslation } from "react-i18next";
@@ -92,7 +92,9 @@ class Profile extends React.Component<Props> {
<CustomQueryFlexWrappedColumns>
<PrimaryContentColumn>
<Route path={url} exact render={() => <ProfileInfo me={me} />} />
{me?._links?.password && (
<Route path={`${url}/settings/password`} render={() => <ChangeUserPassword me={me} />} />
)}
<ExtensionPoint name="profile.route" props={extensionProps} renderAll={true} />
</PrimaryContentColumn>
<SecondaryNavigationColumn>
@@ -103,6 +105,7 @@ class Profile extends React.Component<Props> {
label={t("profile.informationNavLink")}
title={t("profile.informationNavLink")}
/>
{!isAnonymous(me) && (
<SubNavigation
to={`${url}/settings/password`}
label={t("profile.settingsNavLink")}
@@ -111,6 +114,7 @@ class Profile extends React.Component<Props> {
<NavLink to={`${url}/settings/password`} label={t("profile.changePasswordNavLink")} />
<ExtensionPoint name="profile.setting" props={extensionProps} renderAll={true} />
</SubNavigation>
)}
</SecondaryNavigation>
</SecondaryNavigationColumn>
</CustomQueryFlexWrappedColumns>

View File

@@ -35,6 +35,7 @@ import reducer, {
getLoginFailure,
getLogoutFailure,
getMe,
isAnonymous,
isAuthenticated,
isFetchMePending,
isLoginPending,
@@ -117,7 +118,7 @@ describe("auth actions", () => {
fetchMock.postOnce("/api/v2/auth/access_token", {
body: {
cookie: true,
grant_type: "password",
grantType: "password",
username: "tricia",
password: "secret123"
},
@@ -349,23 +350,6 @@ describe("auth selectors", () => {
).toBe(true);
});
it("should return false if me exist and login Link does exist", () => {
expect(
isAuthenticated({
auth: {
me
},
indexResources: {
links: {
login: {
href: "login.href"
}
}
}
})
).toBe(false);
});
it("should return me", () => {
expect(
getMe({
@@ -500,3 +484,8 @@ describe("auth selectors", () => {
).toBe(true);
});
});
it("should check if current user is anonymous", () => {
expect(isAnonymous({ name: "_anonymous", displayName: "Anon", _links: [], groups: [], mail: "" })).toBeTruthy();
expect(isAnonymous({ name: "scmadmin", displayName: "SCM Admin", _links: [], groups: [], mail: "" })).toBeFalsy();
});

View File

@@ -22,7 +22,7 @@
* SOFTWARE.
*/
import { Me } from "@scm-manager/ui-types";
import { Link, Me } from "@scm-manager/ui-types";
import * as types from "./types";
import { apiClient, UnauthorizedError } from "@scm-manager/ui-components";
@@ -32,9 +32,9 @@ import {
callFetchIndexResources,
fetchIndexResources,
fetchIndexResourcesPending,
fetchIndexResourcesSuccess,
getLoginLink
fetchIndexResourcesSuccess, getLoginLink
} from "./indexResource";
import { AnyAction } from "redux";
// Action
@@ -61,7 +61,7 @@ const initialState = {};
export default function reducer(
state: object = initialState,
action: object = {
action: AnyAction = {
type: "UNKNOWN"
}
) {
@@ -174,23 +174,23 @@ const callFetchMe = (link: string): Promise<Me> => {
};
export const login = (loginLink: string, username: string, password: string) => {
const login_data = {
const loginData = {
cookie: true,
grant_type: "password",
grantType: "password",
username,
password
};
return function(dispatch: any) {
dispatch(loginPending());
return apiClient
.post(loginLink, login_data)
.post(loginLink, loginData)
.then(() => {
dispatch(fetchIndexResourcesPending());
return callFetchIndexResources();
})
.then(response => {
dispatch(fetchIndexResourcesSuccess(response));
const meLink = response._links.me.href;
const meLink = (response._links.me as Link).href;
return callFetchMe(meLink);
})
.then(me => {
@@ -256,17 +256,17 @@ export const logout = (link: string) => {
// selectors
const stateAuth = (state: object): object => {
// @ts-ignore Right types for redux not available
return state.auth || {};
};
export const isAuthenticated = (state: object) => {
if (state.auth.me && !getLoginLink(state)) {
return true;
}
return false;
// @ts-ignore Right types for redux not available
return !!((state.auth.me && !getLoginLink(state)) || isAnonymous(state.auth.me));
};
export const getMe = (state: object): Me => {
// @ts-ignore Right types for redux not available
return stateAuth(state).me;
};
@@ -295,5 +295,12 @@ export const getLogoutFailure = (state: object) => {
};
export const isRedirecting = (state: object) => {
// @ts-ignore Right types for redux not available
return !!stateAuth(state).redirecting;
};
// Helper methods
export const isAnonymous = (me: Me) => {
return me?.name === "_anonymous";
};

View File

@@ -40,6 +40,7 @@ function removeAllEntriesOfIdentifierFromState(state: object, payload: any, iden
const newState = {};
for (const failureType in state) {
if (failureType !== identifier && !failureType.startsWith(identifier)) {
// @ts-ignore Right types not available
newState[failureType] = state[failureType];
}
}
@@ -50,6 +51,7 @@ function removeFromState(state: object, identifier: string) {
const newState = {};
for (const failureType in state) {
if (failureType !== identifier) {
// @ts-ignore Right types not available
newState[failureType] = state[failureType];
}
}
@@ -90,11 +92,13 @@ export default function reducer(
}
export function getFailure(state: object, actionType: string, itemId?: string | number) {
// @ts-ignore Right types not available
if (state.failure) {
let identifier = actionType;
if (itemId) {
identifier += "/" + itemId;
}
// @ts-ignore Right types not available
return state.failure[identifier];
}
}

View File

@@ -111,23 +111,29 @@ export function getFetchIndexResourcesFailure(state: object) {
}
export function getLinks(state: object) {
// @ts-ignore Right types not available
return state.indexResources.links;
}
export function getLink(state: object, name: string) {
// @ts-ignore Right types not available
if (state.indexResources.links && state.indexResources.links[name]) {
// @ts-ignore Right types not available
return state.indexResources.links[name].href;
}
}
export function getLinkCollection(state: object, name: string): Link[] {
// @ts-ignore Right types not available
if (state.indexResources.links && state.indexResources.links[name]) {
// @ts-ignore Right types not available
return state.indexResources.links[name];
}
return [];
}
export function getAppVersion(state: object) {
// @ts-ignore Right types not available
return state.indexResources.version;
}

View File

@@ -32,6 +32,7 @@ function removeFromState(state: object, identifier: string) {
const newState = {};
for (const childType in state) {
if (childType !== identifier) {
// @ts-ignore Right types not available
newState[childType] = state[childType];
}
}
@@ -42,6 +43,7 @@ function removeAllEntriesOfIdentifierFromState(state: object, payload: any, iden
const newState = {};
for (const childType in state) {
if (childType !== identifier && !childType.startsWith(identifier)) {
// @ts-ignore Right types not available
newState[childType] = state[childType];
}
}
@@ -92,6 +94,7 @@ export function isPending(state: object, actionType: string, itemId?: string | n
if (itemId) {
type += "/" + itemId;
}
// @ts-ignore Right types not available
if (state.pending && state.pending[type]) {
return true;
}

View File

@@ -35,6 +35,7 @@ import sonia.scm.config.ConfigurationPermissions;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.group.GroupPermissions;
import sonia.scm.plugin.PluginPermissions;
import sonia.scm.security.AnonymousMode;
import sonia.scm.security.Authentications;
import sonia.scm.security.PermissionPermissions;
import sonia.scm.user.UserPermissions;
@@ -70,7 +71,7 @@ public class IndexDtoGenerator extends HalAppenderMapper {
builder.single(link("loginInfo", loginInfoUrl));
}
if (SecurityUtils.getSubject().isAuthenticated()) {
if (SecurityUtils.getSubject().isAuthenticated() && !Authentications.isAuthenticatedSubjectAnonymous() || isAnonymousAccess()) {
builder.single(link("me", resourceLinks.me().self()));
if (Authentications.isAuthenticatedSubjectAnonymous()) {
@@ -120,4 +121,8 @@ public class IndexDtoGenerator extends HalAppenderMapper {
return new IndexDto(builder.build(), embeddedBuilder.build(), scmContextProvider.getVersion());
}
private boolean isAnonymousAccess() {
return Authentications.isAuthenticatedSubjectAnonymous() && configuration.getAnonymousMode() == AnonymousMode.FULL;
}
}

View File

@@ -83,15 +83,17 @@ public class MeDtoFactory extends HalAppenderMapper {
private MeDto createDto(User user) {
Links.Builder linksBuilder = linkingTo().self(resourceLinks.me().self());
if (isNotAnonymous(user)) {
if (UserPermissions.delete(user).isPermitted()) {
linksBuilder.single(link("delete", resourceLinks.me().delete(user.getName())));
}
if (UserPermissions.modify(user).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.me().update(user.getName())));
}
if (userManager.isTypeDefault(user) && UserPermissions.changePassword(user).isPermitted() && !Authentications.isSubjectAnonymous(user.getName())) {
if (userManager.isTypeDefault(user) && UserPermissions.changePassword(user).isPermitted()) {
linksBuilder.single(link("password", resourceLinks.me().passwordChange()));
}
}
Embedded.Builder embeddedBuilder = embeddedBuilder();
applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), new Me(), user);
@@ -99,4 +101,7 @@ public class MeDtoFactory extends HalAppenderMapper {
return new MeDto(linksBuilder.build(), embeddedBuilder.build());
}
private boolean isNotAnonymous(User user) {
return !Authentications.isSubjectAnonymous(user.getName());
}
}

View File

@@ -0,0 +1,137 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.BasicContextProvider;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.AnonymousMode;
import java.net.URI;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import static sonia.scm.SCMContext.USER_ANONYMOUS;
@ExtendWith(MockitoExtension.class)
class IndexDtoGeneratorTest {
private static final ScmPathInfo scmPathInfo = () -> URI.create("/api/v2");
@Mock
private ScmConfiguration configuration;
@Mock
private BasicContextProvider contextProvider;
@Mock
private ResourceLinks resourceLinks;
@Mock
private Subject subject;
@InjectMocks
private IndexDtoGenerator generator;
@BeforeEach
void bindSubject() {
ThreadContext.bind(subject);
}
@AfterEach
void tearDownSubject() {
ThreadContext.unbindSubject();
}
@Test
void shouldAppendMeIfAuthenticated() {
mockSubjectRelatedResourceLinks();
when(subject.isAuthenticated()).thenReturn(true);
when(contextProvider.getVersion()).thenReturn("2.x");
IndexDto dto = generator.generate();
assertThat(dto.getLinks().getLinkBy("me")).isPresent();
}
@Test
void shouldNotAppendMeIfUserIsAuthenticatedButAnonymous() {
mockResourceLinks();
when(subject.getPrincipal()).thenReturn(USER_ANONYMOUS);
when(subject.isAuthenticated()).thenReturn(true);
IndexDto dto = generator.generate();
assertThat(dto.getLinks().getLinkBy("me")).isNotPresent();
}
@Test
void shouldAppendMeIfUserIsAnonymousAndAnonymousModeIsFullEnabled() {
mockSubjectRelatedResourceLinks();
when(subject.getPrincipal()).thenReturn(USER_ANONYMOUS);
when(subject.isAuthenticated()).thenReturn(true);
when(configuration.getAnonymousMode()).thenReturn(AnonymousMode.FULL);
IndexDto dto = generator.generate();
assertThat(dto.getLinks().getLinkBy("me")).isPresent();
}
@Test
void shouldNotAppendMeIfUserIsAnonymousAndAnonymousModeIsProtocolOnly() {
mockResourceLinks();
when(subject.getPrincipal()).thenReturn(USER_ANONYMOUS);
when(subject.isAuthenticated()).thenReturn(true);
when(configuration.getAnonymousMode()).thenReturn(AnonymousMode.PROTOCOL_ONLY);
IndexDto dto = generator.generate();
assertThat(dto.getLinks().getLinkBy("me")).isNotPresent();
}
private void mockResourceLinks() {
when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(scmPathInfo));
when(resourceLinks.uiPluginCollection()).thenReturn(new ResourceLinks.UIPluginCollectionLinks(scmPathInfo));
when(resourceLinks.authentication()).thenReturn(new ResourceLinks.AuthenticationLinks(scmPathInfo));
}
private void mockSubjectRelatedResourceLinks() {
mockResourceLinks();
when(resourceLinks.repositoryCollection()).thenReturn(new ResourceLinks.RepositoryCollectionLinks(scmPathInfo));
when(resourceLinks.repositoryVerbs()).thenReturn(new ResourceLinks.RepositoryVerbLinks(scmPathInfo));
when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(scmPathInfo));
when(resourceLinks.repositoryRoleCollection()).thenReturn(new ResourceLinks.RepositoryRoleCollectionLinks(scmPathInfo));
when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(scmPathInfo));
when(resourceLinks.me()).thenReturn(new ResourceLinks.MeLinks(scmPathInfo, new ResourceLinks.UserLinks(scmPathInfo)));
}
}

View File

@@ -198,6 +198,19 @@ class MeDtoFactoryTest {
assertThat(dto.getLinks().getLinkBy("password")).isNotPresent();
}
@Test
void shouldAppendOnlySelfLinkIfAnonymousUser() {
User user = SCMContext.ANONYMOUS;
prepareSubject(user);
MeDto dto = meDtoFactory.create();
assertThat(dto.getLinks().getLinkBy("self")).isPresent();
assertThat(dto.getLinks().getLinkBy("password")).isNotPresent();
assertThat(dto.getLinks().getLinkBy("delete")).isNotPresent();
assertThat(dto.getLinks().getLinkBy("update")).isNotPresent();
}
@Test
void shouldAppendLinks() {
prepareSubject(UserTestData.createTrillian());
@@ -213,6 +226,4 @@ class MeDtoFactoryTest {
MeDto dto = meDtoFactory.create();
assertThat(dto.getLinks().getLinkBy("profile").get().getHref()).isEqualTo("http://hitchhiker.com/users/trillian");
}
}