Integrate Plugin Center myCloudogu Authentication (#1884)

Allows scm-manager instances to authenticate with the configured plugin center. If the default plugin center is used, a myCloudogu account is used for authentication which in turn enables downloading special myCloudogu plugins directly through the plugin administration page.

Co-authored-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
Co-authored-by: Matthias Thieroff <93515444+mthieroff@users.noreply.github.com>
Co-authored-by: Philipp Ahrendt <philipp.ahrendt@cloudogu.com>
This commit is contained in:
Sebastian Sdorra
2021-12-13 15:15:57 +01:00
committed by GitHub
parent c95888d491
commit 6eba01161f
84 changed files with 3147 additions and 289 deletions

View File

@@ -54,6 +54,7 @@ public class ConfigDto extends HalRepresentation implements UpdateConfigDto {
private Set<String> proxyExcludes;
private boolean skipFailedAuthenticators;
private String pluginUrl;
private String pluginAuthUrl;
private long loginAttemptLimitTimeout;
private boolean enabledXsrfProtection;
private boolean enabledUserConverter;

View File

@@ -105,6 +105,7 @@ public class IndexDtoGenerator extends HalAppenderMapper {
}
if (PluginPermissions.read().isPermitted()) {
builder.single(link("pluginCenterAuth", resourceLinks.pluginCenterAuth().auth()));
builder.single(link("installedPlugins", resourceLinks.installedPluginCollection().self()));
builder.single(link("availablePlugins", resourceLinks.availablePluginCollection().self()));
}

View File

@@ -0,0 +1,429 @@
/*
* 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 com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import de.otto.edison.hal.Link;
import de.otto.edison.hal.Links;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import sonia.scm.ExceptionWithContext;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.plugin.AuthenticationInfo;
import sonia.scm.plugin.PluginCenterAuthenticator;
import sonia.scm.plugin.PluginPermissions;
import sonia.scm.security.AllowAnonymousAccess;
import sonia.scm.security.Impersonator;
import sonia.scm.security.SecureParameterSerializer;
import sonia.scm.security.XsrfExcludes;
import sonia.scm.user.DisplayUser;
import sonia.scm.user.User;
import sonia.scm.user.UserDisplayManager;
import sonia.scm.util.HttpUtil;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.net.URI;
import java.util.Optional;
import java.util.UUID;
@Singleton
public class PluginCenterAuthResource {
@VisibleForTesting
static final String ERROR_SOURCE_MISSING = "5DSqG6Mcg1";
@VisibleForTesting
static final String ERROR_AUTHENTICATION_DISABLED = "8tSqFDot11";
@VisibleForTesting
static final String ERROR_ALREADY_AUTHENTICATED = "8XSqFEBd41";
@VisibleForTesting
static final String ERROR_PARAMS_MISSING = "52SqQBdpO1";
@VisibleForTesting
static final String ERROR_CHALLENGE_MISSING = "FNSqFKQIR1";
@VisibleForTesting
static final String ERROR_CHALLENGE_DOES_NOT_MATCH = "8ESqFElpI1";
private final ScmPathInfoStore pathInfoStore;
private final PluginCenterAuthenticator authenticator;
private final ScmConfiguration configuration;
private final UserDisplayManager userDisplayManager;
private final XsrfExcludes excludes;
private final ChallengeGenerator challengeGenerator;
private final SecureParameterSerializer parameterSerializer;
private final Impersonator impersonator;
private String challenge;
@Inject
public PluginCenterAuthResource(
ScmPathInfoStore pathInfoStore,
PluginCenterAuthenticator authenticator,
UserDisplayManager userDisplayManager,
ScmConfiguration scmConfiguration,
XsrfExcludes excludes,
SecureParameterSerializer parameterSerializer,
Impersonator impersonator) {
this(
pathInfoStore, authenticator, userDisplayManager, scmConfiguration, excludes, () -> UUID.randomUUID().toString(),
parameterSerializer, impersonator);
}
@VisibleForTesting
PluginCenterAuthResource(
ScmPathInfoStore pathInfoStore,
PluginCenterAuthenticator authenticator,
UserDisplayManager userDisplayManager,
ScmConfiguration configuration,
XsrfExcludes excludes,
ChallengeGenerator challengeGenerator,
SecureParameterSerializer parameterSerializer,
Impersonator impersonator) {
this.pathInfoStore = pathInfoStore;
this.authenticator = authenticator;
this.configuration = configuration;
this.userDisplayManager = userDisplayManager;
this.excludes = excludes;
this.challengeGenerator = challengeGenerator;
this.parameterSerializer = parameterSerializer;
this.impersonator = impersonator;
}
@GET
@Path("")
@Operation(
summary = "Return plugin center auth info",
description = "Return authentication information of plugin center connection",
tags = "Plugin Management",
operationId = "plugin_center_auth_information"
)
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = VndMediaType.PLUGIN_COLLECTION,
schema = @Schema(implementation = PluginCenterAuthenticationInfoDto.class)
)
)
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:read\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
@Produces(VndMediaType.PLUGIN_CENTER_AUTH_INFO)
public Response authenticationInfo(@Context UriInfo uriInfo) {
Optional<AuthenticationInfo> authentication = authenticator.getAuthenticationInfo();
if (authentication.isPresent()) {
return Response.ok(createAuthenticatedDto(uriInfo, authentication.get())).build();
}
PluginCenterAuthenticationInfoDto dto = new PluginCenterAuthenticationInfoDto(createLinks(uriInfo, false));
dto.setDefault(configuration.isDefaultPluginAuthUrl());
return Response.ok(dto).build();
}
private PluginCenterAuthenticationInfoDto createAuthenticatedDto(@Context UriInfo uriInfo, AuthenticationInfo info) {
PluginCenterAuthenticationInfoDto dto = new PluginCenterAuthenticationInfoDto(
createLinks(uriInfo, true)
);
dto.setPrincipal(getPrincipalDisplayName(info.getPrincipal()));
dto.setPluginCenterSubject(info.getPluginCenterSubject());
dto.setDate(info.getDate());
dto.setDefault(configuration.isDefaultPluginAuthUrl());
return dto;
}
@GET
@Path("login")
@Operation(
summary = "Login",
description = "Start the authentication flow to connect the plugin center with an account",
tags = "Plugin Management",
operationId = "plugin_center_auth_login"
)
@ApiResponse(
responseCode = "303",
description = "See other"
)
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:write\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public Response login(@Context UriInfo uriInfo, @QueryParam("source") String source) throws IOException {
String pluginAuthUrl = configuration.getPluginAuthUrl();
if (Strings.isNullOrEmpty(source)) {
return error(ERROR_SOURCE_MISSING);
}
if (Strings.isNullOrEmpty(pluginAuthUrl)) {
return error(ERROR_AUTHENTICATION_DISABLED);
}
if (authenticator.isAuthenticated()) {
return error(ERROR_ALREADY_AUTHENTICATED);
}
challenge = challengeGenerator.create();
URI selfUri = uriInfo.getAbsolutePath();
selfUri = selfUri.resolve(selfUri.getPath().replace("/login", "/callback"));
String principal = SecurityUtils.getSubject().getPrincipal().toString();
AuthParameter parameter = new AuthParameter(
principal,
challenge,
source
);
URI callbackUri = UriBuilder.fromUri(selfUri)
.queryParam("params", parameterSerializer.serialize(parameter))
.build();
excludes.add(callbackUri.getPath());
URI authUri = UriBuilder.fromUri(pluginAuthUrl).queryParam("instance", callbackUri.toASCIIString()).build();
return Response.seeOther(authUri).build();
}
private Links createLinks(UriInfo uriInfo, boolean authenticated) {
String self = uriInfo.getAbsolutePath().toASCIIString();
Links.Builder builder = Links.linkingTo().self(self);
if (PluginPermissions.write().isPermitted()) {
if (authenticated) {
builder.single(Link.link("logout", self));
} else {
URI login = uriInfo.getAbsolutePathBuilder().path("login").build();
builder.single(Link.link("login", login.toASCIIString()));
}
}
return builder.build();
}
private String getPrincipalDisplayName(String principal) {
return userDisplayManager.get(principal).map(DisplayUser::getDisplayName).orElse(principal);
}
@DELETE
@Path("")
@Operation(
summary = "Logout",
description = "Start the authentication flow to connect the plugin center with an account",
tags = "Plugin Management",
operationId = "plugin_center_auth_logout"
)
@ApiResponse(
responseCode = "204",
description = "No content"
)
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:write\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public Response logout() {
authenticator.logout();
return Response.noContent().build();
}
@POST
@Path("callback")
@Operation(
summary = "Finalize authentication",
description = "Callback endpoint for the authentication flow to finalize the authentication",
tags = "Plugin Management",
operationId = "plugin_center_auth_callback"
)
@ApiResponse(
responseCode = "303",
description = "See other"
)
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:write\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
@AllowAnonymousAccess
public Response callback(
@Context UriInfo uriInfo,
@QueryParam("params") String encryptedParams,
@FormParam("subject") String subject,
@FormParam("refresh_token") String refreshToken
) throws IOException {
if (Strings.isNullOrEmpty(encryptedParams)) {
return error(ERROR_PARAMS_MISSING);
}
AuthParameter params = parameterSerializer.deserialize(encryptedParams, AuthParameter.class);
Optional<String> error = checkChallenge(params.getChallenge());
if (error.isPresent()) {
return error(error.get());
}
challenge = null;
excludes.remove(uriInfo.getPath());
PrincipalCollection principal = createPrincipalCollection(params);
try (Impersonator.Session session = impersonator.impersonate(principal)) {
authenticator.authenticate(subject, refreshToken);
} catch (ExceptionWithContext ex) {
return error(ex.getCode());
}
return redirect(params.getSource());
}
private PrincipalCollection createPrincipalCollection(AuthParameter params) {
SimplePrincipalCollection principal = new SimplePrincipalCollection(
params.getPrincipal(), "pluginCenterAuth"
);
User user = new User(params.getPrincipal());
principal.add(user, "pluginCenterAuth");
return principal;
}
@GET
@Path("callback")
@Operation(
summary = "Abort authentication",
description = "Callback endpoint for the authentication flow to abort the authentication",
tags = "Plugin Management",
operationId = "plugin_center_auth_callback_abort"
)
@ApiResponse(
responseCode = "303",
description = "See other"
)
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:write\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public Response callbackAbort(@Context UriInfo uriInfo, @QueryParam("params") String encryptedParams) throws IOException {
if (Strings.isNullOrEmpty(encryptedParams)) {
return error(ERROR_PARAMS_MISSING);
}
AuthParameter params = parameterSerializer.deserialize(encryptedParams, AuthParameter.class);
Optional<String> error = checkChallenge(params.getChallenge());
if (error.isPresent()) {
return error(error.get());
}
challenge = null;
excludes.remove(uriInfo.getPath());
return redirect(params.getSource());
}
private Response error(String code) {
return redirect("error/" + code);
}
private Response redirect(String location) {
URI rootUri = pathInfoStore.get().getRootUri();
String path = rootUri.getPath();
if (!Strings.isNullOrEmpty(location)) {
path = HttpUtil.concatenate(path, location);
}
return redirect(rootUri.resolve(path));
}
private Response redirect(URI location) {
return Response.status(Response.Status.SEE_OTHER).location(location).build();
}
private Optional<String> checkChallenge(String challengeFromRequest) {
if (Strings.isNullOrEmpty(challenge)) {
return Optional.of(ERROR_CHALLENGE_MISSING);
}
if (!challenge.equals(challengeFromRequest)) {
return Optional.of(ERROR_CHALLENGE_DOES_NOT_MATCH);
}
return Optional.empty();
}
@FunctionalInterface
interface ChallengeGenerator {
String create();
}
@Data
@NoArgsConstructor
@AllArgsConstructor
static class AuthParameter {
private String principal;
private String challenge;
private String source;
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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 com.fasterxml.jackson.annotation.JsonInclude;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.Setter;
import java.time.Instant;
import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
@Getter
@Setter
@SuppressWarnings("java:S2160") // we need no equals here
public class PluginCenterAuthenticationInfoDto extends HalRepresentation {
@JsonInclude(NON_NULL)
private String principal;
@JsonInclude(NON_NULL)
private String pluginCenterSubject;
@JsonInclude(NON_NULL)
private Instant date;
private boolean isDefault;
public PluginCenterAuthenticationInfoDto(Links links) {
super(links);
}
}

View File

@@ -24,6 +24,7 @@
package sonia.scm.api.v2.resources;
import com.google.common.base.Strings;
import de.otto.edison.hal.Links;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@@ -97,7 +98,9 @@ public abstract class PluginDtoMapper {
if (isCloudoguPlugin) {
Optional<String> cloudoguInstallLink = plugin.getDescriptor().getInstallLink();
cloudoguInstallLink.ifPresent(link -> links.single(link("cloudoguInstall", link)));
} else {
}
if (!Strings.isNullOrEmpty(plugin.getDescriptor().getUrl())) {
String href = resourceLinks.availablePlugin().install(information.getName());
appendLink(links, "install", href);
}

View File

@@ -40,12 +40,19 @@ public class PluginRootResource {
private final Provider<InstalledPluginResource> installedPluginResourceProvider;
private final Provider<AvailablePluginResource> availablePluginResourceProvider;
private final Provider<PendingPluginResource> pendingPluginResourceProvider;
private final Provider<PluginCenterAuthResource> pluginCenterAuthResourceProvider;
@Inject
public PluginRootResource(Provider<InstalledPluginResource> installedPluginResourceProvider, Provider<AvailablePluginResource> availablePluginResourceProvider, Provider<PendingPluginResource> pendingPluginResourceProvider) {
public PluginRootResource(
Provider<InstalledPluginResource> installedPluginResourceProvider,
Provider<AvailablePluginResource> availablePluginResourceProvider,
Provider<PendingPluginResource> pendingPluginResourceProvider,
Provider<PluginCenterAuthResource> pluginCenterAuthResourceProvider
) {
this.installedPluginResourceProvider = installedPluginResourceProvider;
this.availablePluginResourceProvider = availablePluginResourceProvider;
this.pendingPluginResourceProvider = pendingPluginResourceProvider;
this.pluginCenterAuthResourceProvider = pluginCenterAuthResourceProvider;
}
@Path("/installed")
@@ -58,4 +65,9 @@ public class PluginRootResource {
@Path("/pending")
public PendingPluginResource pendingPlugins() { return pendingPluginResourceProvider.get(); }
@Path("/auth")
public PluginCenterAuthResource authResource() {
return pluginCenterAuthResourceProvider.get();
}
}

View File

@@ -1202,4 +1202,20 @@ class ResourceLinks {
.href();
}
}
public PluginCenterAuthLinks pluginCenterAuth() {
return new PluginCenterAuthLinks(scmPathInfoStore.get().get());
}
static class PluginCenterAuthLinks {
private final LinkBuilder indexLinkBuilder;
PluginCenterAuthLinks(ScmPathInfo pathInfo) {
indexLinkBuilder = new LinkBuilder(pathInfo, PluginRootResource.class, PluginCenterAuthResource.class);
}
String auth() {
return indexLinkBuilder.method("authResource").parameters().method("authenticationInfo").parameters().href();
}
}
}

View File

@@ -0,0 +1,52 @@
/*
* 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.plugin;
import java.time.Instant;
/**
* Information about the plugin center authentication.
* @since 2.28.0
*/
public interface AuthenticationInfo {
/**
* Returns the username of the SCM-Manager user which has authenticated the plugin center.
* @return SCM-Manager username
*/
String getPrincipal();
/**
* Returns the subject of the plugin center user.
* @return plugin center subject
*/
String getPluginCenterSubject();
/**
* Returns the date on which the authentication was performed.
* @return authentication date
*/
Instant getDate();
}

View File

@@ -0,0 +1,50 @@
/*
* 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.plugin;
import sonia.scm.ExceptionWithContext;
import java.util.Collections;
/**
* Exception is thrown if the exchange of a refresh token to an access token fails.
*
* @since 2.28.0
*/
public class FetchAccessTokenFailedException extends ExceptionWithContext {
public FetchAccessTokenFailedException(String message) {
super(Collections.emptyList(), message);
}
public FetchAccessTokenFailedException(String message, Exception cause) {
super(Collections.emptyList(), message, cause);
}
@Override
public String getCode() {
return "AHSqALeEv1";
}
}

View File

@@ -24,6 +24,8 @@
package sonia.scm.plugin;
import com.github.legman.Subscribe;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContextProvider;
@@ -34,8 +36,10 @@ import sonia.scm.util.HttpUtil;
import sonia.scm.util.SystemUtil;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Set;
@Singleton
public class PluginCenter {
private static final String CACHE_NAME = "sonia.cache.plugins";
@@ -55,19 +59,37 @@ public class PluginCenter {
this.cache = cacheManager.getCache(CACHE_NAME);
}
@Subscribe
public void handle(PluginCenterAuthenticationEvent event) {
LOG.debug("clear plugin center cache, because of {}", event);
cache.clear();
}
synchronized Set<AvailablePlugin> getAvailable() {
String url = buildPluginUrl(configuration.getPluginUrl());
Set<AvailablePlugin> plugins = cache.get(url);
if (plugins == null) {
LOG.debug("no cached available plugins found, start fetching");
plugins = loader.load(url);
cache.put(url, plugins);
plugins = fetchAvailablePlugins(url);
} else {
LOG.debug("return available plugins from cache");
}
return plugins;
}
@CanIgnoreReturnValue
private Set<AvailablePlugin> fetchAvailablePlugins(String url) {
Set<AvailablePlugin> plugins = loader.load(url);
cache.put(url, plugins);
return plugins;
}
synchronized void refresh() {
LOG.debug("refresh plugin center cache");
String url = buildPluginUrl(configuration.getPluginUrl());
fetchAvailablePlugins(url);
}
private String buildPluginUrl(String url) {
String os = HttpUtil.encode(SystemUtil.getOS());
String arch = SystemUtil.getArch();

View File

@@ -0,0 +1,33 @@
/*
* 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.plugin;
/**
* Marker interface for plugin center authentication events such as login or logout.
* @since 2.28.0
*/
public interface PluginCenterAuthenticationEvent {
}

View File

@@ -0,0 +1,179 @@
/*
* 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.plugin;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.Value;
import org.apache.shiro.SecurityUtils;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.event.ScmEventBus;
import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.net.ahc.AdvancedHttpResponse;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.util.HttpUtil;
import sonia.scm.xml.XmlInstantAdapter;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.io.IOException;
import java.time.Instant;
import java.util.Optional;
import static sonia.scm.plugin.Tracing.SPAN_KIND;
@Singleton
public class PluginCenterAuthenticator {
@VisibleForTesting
static final String STORE_NAME = "plugin-center-auth";
private final ConfigurationStore<Authentication> configurationStore;
private final ScmConfiguration scmConfiguration;
private final AdvancedHttpClient advancedHttpClient;
private final ScmEventBus eventBus;
@Inject
public PluginCenterAuthenticator(
ConfigurationStoreFactory configurationStore, ScmConfiguration scmConfiguration,
AdvancedHttpClient advancedHttpClient, ScmEventBus eventBus
) {
this.configurationStore = configurationStore.withType(Authentication.class).withName(STORE_NAME).build();
this.scmConfiguration = scmConfiguration;
this.advancedHttpClient = advancedHttpClient;
this.eventBus = eventBus;
}
public void authenticate(String pluginCenterSubject, String refreshToken) {
Preconditions.checkArgument(!Strings.isNullOrEmpty(pluginCenterSubject), "pluginCenterSubject is required");
Preconditions.checkArgument(!Strings.isNullOrEmpty(refreshToken), "refresh token is required");
// only a user which is able to manage plugins, can authenticate the plugin center
PluginPermissions.write().check();
// check if refresh token is valid
Authentication authentication = new Authentication(principal(), pluginCenterSubject, refreshToken, Instant.now());
fetchAccessToken(authentication);
eventBus.post(new PluginCenterLoginEvent(authentication));
}
public void logout() {
PluginPermissions.write().check();
getAuthenticationInfo().ifPresent(authenticationInfo -> {
eventBus.post(new PluginCenterLogoutEvent(authenticationInfo));
configurationStore.delete();
});
}
public boolean isAuthenticated() {
return getAuthentication().isPresent();
}
public Optional<AuthenticationInfo> getAuthenticationInfo() {
PluginPermissions.read().check();
return getAuthentication().map(a -> a);
}
public String fetchAccessToken() {
PluginPermissions.read().check();
Authentication authentication = getAuthentication()
.orElseThrow(() -> new IllegalStateException("An access token can only be obtained, after a prior authentication"));
return fetchAccessToken(authentication);
}
@CanIgnoreReturnValue
private String fetchAccessToken(Authentication authentication) {
String pluginAuthUrl = scmConfiguration.getPluginAuthUrl();
Preconditions.checkState(!Strings.isNullOrEmpty(pluginAuthUrl), "plugin auth url is not configured");
try {
AdvancedHttpResponse response = advancedHttpClient.post(HttpUtil.concatenate(pluginAuthUrl, "refresh"))
.spanKind(SPAN_KIND)
.jsonContent(new RefreshRequest(authentication.getRefreshToken()))
.request();
if (!response.isSuccessful()) {
throw new FetchAccessTokenFailedException("failed to obtain access token, server returned status code " + response.getStatus());
}
RefreshResponse refresh = response.contentFromJson(RefreshResponse.class);
authentication.setRefreshToken(refresh.getRefreshToken());
configurationStore.set(authentication);
return refresh.getAccessToken();
} catch (IOException ex) {
throw new FetchAccessTokenFailedException("failed to obtain an access token", ex);
}
}
private String principal() {
return SecurityUtils.getSubject().getPrincipal().toString();
}
private Optional<Authentication> getAuthentication() {
return configurationStore.getOptional();
}
@Data
@XmlRootElement
@VisibleForTesting
@AllArgsConstructor
@NoArgsConstructor
@XmlAccessorType(XmlAccessType.FIELD)
static class Authentication implements AuthenticationInfo {
private String principal;
private String pluginCenterSubject;
private String refreshToken;
@XmlJavaTypeAdapter(XmlInstantAdapter.class)
private Instant date;
}
@Value
public static class RefreshRequest {
@JsonProperty("refresh_token")
String refreshToken;
}
@Data
public static class RefreshResponse {
@JsonProperty("access_token")
private String accessToken;
@JsonProperty("refresh_token")
private String refreshToken;
}
}

View File

@@ -42,6 +42,8 @@ public abstract class PluginCenterDtoMapper {
Set<AvailablePlugin> map(PluginCenterDto pluginCenterDto) {
Set<AvailablePlugin> plugins = new HashSet<>();
for (PluginCenterDto.Plugin plugin : pluginCenterDto.getEmbedded().getPlugins()) {
// plugin center api returns always a download link,
// but for cloudogu plugin without authentication the href is an empty string
String url = plugin.getLinks().get("download").getHref();
String installLink = getInstallLink(plugin);
AvailablePluginDescriptor descriptor = new AvailablePluginDescriptor(

View File

@@ -29,6 +29,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.event.ScmEventBus;
import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.net.ahc.AdvancedHttpRequest;
import javax.inject.Inject;
import java.util.Collections;
@@ -41,17 +42,24 @@ class PluginCenterLoader {
private static final Logger LOG = LoggerFactory.getLogger(PluginCenterLoader.class);
private final AdvancedHttpClient client;
private final PluginCenterAuthenticator authenticator;
private final PluginCenterDtoMapper mapper;
private final ScmEventBus eventBus;
@Inject
public PluginCenterLoader(AdvancedHttpClient client, ScmEventBus eventBus) {
this(client, PluginCenterDtoMapper.INSTANCE, eventBus);
public PluginCenterLoader(AdvancedHttpClient client, ScmEventBus eventBus, PluginCenterAuthenticator authenticator) {
this(client, authenticator, PluginCenterDtoMapper.INSTANCE, eventBus);
}
@VisibleForTesting
PluginCenterLoader(AdvancedHttpClient client, PluginCenterDtoMapper mapper, ScmEventBus eventBus) {
PluginCenterLoader(
AdvancedHttpClient client,
PluginCenterAuthenticator authenticator,
PluginCenterDtoMapper mapper,
ScmEventBus eventBus
) {
this.client = client;
this.authenticator = authenticator;
this.mapper = mapper;
this.eventBus = eventBus;
}
@@ -59,8 +67,11 @@ class PluginCenterLoader {
Set<AvailablePlugin> load(String url) {
try {
LOG.info("fetch plugins from {}", url);
PluginCenterDto pluginCenterDto = client.get(url).spanKind(SPAN_KIND).request()
.contentFromJson(PluginCenterDto.class);
AdvancedHttpRequest request = client.get(url).spanKind(SPAN_KIND);
if (authenticator.isAuthenticated()) {
request.bearerAuth(authenticator.fetchAccessToken());
}
PluginCenterDto pluginCenterDto = request.request().contentFromJson(PluginCenterDto.class);
return mapper.map(pluginCenterDto);
} catch (Exception ex) {
LOG.error("failed to load plugins from plugin center, returning empty list", ex);

View File

@@ -0,0 +1,38 @@
/*
* 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.plugin;
import lombok.Value;
import sonia.scm.event.Event;
/**
* Event is fired after a successful login to the plugin center.
* @since 2.28.0
*/
@Event
@Value
public class PluginCenterLoginEvent implements PluginCenterAuthenticationEvent {
AuthenticationInfo authenticationInfo;
}

View File

@@ -0,0 +1,38 @@
/*
* 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.plugin;
import lombok.Value;
import sonia.scm.event.Event;
/**
* Event is fired after a successful logout from plugin center.
* @since 2.28.0
*/
@Event
@Value
public class PluginCenterLogoutEvent implements PluginCenterAuthenticationEvent {
AuthenticationInfo priorAuthenticationInfo;
}

View File

@@ -0,0 +1,61 @@
/*
* 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.plugin;
import sonia.scm.EagerSingleton;
import sonia.scm.schedule.Scheduler;
import javax.inject.Inject;
/**
* Refresh plugin center cache and refresh the token of plugin center authentication.
* @since 2.28.0
*/
@Extension
@EagerSingleton
public class PluginCenterRefresh {
@Inject
@SuppressWarnings("java:S1118") // could not hide constructor
public PluginCenterRefresh(Scheduler scheduler) {
scheduler.schedule("42 42 0/6 * * ?", RefreshTask.class);
}
public static class RefreshTask implements Runnable {
private final PluginCenter pluginCenter;
@Inject
public RefreshTask(PluginCenter pluginCenter) {
this.pluginCenter = pluginCenter;
}
@Override
public void run() {
pluginCenter.refresh();
}
}
}

View File

@@ -29,6 +29,7 @@ import com.google.common.hash.Hashing;
import com.google.common.hash.HashingInputStream;
import sonia.scm.SCMContextProvider;
import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.net.ahc.AdvancedHttpRequest;
import javax.inject.Inject;
import java.io.IOException;
@@ -40,18 +41,19 @@ import java.util.Optional;
import static sonia.scm.plugin.Tracing.SPAN_KIND;
@SuppressWarnings("UnstableApiUsage")
// guava hash is marked as unstable
@SuppressWarnings("UnstableApiUsage") // guava hash is marked as unstable
class PluginInstaller {
private final SCMContextProvider scmContext;
private final AdvancedHttpClient client;
private final PluginCenterAuthenticator authenticator;
private final SmpDescriptorExtractor smpDescriptorExtractor;
@Inject
public PluginInstaller(SCMContextProvider scmContext, AdvancedHttpClient client, SmpDescriptorExtractor smpDescriptorExtractor) {
public PluginInstaller(SCMContextProvider scmContext, AdvancedHttpClient client, PluginCenterAuthenticator authenticator, SmpDescriptorExtractor smpDescriptorExtractor) {
this.scmContext = scmContext;
this.client = client;
this.authenticator = authenticator;
this.smpDescriptorExtractor = smpDescriptorExtractor;
}
@@ -128,7 +130,11 @@ class PluginInstaller {
}
private InputStream download(AvailablePlugin plugin) throws IOException {
return client.get(plugin.getDescriptor().getUrl()).spanKind(SPAN_KIND).request().contentAsStream();
AdvancedHttpRequest request = client.get(plugin.getDescriptor().getUrl()).spanKind(SPAN_KIND);
if (authenticator.isAuthenticated()) {
request.bearerAuth(authenticator.fetchAccessToken());
}
return request.request().contentAsStream();
}
private Path createFile(AvailablePlugin plugin) throws IOException {

View File

@@ -0,0 +1,51 @@
/*
* 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.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.inject.Inject;
import java.io.IOException;
public final class SecureParameterSerializer {
private final ObjectMapper mapper;
@Inject
public SecureParameterSerializer(ObjectMapper mapper) {
this.mapper = mapper;
}
public String serialize(Object object) throws IOException {
String json = mapper.writeValueAsString(object);
return CipherUtil.getInstance().encode(json);
}
public <T> T deserialize(String serialized, Class<T> type) throws IOException {
String decoded = CipherUtil.getInstance().decode(serialized);
return mapper.readValue(decoded, type);
}
}

View File

@@ -50,16 +50,18 @@ public class XsrfAccessTokenValidator implements AccessTokenValidator {
);
private final Provider<HttpServletRequest> requestProvider;
private final XsrfExcludes excludes;
/**
* Constructs a new instance.
*
*
* @param requestProvider http request provider
* @param excludes
*/
@Inject
public XsrfAccessTokenValidator(Provider<HttpServletRequest> requestProvider) {
public XsrfAccessTokenValidator(Provider<HttpServletRequest> requestProvider, XsrfExcludes excludes) {
this.requestProvider = requestProvider;
this.excludes = excludes;
}
@Override
@@ -67,6 +69,11 @@ public class XsrfAccessTokenValidator implements AccessTokenValidator {
Optional<String> xsrfClaim = accessToken.getCustom(Xsrf.TOKEN_KEY);
if (xsrfClaim.isPresent()) {
HttpServletRequest request = requestProvider.get();
if (excludes.contains(request.getRequestURI())) {
return true;
}
String xsrfHeaderValue = request.getHeader(Xsrf.HEADER_KEY);
return ALLOWED_METHOD.contains(request.getMethod().toUpperCase(Locale.ENGLISH))
|| xsrfClaim.get().equals(xsrfHeaderValue);

View File

@@ -0,0 +1,68 @@
/*
* 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.security;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import javax.inject.Singleton;
import java.util.HashSet;
import java.util.Set;
/**
* XsrfExcludes can be used to define request uris which are excluded from xsrf validation.
* @since 2.28.0
*/
@Singleton
public class XsrfExcludes {
private final Set<String> excludes = new HashSet<>();
/**
* Exclude the given request uri from xsrf validation.
* @param requestUri request uri
*/
public void add(String requestUri) {
excludes.add(requestUri);
}
/**
* Include prior excluded request uri to xsrf validation.
* @param requestUri request uri
* @return {@code true} is uri was excluded
*/
@CanIgnoreReturnValue
public boolean remove(String requestUri) {
return excludes.remove(requestUri);
}
/**
* Returns {@code true} if the request uri is excluded from xsrf validation.
* @param requestUri request uri
* @return {@code true} if uri is excluded
*/
public boolean contains(String requestUri) {
return excludes.contains(requestUri);
}
}