Merged in feature/anonymous_access (pull request #333)

Feature/anonymous access
This commit is contained in:
Rene Pfeuffer
2019-10-17 10:09:17 +00:00
41 changed files with 663 additions and 164 deletions

View File

@@ -39,7 +39,7 @@ import sonia.scm.user.User;
import sonia.scm.util.ServiceUtil;
/**
* The SCMConext searches a implementation of {@link SCMContextProvider} and
* The SCMContext searches a implementation of {@link SCMContextProvider} and
* holds a singleton instance of this implementation.
*
* @author Sebastian Sdorra
@@ -51,7 +51,7 @@ public final class SCMContext
public static final String DEFAULT_PACKAGE = "sonia.scm";
/** Name of the anonymous user */
public static final String USER_ANONYMOUS = "anonymous";
public static final String USER_ANONYMOUS = "_anonymous";
/**
* the anonymous user

View File

@@ -0,0 +1,33 @@
package sonia.scm.config;
import com.github.legman.Subscribe;
import com.google.inject.Inject;
import sonia.scm.EagerSingleton;
import sonia.scm.SCMContext;
import sonia.scm.plugin.Extension;
import sonia.scm.user.UserManager;
@Extension
@EagerSingleton
public class ScmConfigurationChangedListener {
private UserManager userManager;
@Inject
public ScmConfigurationChangedListener(UserManager userManager) {
this.userManager = userManager;
}
@Subscribe
public void handleEvent(ScmConfigurationChangedEvent event) {
createAnonymousUserIfRequired(event);
}
private void createAnonymousUserIfRequired(ScmConfigurationChangedEvent event) {
if (event.getConfiguration().isAnonymousAccessEnabled() && !userManager.contains(SCMContext.USER_ANONYMOUS)) {
userManager.create(SCMContext.ANONYMOUS);
}
}
}

View File

@@ -0,0 +1,16 @@
package sonia.scm.security;
import org.apache.shiro.authc.AuthenticationToken;
public class AnonymousToken implements AuthenticationToken {
//Anonymous Token does not need an implementation
@Override
public Object getPrincipal() {
return null;
}
@Override
public Object getCredentials() {
return null;
}
}

View File

@@ -0,0 +1,15 @@
package sonia.scm.security;
import org.apache.shiro.SecurityUtils;
import sonia.scm.SCMContext;
public class Authentications {
public static boolean isAuthenticatedSubjectAnonymous() {
return isSubjectAnonymous((String) SecurityUtils.getSubject().getPrincipal());
}
public static boolean isSubjectAnonymous(String principal) {
return SCMContext.USER_ANONYMOUS.equals(principal);
}
}

View File

@@ -37,31 +37,27 @@ package sonia.scm.web.filter;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.AnonymousToken;
import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util;
import sonia.scm.web.WebTokenGenerator;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import java.util.Set;
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 ------------------------------------------------------------
/**
* Handles authentication, if a one of the {@link WebTokenGenerator} returns
@@ -134,6 +130,7 @@ public class AuthenticationFilter extends HttpFilter
else if (isAnonymousAccessEnabled())
{
logger.trace("anonymous access granted");
subject.login(new AnonymousToken());
processChain(request, response, chain, subject);
}
else

View File

@@ -0,0 +1,56 @@
package sonia.scm.config;
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.user.UserManager;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class ScmConfigurationChangedListenerTest {
@Mock
UserManager userManager;
ScmConfiguration scmConfiguration = new ScmConfiguration();
@InjectMocks
ScmConfigurationChangedListener listener = new ScmConfigurationChangedListener(userManager);
@Test
void shouldCreateAnonymousUserIfAnoymousAccessEnabled() {
when(userManager.contains(any())).thenReturn(false);
ScmConfiguration changes = new ScmConfiguration();
changes.setAnonymousAccessEnabled(true);
scmConfiguration.load(changes);
listener.handleEvent(new ScmConfigurationChangedEvent(scmConfiguration));
verify(userManager).create(any());
}
@Test
void shouldNotCreateAnonymousUserIfAlreadyExists() {
when(userManager.contains(any())).thenReturn(true);
ScmConfiguration changes = new ScmConfiguration();
changes.setAnonymousAccessEnabled(true);
scmConfiguration.load(changes);
listener.handleEvent(new ScmConfigurationChangedEvent(scmConfiguration));
verify(userManager, never()).create(any());
}
@Test
void shouldNotCreateAnonymousUserIfAnonymousAccessDisabled() {
//anonymous access disabled by default
listener.handleEvent(new ScmConfigurationChangedEvent(scmConfiguration));
verify(userManager, never()).create(any());
}
}

View File

@@ -37,33 +37,28 @@ package sonia.scm.web.filter;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import com.google.common.collect.ImmutableSet;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.web.WebTokenGenerator;
import static org.mockito.Mockito.*;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.verify;
//~--- JDK imports ------------------------------------------------------------
/**
*
@@ -109,26 +104,6 @@ public class AuthenticationFilterTest
"Authorization Required");
}
/**
* Method description
*
*
* @throws IOException
* @throws ServletException
*/
@Test
public void testDoFilterWithAnonymousAccess()
throws IOException, ServletException
{
configuration.setAnonymousAccessEnabled(true);
AuthenticationFilter filter = createAuthenticationFilter();
filter.doFilter(request, response, chain);
verify(chain).doFilter(any(HttpServletRequest.class),
any(HttpServletResponse.class));
}
/**
* Method description
*

View File

@@ -1,26 +1,170 @@
package sonia.scm.it;
import io.restassured.RestAssured;
import org.junit.Test;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junitpioneer.jupiter.TempDirectory;
import sonia.scm.it.utils.RepositoryUtil;
import sonia.scm.it.utils.RestUtil;
import sonia.scm.it.utils.ScmRequests;
import sonia.scm.it.utils.ScmTypes;
import sonia.scm.it.utils.TestData;
import sonia.scm.repository.client.api.RepositoryClient;
import sonia.scm.repository.client.api.RepositoryClientException;
import javax.json.Json;
import javax.json.JsonArray;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.stream.Stream;
import static java.util.Collections.emptyMap;
import static org.junit.Assert.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static sonia.scm.it.utils.TestData.JSON_BUILDER;
import static sonia.scm.it.utils.TestData.USER_ANONYMOUS;
import static sonia.scm.it.utils.TestData.WRITE;
import static sonia.scm.it.utils.TestData.getDefaultRepositoryUrl;
public class AnonymousAccessITCase {
@ExtendWith(TempDirectory.class)
class AnonymousAccessITCase {
@Test
public void shouldAccessIndexResourceWithoutAuthentication() {
void shouldAccessIndexResourceWithoutAuthentication() {
ScmRequests.start()
.requestIndexResource()
.assertStatusCode(200);
}
@Test
public void shouldRejectUserResourceWithoutAuthentication() {
void shouldRejectRepositoryResourceWithoutAuthentication() {
assertEquals(401, RestAssured.given()
.when()
.get(RestUtil.REST_BASE_URL.resolve("users/"))
.get(RestUtil.REST_BASE_URL.resolve("repositories/"))
.statusCode());
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class WithAnonymousAccess {
@BeforeAll
void enableAnonymousAccess() {
setAnonymousAccess(true);
}
@BeforeEach
void createRepository() {
TestData.createDefault();
}
@Test
void shouldGrantAnonymousAccessToRepositoryList() {
assertEquals(200, RestAssured.given()
.when()
.get(RestUtil.REST_BASE_URL.resolve("repositories"))
.statusCode());
}
@Nested
class WithoutAnonymousAccessForRepository {
@ParameterizedTest
@ArgumentsSource(ScmTypes.class)
void shouldGrantAnonymousAccessToRepository(String type) {
assertEquals(401, RestAssured.given()
.when()
.get(getDefaultRepositoryUrl(type))
.statusCode());
}
@ParameterizedTest
@ArgumentsSource(ScmTypes.class)
void shouldNotCloneRepository(String type, @TempDirectory.TempDir Path temporaryFolder) {
assertThrows(RepositoryClientException.class, () -> RepositoryUtil.createAnonymousRepositoryClient(type, Files.createDirectories(temporaryFolder).toFile()));
}
}
@Nested
class WithAnonymousAccessForRepository {
@BeforeEach
void grantAnonymousAccessToRepo() {
ScmTypes.availableScmTypes().stream().forEach(type -> TestData.createUserPermission(USER_ANONYMOUS, WRITE, type));
}
@ParameterizedTest
@ArgumentsSource(ScmTypes.class)
void shouldGrantAnonymousAccessToRepository(String type) {
assertEquals(200, RestAssured.given()
.when()
.get(getDefaultRepositoryUrl(type))
.statusCode());
}
@ParameterizedTest
@ArgumentsSource(ScmTypes.class)
void shouldCloneRepository(String type, @TempDirectory.TempDir Path temporaryFolder) throws IOException {
RepositoryClient client = RepositoryUtil.createAnonymousRepositoryClient(type, Files.createDirectories(temporaryFolder).toFile());
assertEquals(1, Objects.requireNonNull(client.getWorkingCopy().list()).length);
}
}
@AfterAll
void disableAnonymousAccess() {
setAnonymousAccess(false);
}
}
private static void setAnonymousAccess(boolean anonymousAccessEnabled) {
RestUtil.given("application/vnd.scmm-config+json;v=2")
.body(createConfig(anonymousAccessEnabled))
.when()
.put(RestUtil.REST_BASE_URL.toASCIIString() + "config")
.then()
.statusCode(HttpServletResponse.SC_NO_CONTENT);
}
private static String createConfig(boolean anonymousAccessEnabled) {
JsonArray emptyArray = Json.createBuilderFactory(emptyMap()).createArrayBuilder().build();
return JSON_BUILDER
.add("adminGroups", emptyArray)
.add("adminUsers", emptyArray)
.add("anonymousAccessEnabled", anonymousAccessEnabled)
.add("baseUrl", "https://next-scm.cloudogu.com/scm")
.add("dateFormat", "YYYY-MM-DD HH:mm:ss")
.add("disableGroupingGrid", false)
.add("enableProxy", false)
.add("enabledXsrfProtection", true)
.add("forceBaseUrl", false)
.add("loginAttemptLimit", 100)
.add("loginAttemptLimitTimeout", 300)
.add("loginInfoUrl", "https://login-info.scm-manager.org/api/v1/login-info")
.add("namespaceStrategy", "UsernameNamespaceStrategy")
.add("pluginUrl", "https://oss.cloudogu.com/jenkins/job/scm-manager/job/scm-manager-bitbucket/job/plugin-snapshot/job/master/lastSuccessfulBuild/artifact/plugins/plugin-center.json")
.add("proxyExcludes", emptyArray)
.addNull("proxyPassword")
.add("proxyPort", 8080)
.add("proxyServer", "proxy.mydomain.com")
.addNull("proxyUser")
.add("realmDescription", "SONIA :: SCM Manager")
.add("skipFailedAuthenticators", false)
.build().toString();
}
}

View File

@@ -36,6 +36,14 @@ public class RepositoryUtil {
return REPOSITORY_CLIENT_FACTORY.create(repositoryType, httpProtocolUrl, username, password, folder);
}
public static RepositoryClient createAnonymousRepositoryClient(String repositoryType, File folder) throws IOException {
String httpProtocolUrl = TestData.callRepository("scmadmin", "scmadmin", repositoryType, HttpStatus.SC_OK)
.extract()
.path("_links.protocol.find{it.name=='http'}.href");
return REPOSITORY_CLIENT_FACTORY.create(repositoryType, httpProtocolUrl, folder);
}
public static String addAndCommitRandomFile(RepositoryClient client, String username) throws IOException {
String uuid = UUID.randomUUID().toString();
String name = "file-" + uuid + ".uuid";

View File

@@ -29,9 +29,13 @@ public class RestUtil {
}
public static RequestSpecification given(String mediaType, String username, String password) {
return RestAssured.given()
.contentType(mediaType)
.accept(mediaType)
return givenAnonymous(mediaType)
.auth().preemptive().basic(username, password);
}
public static RequestSpecification givenAnonymous(String mediaType) {
return RestAssured.given()
.contentType(mediaType)
.accept(mediaType);
}
}

View File

@@ -1,11 +1,15 @@
package sonia.scm.it.utils;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import sonia.scm.util.IOUtil;
import java.util.ArrayList;
import java.util.Collection;
import java.util.stream.Stream;
public class ScmTypes {
public class ScmTypes implements ArgumentsProvider {
public static Collection<String> availableScmTypes() {
Collection<String> params = new ArrayList<>();
@@ -18,4 +22,9 @@ public class ScmTypes {
return params;
}
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception {
return availableScmTypes().stream().map(Arguments::of);
}
}

View File

@@ -18,6 +18,7 @@ import java.util.stream.Collectors;
import static java.util.Arrays.asList;
import static sonia.scm.it.utils.RestUtil.createResourceUrl;
import static sonia.scm.it.utils.RestUtil.given;
import static sonia.scm.it.utils.RestUtil.givenAnonymous;
import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
public class TestData {
@@ -25,7 +26,7 @@ public class TestData {
private static final Logger LOG = LoggerFactory.getLogger(TestData.class);
public static final String USER_SCM_ADMIN = "scmadmin";
public static final String USER_ANONYMOUS = "anonymous";
public static final String USER_ANONYMOUS = "_anonymous";
public static final Collection<String> READ = asList("read", "pull");
public static final Collection<String> WRITE = asList("read", "write", "pull", "push");
@@ -147,6 +148,16 @@ public class TestData {
.statusCode(expectedStatusCode);
}
public static ValidatableResponse callAnonymousRepository(String repositoryType, int expectedStatusCode) {
return givenAnonymous(VndMediaType.REPOSITORY)
.when()
.get(getDefaultRepositoryUrl(repositoryType))
.then()
.statusCode(expectedStatusCode);
}
public static String getDefaultPermissionUrl(String username, String password, String repositoryType) {
return given(VndMediaType.REPOSITORY, username, password)
.when()

View File

@@ -28,10 +28,10 @@ import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
public class GitLfsFilterContextListener implements ServletContextListener {
public static final String GITCONFIG = "[filter \"lfs\"]\n" +
"clean = git-lfs clean -- %f\n" +
"smudge = git-lfs smudge -- %f\n" +
"process = git-lfs filter-process\n" +
"required = true\n";
"clean = git-lfs clean -- %f\n" +
"smudge = git-lfs smudge -- %f\n" +
"process = git-lfs filter-process\n" +
"required = true\n";
public static final Pattern COMMAND_NAME_PATTERN = Pattern.compile("git-lfs (smudge|clean) -- .*");
private static final Logger LOG = LoggerFactory.getLogger(GitLfsFilterContextListener.class);

View File

@@ -3,7 +3,6 @@ package sonia.scm.repository.spi;
import org.eclipse.jgit.lib.Repository;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Path;
@@ -20,7 +19,8 @@ class LfsBlobStoreCleanFilterFactory {
this.targetFile = targetFile;
}
@SuppressWarnings("squid:S1172") // suppress unused parameter to keep the api compatible to jgit's FilterCommandFactory
@SuppressWarnings("squid:S1172")
// suppress unused parameter to keep the api compatible to jgit's FilterCommandFactory
LfsBlobStoreCleanFilter createFilter(Repository db, InputStream in, OutputStream out) {
return new LfsBlobStoreCleanFilter(in, out, blobStoreFactory.getLfsBlobStore(repository), targetFile);
}

View File

@@ -17,7 +17,6 @@ import sonia.scm.AlreadyExistsException;
import sonia.scm.BadRequestException;
import sonia.scm.ConcurrentModificationException;
import sonia.scm.NotFoundException;
import sonia.scm.ScmConstraintViolationException;
import sonia.scm.repository.Person;
import sonia.scm.repository.util.WorkdirProvider;
import sonia.scm.web.lfs.LfsBlobStoreFactory;

View File

@@ -13,7 +13,6 @@ import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import sonia.scm.ScmConstraintViolationException;
import sonia.scm.repository.Person;
import sonia.scm.repository.util.WorkdirProvider;
import sonia.scm.web.lfs.LfsBlobStoreFactory;

View File

@@ -123,6 +123,12 @@ public final class RepositoryClientFactory
password, workingCopy));
}
public RepositoryClient create(String type, String url, File workingCopy)
throws IOException
{
return new RepositoryClient(getProvider(type).create(url, null, null, workingCopy));
}
//~--- get methods ----------------------------------------------------------
/**

View File

@@ -57,7 +57,7 @@
"pluginUrlHelpText": "Die URL der Plugin Center API. Beschreibung der Platzhalter: version = SCM-Manager Version; os = Betriebssystem; arch = Architektur",
"enableForwardingHelpText": "mod_proxy Port Weiterleitung aktivieren.",
"disableGroupingGridHelpText": "Repository Gruppen deaktivieren. Nach einer Änderung an dieser Einstellung muss die Seite komplett neu geladen werden.",
"allowAnonymousAccessHelpText": "Anonyme Benutzer haben Zugriff auf öffentliche Repositories.",
"allowAnonymousAccessHelpText": "Anonyme Benutzer haben Zugriff auf freigegebene Repositories.",
"skipFailedAuthenticatorsHelpText": "Die Kette der Authentifikatoren wird nicht beendet, wenn ein Authentifikator einen Benutzer findet, ihn aber nicht erfolgreich authentifizieren kann.",
"adminGroupsHelpText": "Namen von Gruppen mit Admin-Berechtigungen.",
"adminUsersHelpText": "Namen von Benutzern mit Admin-Berechtigungen.",

View File

@@ -57,7 +57,7 @@
"pluginUrlHelpText": "The url of the Plugin Center API. Explanation of the placeholders: version = SCM-Manager Version; os = Operation System; arch = Architecture",
"enableForwardingHelpText": "Enable mod_proxy port forwarding.",
"disableGroupingGridHelpText": "Disable repository Groups. A complete page reload is required after a change of this value.",
"allowAnonymousAccessHelpText": "Anonymous users have read access on public repositories.",
"allowAnonymousAccessHelpText": "Anonymous users have access on granted repositories.",
"skipFailedAuthenticatorsHelpText": "Do not stop the authentication chain, if an authenticator finds the user but fails to authenticate the user.",
"adminGroupsHelpText": "Names of groups with admin permissions.",
"adminUsersHelpText": "Names of users with admin permissions.",

View File

@@ -57,7 +57,7 @@
"pluginUrlHelpText": "La URL de la API del almacén de complementos. Explicación de los marcadores: version = Versión de SCM-Manager; os = Sistema operativo; arch = Arquitectura",
"enableForwardingHelpText": "Habilitar el redireccionamiento de puertos para mod_proxy.",
"disableGroupingGridHelpText": "Deshabilitar los grupos de repositorios. Se requiere una recarga completa de la página después de un cambio en este valor.",
"allowAnonymousAccessHelpText": "Los usuarios anónimos tienen acceso de lectura en los repositorios públicos.",
"allowAnonymousAccessHelpText": "Los usuarios anónimos tienen acceso a repositorios otorgados.",
"skipFailedAuthenticatorsHelpText": "No detenga la cadena de autenticación si un autenticador encuentra al usuario pero no puede autenticarlo.",
"adminGroupsHelpText": "Nombres de los grupos con permisos de administrador.",
"adminUsersHelpText": "Nombres de los usuarios con permisos de administrador.",

View File

@@ -1,8 +1,8 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Checkbox, InputField } from "@scm-manager/ui-components";
import type { NamespaceStrategies } from "@scm-manager/ui-types";
import {translate} from "react-i18next";
import {Checkbox, InputField} from "@scm-manager/ui-components";
import type {NamespaceStrategies} from "@scm-manager/ui-types";
import NamespaceStrategySelect from "./NamespaceStrategySelect";
type Props = {
@@ -30,6 +30,7 @@ class GeneralSettings extends React.Component<Props> {
loginInfoUrl,
pluginUrl,
enabledXsrfProtection,
anonymousAccessEnabled,
namespaceStrategy,
hasUpdatePermission,
namespaceStrategies
@@ -88,6 +89,15 @@ class GeneralSettings extends React.Component<Props> {
helpText={t("help.pluginUrlHelpText")}
/>
</div>
<div className="column is-half">
<Checkbox
checked={anonymousAccessEnabled}
label={t("general-settings.anonymous-access-enabled")}
onChange={this.handleEnableAnonymousAccess}
disabled={!hasUpdatePermission}
helpText={t("help.allowAnonymousAccessHelpText")}
/>
</div>
</div>
</div>
);
@@ -102,6 +112,9 @@ class GeneralSettings extends React.Component<Props> {
handleEnabledXsrfProtectionChange = (value: boolean) => {
this.props.onChange(true, value, "enabledXsrfProtection");
};
handleEnableAnonymousAccess = (value: boolean) => {
this.props.onChange(true, value, "anonymousAccessEnabled");
};
handleNamespaceStrategyChange = (value: string) => {
this.props.onChange(true, value, "namespaceStrategy");
};

View File

@@ -6,18 +6,18 @@ import { translate } from "react-i18next";
import { withRouter } from "react-router-dom";
import {
fetchMe,
isAuthenticated,
getFetchMeFailure,
getMe,
isFetchMePending,
getFetchMeFailure
isAuthenticated,
isFetchMePending
} from "../modules/auth";
import {
PrimaryNavigation,
Loading,
ErrorPage,
Footer,
Header
Header,
Loading,
PrimaryNavigation
} from "@scm-manager/ui-components";
import type { Links, Me } from "@scm-manager/ui-types";
import {
@@ -50,23 +50,10 @@ class App extends Component<Props> {
}
render() {
const {
me,
loading,
error,
authenticated,
links,
t
} = this.props;
const { me, loading, error, authenticated, links, t } = this.props;
let content;
const navigation = authenticated ? (
<PrimaryNavigation
links={links}
/>
) : (
""
);
const navigation = authenticated ? <PrimaryNavigation links={links} /> : "";
if (loading) {
content = <Loading />;
@@ -85,7 +72,7 @@ class App extends Component<Props> {
<div className="App">
<Header>{navigation}</Header>
{content}
<Footer me={me} />
{authenticated && <Footer me={me} />}
</div>
);
}

View File

@@ -6,12 +6,12 @@ import { compose } from "redux";
import { translate } from "react-i18next";
import styled from "styled-components";
import {
login,
getLoginFailure,
isAuthenticated,
isLoginPending,
getLoginFailure
login
} from "../modules/auth";
import { getLoginLink, getLoginInfoLink } from "../modules/indexResource";
import { getLoginInfoLink, getLoginLink } from "../modules/indexResource";
import LoginInfo from "../components/LoginInfo";
type Props = {

View File

@@ -9,7 +9,8 @@ import {
callFetchIndexResources,
fetchIndexResources,
fetchIndexResourcesPending,
fetchIndexResourcesSuccess
fetchIndexResourcesSuccess,
getLoginLink
} from "./indexResource";
// Action
@@ -44,13 +45,11 @@ export default function reducer(
case FETCH_ME_SUCCESS:
return {
...state,
me: action.payload,
authenticated: true
me: action.payload
};
case FETCH_ME_UNAUTHORIZED:
return {
me: {},
authenticated: false
me: {}
};
case LOGOUT_SUCCESS:
return initialState;
@@ -240,7 +239,7 @@ const stateAuth = (state: Object): Object => {
};
export const isAuthenticated = (state: Object) => {
if (stateAuth(state).authenticated) {
if (state.auth.me && !getLoginLink(state)) {
return true;
}
return false;

View File

@@ -1,32 +1,35 @@
import reducer, {
fetchMeSuccess,
logout,
logoutSuccess,
loginSuccess,
fetchMeUnauthenticated,
LOGIN_SUCCESS,
login,
LOGIN_FAILURE,
LOGOUT_FAILURE,
LOGOUT_SUCCESS,
FETCH_ME_SUCCESS,
fetchMe,
FETCH_ME,
FETCH_ME_FAILURE,
FETCH_ME_UNAUTHORIZED,
isAuthenticated,
LOGIN_PENDING,
FETCH_ME_PENDING,
LOGOUT_PENDING,
FETCH_ME_SUCCESS,
FETCH_ME_UNAUTHORIZED,
fetchMe,
fetchMeSuccess,
fetchMeUnauthenticated,
getFetchMeFailure,
getLoginFailure,
getLogoutFailure,
getMe,
isAuthenticated,
isFetchMePending,
isLoginPending,
isLogoutPending,
getFetchMeFailure,
isRedirecting,
login,
LOGIN,
FETCH_ME,
LOGIN_FAILURE,
LOGIN_PENDING,
LOGIN_SUCCESS,
loginSuccess,
logout,
LOGOUT,
getLoginFailure,
getLogoutFailure, isRedirecting, LOGOUT_REDIRECT, redirectAfterLogout,
LOGOUT_FAILURE,
LOGOUT_PENDING,
LOGOUT_REDIRECT,
LOGOUT_SUCCESS,
logoutSuccess,
redirectAfterLogout
} from "./auth";
import configureMockStore from "redux-mock-store";
@@ -47,22 +50,18 @@ describe("auth reducer", () => {
it("should set me and login on successful fetch of me", () => {
const state = reducer(undefined, fetchMeSuccess(me));
expect(state.me).toBe(me);
expect(state.authenticated).toBe(true);
});
it("should set authenticated to false", () => {
const initialState = {
authenticated: true,
me
};
const state = reducer(initialState, fetchMeUnauthenticated());
expect(state.me.name).toBeUndefined();
expect(state.authenticated).toBe(false);
});
it("should reset the state after logout", () => {
const initialState = {
authenticated: true,
me
};
const state = reducer(initialState, logoutSuccess());
@@ -72,19 +71,16 @@ describe("auth reducer", () => {
it("should keep state and set redirecting to true", () => {
const initialState = {
authenticated: true,
me
};
const state = reducer(initialState, redirectAfterLogout());
expect(state.me).toBe(initialState.me);
expect(state.authenticated).toBe(initialState.authenticated);
expect(state.redirecting).toBe(true);
});
it("should set state authenticated and me after login", () => {
const state = reducer(undefined, loginSuccess(me));
expect(state.me).toBe(me);
expect(state.authenticated).toBe(true);
});
});
@@ -288,14 +284,19 @@ describe("auth actions", () => {
describe("auth selectors", () => {
const error = new Error("yo it failed");
it("should be false, if authenticated is undefined or false", () => {
expect(isAuthenticated({})).toBe(false);
expect(isAuthenticated({ auth: {} })).toBe(false);
expect(isAuthenticated({ auth: { authenticated: false } })).toBe(false);
it("should return true if me exist and login Link does not exist", () => {
expect(
isAuthenticated({ auth: { me }, indexResources: { links: {} } })
).toBe(true);
});
it("should be true, if authenticated is true", () => {
expect(isAuthenticated({ auth: { authenticated: true } })).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", () => {
@@ -359,10 +360,10 @@ describe("auth selectors", () => {
});
it("should return false, if redirecting is false", () => {
expect(isRedirecting({auth: { redirecting: false }})).toBe(false);
expect(isRedirecting({ auth: { redirecting: false } })).toBe(false);
});
it("should return true, if redirecting is true", () => {
expect(isRedirecting({auth: { redirecting: true }})).toBe(true);
expect(isRedirecting({ auth: { redirecting: true } })).toBe(true);
});
});

View File

@@ -36,12 +36,15 @@ package sonia.scm.api.rest;
import org.apache.shiro.authz.AuthorizationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.security.Authentications;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
//~--- JDK imports ------------------------------------------------------------
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
/**
*
* @author Sebastian Sdorra
@@ -49,20 +52,22 @@ import javax.ws.rs.ext.Provider;
*/
@Provider
public class AuthorizationExceptionMapper
extends StatusExceptionMapper<AuthorizationException>
implements ExceptionMapper<AuthorizationException>
{
private static final Logger LOG = LoggerFactory.getLogger(AuthorizationExceptionMapper.class);
public AuthorizationExceptionMapper()
{
super(AuthorizationException.class, Response.Status.FORBIDDEN);
}
@Override
public Response toResponse(AuthorizationException exception) {
LOG.info("user is missing permission: {}", exception.getMessage());
LOG.trace("AuthorizationException:", exception);
return super.toResponse(exception);
LOG.trace(getStatus().toString(), exception);
return Response.status(getStatus())
.entity(exception.getMessage())
.type(MediaType.TEXT_PLAIN_TYPE)
.build();
}
private Response.Status getStatus() {
return Authentications.isAuthenticatedSubjectAnonymous() ? Response.Status.UNAUTHORIZED : Response.Status.FORBIDDEN;
}
}

View File

@@ -11,7 +11,7 @@ import sonia.scm.config.ConfigurationPermissions;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.group.GroupPermissions;
import sonia.scm.plugin.PluginPermissions;
import sonia.scm.repository.RepositoryRolePermissions;
import sonia.scm.security.Authentications;
import sonia.scm.security.PermissionPermissions;
import sonia.scm.user.UserPermissions;
@@ -46,10 +46,14 @@ public class IndexDtoGenerator extends HalAppenderMapper {
}
if (SecurityUtils.getSubject().isAuthenticated()) {
builder.single(
link("me", resourceLinks.me().self()),
link("logout", resourceLinks.authentication().logout())
);
builder.single(link("me", resourceLinks.me().self()));
if (Authentications.isAuthenticatedSubjectAnonymous()) {
builder.single(link("login", resourceLinks.authentication().jsonLogin()));
} else {
builder.single(link("logout", resourceLinks.authentication().logout()));
}
if (PluginPermissions.read().isPermitted()) {
builder.single(link("installedPlugins", resourceLinks.installedPluginCollection().self()));
builder.single(link("availablePlugins", resourceLinks.availablePluginCollection().self()));

View File

@@ -6,6 +6,7 @@ import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import sonia.scm.group.GroupCollector;
import sonia.scm.security.Authentications;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.user.UserPermissions;
@@ -63,7 +64,7 @@ public class MeDtoFactory extends HalAppenderMapper {
if (UserPermissions.modify(user).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.me().update(user.getName())));
}
if (userManager.isTypeDefault(user) && UserPermissions.changePassword(user).isPermitted()) {
if (userManager.isTypeDefault(user) && UserPermissions.changePassword(user).isPermitted() && !Authentications.isSubjectAnonymous(user.getName())) {
linksBuilder.single(link("password", resourceLinks.me().passwordChange()));
}

View File

@@ -6,6 +6,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager;
import sonia.scm.security.Authentications;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -38,7 +39,10 @@ public class DefaultGroupCollector implements GroupCollector {
public Set<String> collect(String principal) {
ImmutableSet.Builder<String> builder = ImmutableSet.builder();
builder.add(AUTHENTICATED);
if (!Authentications.isSubjectAnonymous(principal)) {
builder.add(AUTHENTICATED);
}
builder.addAll(resolveExternalGroups(principal));
appendInternalGroups(principal, builder);

View File

@@ -4,6 +4,8 @@ import com.google.common.annotations.VisibleForTesting;
import org.apache.shiro.authc.credential.PasswordService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.plugin.Extension;
import sonia.scm.security.PermissionAssigner;
import sonia.scm.security.PermissionDescriptor;
@@ -47,12 +49,14 @@ public class SetupContextListener implements ServletContextListener {
private final UserManager userManager;
private final PasswordService passwordService;
private final PermissionAssigner permissionAssigner;
private final ScmConfiguration scmConfiguration;
@Inject
public SetupAction(UserManager userManager, PasswordService passwordService, PermissionAssigner permissionAssigner) {
public SetupAction(UserManager userManager, PasswordService passwordService, PermissionAssigner permissionAssigner, ScmConfiguration scmConfiguration) {
this.userManager = userManager;
this.passwordService = passwordService;
this.permissionAssigner = permissionAssigner;
this.scmConfiguration = scmConfiguration;
}
@Override
@@ -60,6 +64,13 @@ public class SetupContextListener implements ServletContextListener {
if (isFirstStart()) {
createAdminAccount();
}
if (anonymousUserRequiredButNotExists()) {
userManager.create(SCMContext.ANONYMOUS);
}
}
private boolean anonymousUserRequiredButNotExists() {
return scmConfiguration.isAnonymousAccessEnabled() && !userManager.contains(SCMContext.USER_ANONYMOUS);
}
private boolean isFirstStart() {

View File

@@ -0,0 +1,44 @@
package sonia.scm.security;
import com.google.common.annotations.VisibleForTesting;
import com.google.inject.Inject;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
import org.apache.shiro.realm.AuthenticatingRealm;
import sonia.scm.SCMContext;
import sonia.scm.plugin.Extension;
import javax.inject.Singleton;
import static com.google.common.base.Preconditions.checkArgument;
@Singleton
@Extension
public class AnonymousRealm extends AuthenticatingRealm {
/**
* realm name
*/
@VisibleForTesting
static final String REALM = "AnonymousRealm";
/**
* dao realm helper
*/
private final DAORealmHelper helper;
@Inject
public AnonymousRealm(DAORealmHelperFactory helperFactory) {
this.helper = helperFactory.create(REALM);
setAuthenticationTokenClass(AnonymousToken.class);
setCredentialsMatcher(new AllowAllCredentialsMatcher());
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
checkArgument(authenticationToken instanceof AnonymousToken, "%s is required", AnonymousToken.class);
return helper.authenticationInfoBuilder(SCMContext.USER_ANONYMOUS).build();
}
}

View File

@@ -254,9 +254,11 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
collectGlobalPermissions(builder, user, groups);
collectRepositoryPermissions(builder, user, groups);
builder.add(canReadOwnUser(user));
builder.add(getUserAutocompletePermission());
builder.add(getGroupAutocompletePermission());
builder.add(getChangeOwnPasswordPermission(user));
if (!Authentications.isSubjectAnonymous(user.getName())) {
builder.add(getUserAutocompletePermission());
builder.add(getGroupAutocompletePermission());
builder.add(getChangeOwnPasswordPermission(user));
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(ImmutableSet.of(Role.USER));
info.addStringPermissions(builder.build());

View File

@@ -48,15 +48,11 @@ import sonia.scm.SCMContextProvider;
import sonia.scm.TransformFilter;
import sonia.scm.search.SearchRequest;
import sonia.scm.search.SearchUtil;
import sonia.scm.security.Authentications;
import sonia.scm.util.CollectionAppender;
import sonia.scm.util.IOUtil;
import sonia.scm.util.Util;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -383,7 +379,7 @@ public class DefaultUserManager extends AbstractUserManager
public void changePasswordForLoggedInUser(String oldPassword, String newPassword) {
User user = get((String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal());
if (!user.getPassword().equals(oldPassword)) {
if (!isAnonymousUser(user) && !user.getPassword().equals(oldPassword)) {
throw new InvalidPasswordException(ContextEntry.ContextBuilder.entity("PasswordChange", "-").in(User.class, user.getName()));
}
@@ -402,13 +398,17 @@ public class DefaultUserManager extends AbstractUserManager
if (user == null) {
throw new NotFoundException(User.class, userId);
}
if (!isTypeDefault(user)) {
if (!isTypeDefault(user) || isAnonymousUser(user)) {
throw new ChangePasswordNotAllowedException(ContextEntry.ContextBuilder.entity("PasswordChange", "-").in(User.class, user.getName()), user.getType());
}
user.setPassword(newPassword);
this.modify(user);
}
private boolean isAnonymousUser(User user) {
return Authentications.isSubjectAnonymous(user.getName());
}
//~--- fields ---------------------------------------------------------------
private final UserDAO userDAO;

View File

@@ -13,6 +13,8 @@ import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.repository.spi.HttpScmProtocol;
import sonia.scm.security.Authentications;
import sonia.scm.util.HttpUtil;
import sonia.scm.web.UserAgent;
import sonia.scm.web.UserAgentParser;
@@ -73,7 +75,11 @@ public class HttpProtocolServlet extends HttpServlet {
resp.setStatus(HttpStatus.SC_NOT_FOUND);
} catch (AuthorizationException e) {
log.debug(e.getMessage());
resp.setStatus(HttpStatus.SC_FORBIDDEN);
if (Authentications.isAuthenticatedSubjectAnonymous()) {
HttpUtil.sendUnauthorized(resp);
} else {
resp.setStatus(HttpStatus.SC_FORBIDDEN);
}
}
}
}

View File

@@ -12,14 +12,17 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import sonia.scm.SCMContext;
import sonia.scm.group.GroupCollector;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.user.UserPermissions;
import sonia.scm.user.UserTestData;
import java.net.URI;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -159,6 +162,18 @@ class MeDtoFactoryTest {
assertThat(dto.getLinks().getLinkBy("password")).isNotPresent();
}
@Test
void shouldNotGetPasswordLinkForAnonymousUser() {
User user = SCMContext.ANONYMOUS;
prepareSubject(user);
when(userManager.isTypeDefault(any())).thenReturn(true);
when(UserPermissions.changePassword(user).isPermitted()).thenReturn(true);
MeDto dto = meDtoFactory.create();
assertThat(dto.getLinks().getLinkBy("password")).isNotPresent();
}
@Test
void shouldAppendLinks() {
prepareSubject(UserTestData.createTrillian());

View File

@@ -170,6 +170,7 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase {
@TestFactory
@DisplayName("test endpoints on missing permissions and user is not Admin")
Stream<DynamicTest> missedPermissionUserForbiddenTestFactory() {
when(subject.getPrincipal()).thenReturn("user");
doThrow(AuthorizationException.class).when(repositoryManager).get(any(NamespaceAndName.class));
return createDynamicTestsToAssertResponses(
requestGETPermission.expectedResponseStatus(403),
@@ -179,6 +180,19 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase {
requestPUTPermission.expectedResponseStatus(403));
}
@TestFactory
@DisplayName("test endpoints on missing permissions and is _anonymous")
Stream<DynamicTest> missedPermissionAnonymousUnauthorizedTestFactory() {
when(subject.getPrincipal()).thenReturn("_anonymous");
doThrow(AuthorizationException.class).when(repositoryManager).get(any(NamespaceAndName.class));
return createDynamicTestsToAssertResponses(
requestGETPermission.expectedResponseStatus(401),
requestPOSTPermission.expectedResponseStatus(401),
requestGETAllPermissions.expectedResponseStatus(401),
requestDELETEPermission.expectedResponseStatus(401),
requestPUTPermission.expectedResponseStatus(401));
}
@Test
public void userWithPermissionWritePermissionShouldGetAllPermissionsWithCreateAndUpdateLinks() throws URISyntaxException {
createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE);

View File

@@ -40,6 +40,7 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.slf4j.MDC;
import sonia.scm.AbstractTestBase;
import sonia.scm.SCMContext;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
@@ -117,7 +118,7 @@ public class MDCFilterTest extends AbstractTestBase {
filter.doFilter(request, response, chain);
assertNotNull(chain.ctx);
assertEquals("anonymous", chain.ctx.get(MDCFilter.MDC_USERNAME));
assertEquals(SCMContext.USER_ANONYMOUS, chain.ctx.get(MDCFilter.MDC_USERNAME));
}
private static class MDCCapturingFilterChain implements FilterChain {

View File

@@ -11,6 +11,8 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.PermissionAssigner;
import sonia.scm.security.PermissionDescriptor;
import sonia.scm.user.User;
@@ -23,7 +25,12 @@ import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class SetupContextListenerTest {
@@ -40,12 +47,20 @@ class SetupContextListenerTest {
@Mock
private PasswordService passwordService;
@Mock
ScmConfiguration scmConfiguration;
@Mock
private PermissionAssigner permissionAssigner;
@InjectMocks
private SetupContextListener.SetupAction setupAction;
@BeforeEach
void mockScmConfiguration() {
when(scmConfiguration.isAnonymousAccessEnabled()).thenReturn(false);
}
@BeforeEach
void setupObjectUnderTest() {
doAnswer(ic -> {
@@ -90,6 +105,38 @@ class SetupContextListenerTest {
verify(permissionAssigner, never()).setPermissionsForUser(anyString(), any(Collection.class));
}
@Test
void shouldCreateAnonymousUserIfRequired() {
List<User> users = Lists.newArrayList(UserTestData.createTrillian());
when(userManager.getAll()).thenReturn(users);
when(scmConfiguration.isAnonymousAccessEnabled()).thenReturn(true);
setupContextListener.contextInitialized(null);
verify(userManager).create(SCMContext.ANONYMOUS);
}
@Test
void shouldNotCreateAnonymousUserIfNotRequired() {
List<User> users = Lists.newArrayList(UserTestData.createTrillian());
when(userManager.getAll()).thenReturn(users);
setupContextListener.contextInitialized(null);
verify(userManager, never()).create(SCMContext.ANONYMOUS);
}
@Test
void shouldNotCreateAnonymousUserIfAlreadyExists() {
List<User> users = Lists.newArrayList(SCMContext.ANONYMOUS);
when(userManager.getAll()).thenReturn(users);
when(scmConfiguration.isAnonymousAccessEnabled()).thenReturn(true);
setupContextListener.contextInitialized(null);
verify(userManager, times(1)).create(SCMContext.ANONYMOUS);
}
private void verifyAdminPermissionsAssigned() {
ArgumentCaptor<String> usernameCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<Collection<PermissionDescriptor>> permissionCaptor = ArgumentCaptor.forClass(Collection.class);

View File

@@ -0,0 +1,54 @@
package sonia.scm.security;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
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.SCMContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class AnonymousRealmTest {
@Mock
private DAORealmHelperFactory realmHelperFactory;
@Mock
private DAORealmHelper realmHelper;
@Mock
private DAORealmHelper.AuthenticationInfoBuilder builder;
@InjectMocks
private AnonymousRealm realm;
@Mock
private AuthenticationInfo authenticationInfo;
@BeforeEach
void prepareObjectUnderTest() {
when(realmHelperFactory.create(AnonymousRealm.REALM)).thenReturn(realmHelper);
realm = new AnonymousRealm(realmHelperFactory);
}
@Test
void shouldDoGetAuthentication() {
when(realmHelper.authenticationInfoBuilder(SCMContext.USER_ANONYMOUS)).thenReturn(builder);
when(builder.build()).thenReturn(authenticationInfo);
AuthenticationInfo result = realm.doGetAuthenticationInfo(new AnonymousToken());
assertThat(result).isSameAs(authenticationInfo);
}
@Test
void shouldThrowIllegalArgumentExceptionForWrongTypeOfToken() {
assertThrows(IllegalArgumentException.class, () -> realm.doGetAuthenticationInfo(new UsernamePasswordToken()));
}
}

View File

@@ -48,6 +48,7 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.SCMContext;
import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager;
import sonia.scm.group.GroupCollector;
@@ -172,6 +173,23 @@ public class DefaultAuthorizationCollectorTest {
assertThat(authInfo.getObjectPermissions(), nullValue());
}
/**
* Tests {@link AuthorizationCollector#collect(PrincipalCollection)} ()} without permissions.
*/
@Test
@SubjectAware(
configuration = "classpath:sonia/scm/shiro-001.ini"
)
public void testCollectWithoutPermissionsForAnonymousUser() {
authenticate(SCMContext.ANONYMOUS, "anon");
AuthorizationInfo authInfo = collector.collect();
assertThat(authInfo.getRoles(), Matchers.contains(Role.USER));
assertThat(authInfo.getStringPermissions(), hasSize(1));
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:read:_anonymous"));
assertThat(authInfo.getObjectPermissions(), nullValue());
}
/**
* Tests {@link AuthorizationCollector#collect(PrincipalCollection)} ()} with repository permissions.
*/

View File

@@ -1,6 +1,7 @@
[users]
trillian = secret, user
dent = secret, admin
_anonymous = secret, user
[roles]
admin = *