Add plugin wizard initialization step (#2045)

Adds a new initialization step after setting up the initial administration account that allows administrators to initialize the instance with a selection of plugin sets.

Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>
Co-authored-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com>
Co-authored-by: Matthias Thieroff <matthias.thieroff@cloudogu.com>
This commit is contained in:
Konstantin Schaper
2022-05-31 15:14:52 +02:00
committed by Eduard Heimbuch
parent 6216945f0d
commit 1b18191c57
63 changed files with 2294 additions and 135 deletions

View File

@@ -27,14 +27,20 @@ package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.Links;
import lombok.Data;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.UnauthenticatedException;
import sonia.scm.initialization.InitializationAuthenticationService;
import sonia.scm.initialization.InitializationStepResource;
import sonia.scm.lifecycle.AdminAccountStartupAction;
import sonia.scm.plugin.Extension;
import sonia.scm.security.AllowAnonymousAccess;
import sonia.scm.security.Tokens;
import sonia.scm.util.ValidationUtil;
import javax.inject.Inject;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
@@ -42,9 +48,12 @@ import javax.validation.constraints.Pattern;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import static de.otto.edison.hal.Link.link;
import static sonia.scm.ScmConstraintViolationException.Builder.doThrow;
import static sonia.scm.initialization.InitializationWebTokenGenerator.INIT_TOKEN_HEADER;
@AllowAnonymousAccess
@Extension
@@ -52,20 +61,34 @@ public class AdminAccountStartupResource implements InitializationStepResource {
private final AdminAccountStartupAction adminAccountStartupAction;
private final ResourceLinks resourceLinks;
private final InitializationAuthenticationService authenticationService;
@Inject
public AdminAccountStartupResource(AdminAccountStartupAction adminAccountStartupAction, ResourceLinks resourceLinks) {
public AdminAccountStartupResource(AdminAccountStartupAction adminAccountStartupAction, ResourceLinks resourceLinks, InitializationAuthenticationService authenticationService) {
this.adminAccountStartupAction = adminAccountStartupAction;
this.resourceLinks = resourceLinks;
this.authenticationService = authenticationService;
}
@POST
@Path("")
@Consumes("application/json")
public void postAdminInitializationData(@Valid AdminInitializationData data) {
public Response postAdminInitializationData(
@Context HttpServletRequest request,
@Context HttpServletResponse response,
@Valid AdminInitializationData data
) {
verifyInInitialization();
verifyToken(data);
createAdminUser(data);
// Invalidate old access token cookies to prevent conflicts during authentication
authenticationService.invalidateCookies(request, response);
SecurityUtils.getSubject().login(Tokens.createAuthenticationToken(request, data.userName, data.password));
// Create cookie which will be used for authentication during the initialization process
authenticationService.authenticate(request, response);
return Response.noContent().build();
}
private void verifyInInitialization() {

View File

@@ -168,7 +168,7 @@ public class AvailablePluginResource {
)
@ApiResponse(responseCode = "200", description = "success")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:manage\" privilege")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:write\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",

View File

@@ -47,6 +47,7 @@ import sonia.scm.web.EdisonHalAppender;
import javax.inject.Inject;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import static de.otto.edison.hal.Embedded.embeddedBuilder;
@@ -75,6 +76,10 @@ public class IndexDtoGenerator extends HalAppenderMapper {
}
public IndexDto generate() {
return generate(Locale.getDefault());
}
public IndexDto generate(Locale locale) {
Links.Builder builder = Links.linkingTo();
Embedded.Builder embeddedBuilder = embeddedBuilder();
@@ -84,7 +89,7 @@ public class IndexDtoGenerator extends HalAppenderMapper {
if (initializationFinisher.isFullyInitialized()) {
return handleNormalIndex(builder, embeddedBuilder);
} else {
return handleInitialization(builder, embeddedBuilder);
return handleInitialization(builder, embeddedBuilder, locale);
}
}
@@ -170,11 +175,11 @@ public class IndexDtoGenerator extends HalAppenderMapper {
.collect(Collectors.toList());
}
private IndexDto handleInitialization(Links.Builder builder, Embedded.Builder embeddedBuilder) {
private IndexDto handleInitialization(Links.Builder builder, Embedded.Builder embeddedBuilder, Locale locale) {
Links.Builder initializationLinkBuilder = Links.linkingTo();
Embedded.Builder initializationEmbeddedBuilder = embeddedBuilder();
InitializationStep initializationStep = initializationFinisher.missingInitialization();
initializationFinisher.getResource(initializationStep.name()).setupIndex(initializationLinkBuilder, initializationEmbeddedBuilder);
initializationFinisher.getResource(initializationStep.name()).setupIndex(initializationLinkBuilder, initializationEmbeddedBuilder, locale);
embeddedBuilder.with(initializationStep.name(), new InitializationDto(initializationLinkBuilder.build(), initializationEmbeddedBuilder.build()));
return new IndexDto(builder.build(), embeddedBuilder.build(), scmContextProvider.getVersion(), scmContextProvider.getInstanceId(), initializationStep.name());
}

View File

@@ -35,9 +35,11 @@ import sonia.scm.security.AllowAnonymousAccess;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
@OpenAPIDefinition(
security = {
@@ -80,7 +82,7 @@ public class IndexResource {
schema = @Schema(implementation = ErrorDto.class)
)
)
public IndexDto getIndex() {
return indexDtoGenerator.generate();
public IndexDto getIndex(@Context HttpServletRequest request) {
return indexDtoGenerator.generate(request.getLocale());
}
}

View File

@@ -112,7 +112,7 @@ public class InstalledPluginResource {
)
@ApiResponse(responseCode = "200", description = "success")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:manage\" privilege")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:write\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
@@ -191,7 +191,7 @@ public class InstalledPluginResource {
)
@ApiResponse(responseCode = "200", description = "success")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:manage\" privilege")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:write\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",

View File

@@ -160,7 +160,7 @@ public class PendingPluginResource {
)
@ApiResponse(responseCode = "200", description = "success")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:manage\" privilege")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:write\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
@@ -183,7 +183,7 @@ public class PendingPluginResource {
)
@ApiResponse(responseCode = "200", description = "success")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:manage\" privilege")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:write\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",

View File

@@ -0,0 +1,42 @@
/*
* 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 de.otto.edison.hal.HalRepresentation;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.Set;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuppressWarnings("squid:S2160") // we do not need equals for dto
public class PluginSetCollectionDto extends HalRepresentation {
Set<PluginSetDto> pluginSets;
}

View File

@@ -0,0 +1,49 @@
/*
* 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 de.otto.edison.hal.HalRepresentation;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
import java.util.Map;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuppressWarnings("squid:S2160") // we do not need equals for dto
public class PluginSetDto extends HalRepresentation {
private String id;
private int sequence;
private List<PluginDto> plugins;
private String name;
private List<String> features;
private Map<String, String> images;
}

View File

@@ -0,0 +1,65 @@
/*
* 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 sonia.scm.plugin.AvailablePlugin;
import sonia.scm.plugin.PluginSet;
import javax.inject.Inject;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.stream.Collectors;
public class PluginSetDtoMapper {
private final PluginDtoMapper pluginDtoMapper;
@Inject
protected PluginSetDtoMapper(PluginDtoMapper pluginDtoMapper) {
this.pluginDtoMapper = pluginDtoMapper;
}
public List<PluginSetDto> map(Collection<PluginSet> pluginSets, List<AvailablePlugin> availablePlugins, Locale locale) {
return pluginSets.stream()
.map(it -> map(it, availablePlugins, locale))
.sorted(Comparator.comparingInt(PluginSetDto::getSequence))
.collect(Collectors.toList());
}
private PluginSetDto map(PluginSet pluginSet, List<AvailablePlugin> availablePlugins, Locale locale) {
List<PluginDto> pluginDtos = pluginSet.getPlugins().stream()
.map(it -> availablePlugins.stream().filter(avail -> avail.getDescriptor().getInformation().getName().equals(it)).findFirst())
.filter(Optional::isPresent)
.map(Optional::get)
.map(pluginDtoMapper::mapAvailable)
.collect(Collectors.toList());
PluginSet.Description description = pluginSet.getDescriptions().get(locale.getLanguage());
return new PluginSetDto(pluginSet.getId(), pluginSet.getSequence(), pluginDtos, description.getName(), description.getFeatures(), pluginSet.getImages());
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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 lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.validation.constraints.NotNull;
import java.util.Set;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class PluginSetsInstallDto {
@NotNull
private Set<String> pluginSetIds;
}

View File

@@ -0,0 +1,149 @@
/*
* 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 de.otto.edison.hal.Embedded;
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.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import sonia.scm.initialization.InitializationStepResource;
import sonia.scm.lifecycle.PluginWizardStartupAction;
import sonia.scm.lifecycle.PrivilegedStartupAction;
import sonia.scm.plugin.AvailablePlugin;
import sonia.scm.plugin.Extension;
import sonia.scm.plugin.PluginManager;
import sonia.scm.plugin.PluginSet;
import sonia.scm.security.AccessTokenCookieIssuer;
import sonia.scm.web.VndMediaType;
import sonia.scm.web.security.AdministrationContext;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import static de.otto.edison.hal.Link.link;
import static sonia.scm.ScmConstraintViolationException.Builder.doThrow;
@Extension
public class PluginWizardStartupResource implements InitializationStepResource {
private final PluginWizardStartupAction pluginWizardStartupAction;
private final ResourceLinks resourceLinks;
private final PluginManager pluginManager;
private final AccessTokenCookieIssuer cookieIssuer;
private final PluginSetDtoMapper pluginSetDtoMapper;
private final AdministrationContext context;
@Inject
public PluginWizardStartupResource(PluginWizardStartupAction pluginWizardStartupAction, ResourceLinks resourceLinks, PluginManager pluginManager, AccessTokenCookieIssuer cookieIssuer, PluginSetDtoMapper pluginSetDtoMapper, AdministrationContext context) {
this.pluginWizardStartupAction = pluginWizardStartupAction;
this.resourceLinks = resourceLinks;
this.pluginManager = pluginManager;
this.cookieIssuer = cookieIssuer;
this.pluginSetDtoMapper = pluginSetDtoMapper;
this.context = context;
}
@Override
public void setupIndex(Links.Builder builder, Embedded.Builder embeddedBuilder) {
setupIndex(builder, embeddedBuilder, Locale.getDefault());
}
@Override
public void setupIndex(Links.Builder builder, Embedded.Builder embeddedBuilder, Locale locale) {
context.runAsAdmin((PrivilegedStartupAction)() -> {
Set<PluginSet> pluginSets = pluginManager.getPluginSets();
List<AvailablePlugin> availablePlugins = pluginManager.getAvailable();
List<PluginSetDto> pluginSetDtos = pluginSetDtoMapper.map(pluginSets, availablePlugins, locale);
embeddedBuilder.with("pluginSets", pluginSetDtos);
String link = resourceLinks.pluginWizard().indexLink(name());
builder.single(link("installPluginSets", link));
});
}
@Override
public String name() {
return pluginWizardStartupAction.name();
}
@POST
@Path("")
@Consumes("application/json")
@Operation(
summary = "Install plugin sets and restart",
description = "Installs all plugins contained in the provided plugin sets and restarts the server",
tags = "Plugin Management",
requestBody = @RequestBody(
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = PluginSetsInstallDto.class)
)
)
)
@ApiResponse(responseCode = "200", description = "success")
@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 installPluginSets(@Context HttpServletRequest request,
@Context HttpServletResponse response,
@Valid PluginSetsInstallDto dto) {
verifyInInitialization();
cookieIssuer.invalidate(request, response);
pluginManager.installPluginSets(dto.getPluginSetIds(), true);
return Response.ok().build();
}
private void verifyInInitialization() {
doThrow()
.violation("initialization not necessary")
.when(pluginWizardStartupAction.done());
}
}

View File

@@ -1207,6 +1207,25 @@ class ResourceLinks {
}
}
public PluginWizardLinks pluginWizard() {
return new PluginWizardLinks(new LinkBuilder(accessScmPathInfoStore().get(), InitializationResource.class, PluginWizardStartupResource.class));
}
public static class PluginWizardLinks {
private final LinkBuilder initializationLinkBuilder;
private PluginWizardLinks(LinkBuilder initializationLinkBuilder) {
this.initializationLinkBuilder = initializationLinkBuilder;
}
public String indexLink(String stepName) {
return initializationLinkBuilder
.method("step").parameters(stepName)
.method("installPluginSets").parameters()
.href();
}
}
public PluginCenterAuthLinks pluginCenterAuth() {
return new PluginCenterAuthLinks(scmPathInfoStore.get().get());
}

View File

@@ -0,0 +1,89 @@
/*
* 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.initialization;
import org.apache.shiro.authc.AuthenticationException;
import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.security.AccessTokenCookieIssuer;
import sonia.scm.security.PermissionAssigner;
import sonia.scm.security.PermissionDescriptor;
import sonia.scm.web.security.AdministrationContext;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Singleton
public class InitializationAuthenticationService {
private static final String INITIALIZATION_SUBJECT = "SCM-INIT";
private final AccessTokenBuilderFactory tokenBuilderFactory;
private final PermissionAssigner permissionAssigner;
private final AccessTokenCookieIssuer cookieIssuer;
private final InitializationCookieIssuer initializationCookieIssuer;
private final AdministrationContext administrationContext;
@Inject
public InitializationAuthenticationService(AccessTokenBuilderFactory tokenBuilderFactory, PermissionAssigner permissionAssigner, AccessTokenCookieIssuer cookieIssuer, InitializationCookieIssuer initializationCookieIssuer, AdministrationContext administrationContext) {
this.tokenBuilderFactory = tokenBuilderFactory;
this.permissionAssigner = permissionAssigner;
this.cookieIssuer = cookieIssuer;
this.initializationCookieIssuer = initializationCookieIssuer;
this.administrationContext = administrationContext;
}
public void validateToken(AccessToken token) {
if (token == null || !INITIALIZATION_SUBJECT.equals(token.getSubject())) {
throw new AuthenticationException("Could not authenticate to initialization realm because of missing or invalid token.");
}
}
public void setPermissions() {
administrationContext.runAsAdmin(() -> permissionAssigner.setPermissionsForUser(
InitializationRealm.INIT_PRINCIPAL,
Set.of(new PermissionDescriptor("plugin:read,write"))
));
}
public void authenticate(HttpServletRequest request, HttpServletResponse response) {
AccessToken initToken =
tokenBuilderFactory.create()
.subject(INITIALIZATION_SUBJECT)
.expiresIn(365, TimeUnit.DAYS)
.refreshableFor(0, TimeUnit.SECONDS)
.build();
initializationCookieIssuer.authenticateForInitialization(request, response, initToken);
}
public void invalidateCookies(HttpServletRequest request, HttpServletResponse response) {
cookieIssuer.invalidate(request, response);
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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.initialization;
import sonia.scm.security.AccessToken;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Generates cookies and invalidates initialization token cookies.
*
* @author Sebastian Sdorra
* @since 2.35.0
*/
public interface InitializationCookieIssuer {
/**
* Creates a cookie for token authentication and attaches it to the response.
*
* @param request http servlet request
* @param response http servlet response
* @param accessToken initialization access token
*/
void authenticateForInitialization(HttpServletRequest request, HttpServletResponse response, AccessToken accessToken);
}

View File

@@ -0,0 +1,78 @@
/*
* 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.initialization;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.subject.SimplePrincipalCollection;
import sonia.scm.plugin.Extension;
import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenResolver;
import sonia.scm.security.BearerToken;
import sonia.scm.user.User;
import javax.inject.Inject;
import javax.inject.Singleton;
import static com.google.common.base.Preconditions.checkArgument;
@Extension
@Singleton
public class InitializationRealm extends AuthenticatingRealm {
private static final String REALM = "InitializationRealm";
public static final String INIT_PRINCIPAL = "__SCM_INIT__";
private final InitializationAuthenticationService authenticationService;
private final AccessTokenResolver accessTokenResolver;
@Inject
public InitializationRealm(InitializationAuthenticationService authenticationService, AccessTokenResolver accessTokenResolver) {
this.authenticationService = authenticationService;
this.accessTokenResolver = accessTokenResolver;
setAuthenticationTokenClass(InitializationToken.class);
setCredentialsMatcher(new AllowAllCredentialsMatcher());
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
checkArgument(token instanceof InitializationToken, "%s is required", InitializationToken.class);
AccessToken accessToken = accessTokenResolver.resolve(BearerToken.valueOf(token.getCredentials().toString()));
authenticationService.validateToken(accessToken);
SimplePrincipalCollection principalCollection = new SimplePrincipalCollection(INIT_PRINCIPAL, REALM);
principalCollection.add(new User(INIT_PRINCIPAL), REALM);
authenticationService.setPermissions();
return new SimpleAuthenticationInfo(principalCollection, token.getCredentials());
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof InitializationToken;
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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.initialization;
import org.apache.shiro.authc.AuthenticationToken;
public class InitializationToken implements AuthenticationToken {
private final String token;
private final String principal;
public InitializationToken(String token, String principal) {
this.token = token;
this.principal = principal;
}
@Override
public Object getPrincipal() {
return principal;
}
@Override
public Object getCredentials() {
return token;
}
}

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.initialization;
import org.apache.shiro.authc.AuthenticationToken;
import sonia.scm.plugin.Extension;
import sonia.scm.web.WebTokenGenerator;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
@Extension
public class InitializationWebTokenGenerator implements WebTokenGenerator {
public static final String INIT_TOKEN_HEADER = "X-SCM-Init-Token";
@Override
public AuthenticationToken createToken(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
AuthenticationToken token = null;
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(INIT_TOKEN_HEADER)) {
token = new InitializationToken(cookie.getValue(), "SCM_INIT");
}
}
}
return token;
}
}

View File

@@ -48,7 +48,7 @@ public class AdminAccountStartupAction implements InitializationStep {
private static final Logger LOG = LoggerFactory.getLogger(AdminAccountStartupAction.class);
private static final String INITIAL_PASSWORD_PROPERTY = "scm.initialPassword";
public static final String INITIAL_PASSWORD_PROPERTY = "scm.initialPassword";
private static final String INITIAL_USER_PROPERTY = "scm.initialUser";
private final PasswordService passwordService;

View File

@@ -0,0 +1,60 @@
/*
* 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.lifecycle;
import sonia.scm.initialization.InitializationStep;
import sonia.scm.plugin.Extension;
import sonia.scm.plugin.PluginSetConfigStore;
import javax.inject.Inject;
import javax.inject.Singleton;
@Extension
@Singleton
public class PluginWizardStartupAction implements InitializationStep {
private final PluginSetConfigStore store;
@Inject
public PluginWizardStartupAction(PluginSetConfigStore pluginSetConfigStore) {
this.store = pluginSetConfigStore;
}
@Override
public String name() {
return "pluginWizard";
}
@Override
public int sequence() {
return 1;
}
@Override
public boolean done() {
return System.getProperty(AdminAccountStartupAction.INITIAL_PASSWORD_PROPERTY) != null || store.getPluginSets().isPresent();
}
}

View File

@@ -28,4 +28,4 @@ import sonia.scm.plugin.ExtensionPoint;
import sonia.scm.web.security.PrivilegedAction;
@ExtensionPoint
interface PrivilegedStartupAction extends PrivilegedAction {}
public interface PrivilegedStartupAction extends PrivilegedAction {}

View File

@@ -58,6 +58,7 @@ import sonia.scm.group.GroupManager;
import sonia.scm.group.GroupManagerProvider;
import sonia.scm.group.xml.XmlGroupDAO;
import sonia.scm.initialization.DefaultInitializationFinisher;
import sonia.scm.initialization.InitializationCookieIssuer;
import sonia.scm.initialization.InitializationFinisher;
import sonia.scm.io.ContentTypeResolver;
import sonia.scm.io.DefaultContentTypeResolver;
@@ -271,6 +272,7 @@ class ScmServletModule extends ServletModule {
// bind events
bind(AccessTokenCookieIssuer.class).to(DefaultAccessTokenCookieIssuer.class);
bind(InitializationCookieIssuer.class).to(DefaultAccessTokenCookieIssuer.class);
bind(PushStateDispatcher.class).toProvider(PushStateDispatcherProvider.class);
// bind api link provider

View File

@@ -39,6 +39,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -65,6 +66,8 @@ public class DefaultPluginManager implements PluginManager {
private final Restarter restarter;
private final ScmEventBus eventBus;
private final PluginSetConfigStore pluginSetConfigStore;
private final Collection<PendingPluginInstallation> pendingInstallQueue = new ArrayList<>();
private final Collection<PendingPluginUninstallation> pendingUninstallQueue = new ArrayList<>();
private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker();
@@ -72,16 +75,17 @@ public class DefaultPluginManager implements PluginManager {
private final Function<List<AvailablePlugin>, PluginInstallationContext> contextFactory;
@Inject
public DefaultPluginManager(PluginLoader loader, PluginCenter center, PluginInstaller installer, Restarter restarter, ScmEventBus eventBus) {
this(loader, center, installer, restarter, eventBus, null);
public DefaultPluginManager(PluginLoader loader, PluginCenter center, PluginInstaller installer, Restarter restarter, ScmEventBus eventBus, PluginSetConfigStore pluginSetConfigStore) {
this(loader, center, installer, restarter, eventBus, null, pluginSetConfigStore);
}
DefaultPluginManager(PluginLoader loader, PluginCenter center, PluginInstaller installer, Restarter restarter, ScmEventBus eventBus, Function<List<AvailablePlugin>, PluginInstallationContext> contextFactory) {
DefaultPluginManager(PluginLoader loader, PluginCenter center, PluginInstaller installer, Restarter restarter, ScmEventBus eventBus, Function<List<AvailablePlugin>, PluginInstallationContext> contextFactory, PluginSetConfigStore pluginSetConfigStore) {
this.loader = loader;
this.center = center;
this.installer = installer;
this.restarter = restarter;
this.eventBus = eventBus;
this.pluginSetConfigStore = pluginSetConfigStore;
if (contextFactory != null) {
this.contextFactory = contextFactory;
@@ -109,7 +113,7 @@ public class DefaultPluginManager implements PluginManager {
@Override
public Optional<AvailablePlugin> getAvailable(String name) {
PluginPermissions.read().check();
return center.getAvailable()
return center.getAvailablePlugins()
.stream()
.filter(filterByName(name))
.filter(this::isNotInstalledOrMoreUpToDate)
@@ -143,13 +147,49 @@ public class DefaultPluginManager implements PluginManager {
@Override
public List<AvailablePlugin> getAvailable() {
PluginPermissions.read().check();
return center.getAvailable()
return center.getAvailablePlugins()
.stream()
.filter(this::isNotInstalledOrMoreUpToDate)
.map(p -> getPending(p.getDescriptor().getInformation().getName()).orElse(p))
.collect(Collectors.toList());
}
@Override
public Set<PluginSet> getPluginSets() {
PluginPermissions.read().check();
return center.getAvailablePluginSets();
}
@Override
public void installPluginSets(Set<String> pluginSetIds, boolean restartAfterInstallation) {
PluginPermissions.write().check();
Set<PluginSet> pluginSets = getPluginSets();
Set<PluginSet> pluginSetsToInstall = pluginSetIds.stream()
.map(id -> pluginSets.stream().filter(pluginSet -> pluginSet.getId().equals(id)).findFirst())
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toSet());
Set<AvailablePlugin> pluginsToInstall = pluginSetsToInstall
.stream()
.flatMap(pluginSet -> pluginSet
.getPlugins()
.stream()
.map(this::collectPluginsToInstall)
.flatMap(Collection::stream)
)
.collect(Collectors.toSet());
Set<String> newlyInstalledPluginSetIds = pluginSetsToInstall.stream().map(PluginSet::getId).collect(Collectors.toSet());
Set<String> installedPluginSetIds = pluginSetConfigStore.getPluginSets().map(PluginSetsConfig::getPluginSets).orElse(new HashSet<>());
installedPluginSetIds.addAll(newlyInstalledPluginSetIds);
pluginSetConfigStore.setPluginSets(new PluginSetsConfig(installedPluginSetIds));
installPlugins(new ArrayList<>(pluginsToInstall), restartAfterInstallation);
}
@Override
public List<InstalledPlugin> getUpdatable() {
return getInstalled()
@@ -184,6 +224,10 @@ public class DefaultPluginManager implements PluginManager {
);
List<AvailablePlugin> plugins = collectPluginsToInstall(name);
installPlugins(plugins, restartAfterInstallation);
}
private void installPlugins(List<AvailablePlugin> plugins, boolean restartAfterInstallation) {
List<PendingPluginInstallation> pendingInstallations = new ArrayList<>();
for (AvailablePlugin plugin : plugins) {

View File

@@ -42,52 +42,61 @@ import java.util.Set;
@Singleton
public class PluginCenter {
private static final String CACHE_NAME = "sonia.cache.plugins";
private static final String PLUGIN_CENTER_RESULT_CACHE_NAME = "sonia.cache.plugin-center";
private static final Logger LOG = LoggerFactory.getLogger(PluginCenter.class);
private final SCMContextProvider context;
private final ScmConfiguration configuration;
private final PluginCenterLoader loader;
private final Cache<String, Set<AvailablePlugin>> cache;
private final Cache<String, PluginCenterResult> pluginCenterResultCache;
@Inject
public PluginCenter(SCMContextProvider context, CacheManager cacheManager, ScmConfiguration configuration, PluginCenterLoader loader) {
this.context = context;
this.configuration = configuration;
this.loader = loader;
this.cache = cacheManager.getCache(CACHE_NAME);
this.pluginCenterResultCache = cacheManager.getCache(PLUGIN_CENTER_RESULT_CACHE_NAME);
}
@Subscribe
public void handle(PluginCenterAuthenticationEvent event) {
LOG.debug("clear plugin center cache, because of {}", event);
cache.clear();
pluginCenterResultCache.clear();
}
synchronized Set<AvailablePlugin> getAvailable() {
synchronized Set<AvailablePlugin> getAvailablePlugins() {
String url = buildPluginUrl(configuration.getPluginUrl());
Set<AvailablePlugin> plugins = cache.get(url);
if (plugins == null) {
LOG.debug("no cached available plugins found, start fetching");
plugins = fetchAvailablePlugins(url);
return getPluginCenterResult(url).getPlugins();
}
synchronized Set<PluginSet> getAvailablePluginSets() {
String url = buildPluginUrl(configuration.getPluginUrl());
return getPluginCenterResult(url).getPluginSets();
}
private PluginCenterResult getPluginCenterResult(String url) {
PluginCenterResult pluginCenterResult = pluginCenterResultCache.get(url);
if (pluginCenterResult == null) {
LOG.debug("no cached plugin center result found, start fetching");
pluginCenterResult = fetchPluginCenter(url);
} else {
LOG.debug("return available plugins from cache");
LOG.debug("return plugin center result from cache");
}
return plugins;
return pluginCenterResult;
}
@CanIgnoreReturnValue
private Set<AvailablePlugin> fetchAvailablePlugins(String url) {
Set<AvailablePlugin> plugins = loader.load(url);
cache.put(url, plugins);
return plugins;
private PluginCenterResult fetchPluginCenter(String url) {
PluginCenterResult pluginCenterResult = loader.load(url);
pluginCenterResultCache.put(url, pluginCenterResult);
return pluginCenterResult;
}
synchronized void refresh() {
LOG.debug("refresh plugin center cache");
String url = buildPluginUrl(configuration.getPluginUrl());
fetchAvailablePlugins(url);
fetchPluginCenter(url);
}
private String buildPluginUrl(String url) {

View File

@@ -21,10 +21,9 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.plugin;
import com.google.common.collect.ImmutableList;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -56,12 +55,22 @@ public final class PluginCenterDto implements Serializable {
@XmlElement(name = "plugins")
private List<Plugin> plugins;
@XmlElement(name = "plugin-sets")
private List<PluginSet> pluginSets;
public List<Plugin> getPlugins() {
if (plugins == null) {
plugins = ImmutableList.of();
plugins = List.of();
}
return plugins;
}
public List<PluginSet> getPluginSets() {
if (pluginSets == null) {
pluginSets = List.of();
}
return pluginSets;
}
}
@XmlAccessorType(XmlAccessType.FIELD)
@@ -93,6 +102,36 @@ public final class PluginCenterDto implements Serializable {
private final Map<String, Link> links;
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "pluginSets")
@Getter
@AllArgsConstructor
public static class PluginSet {
private final String id;
private final String versions;
private final int sequence;
@XmlElement(name = "plugins")
private final Set<String> plugins;
@XmlElement(name = "descriptions")
private final Map<String, Description> descriptions;
@XmlElement(name = "images")
private final Map<String, String> images;
}
@XmlAccessorType(XmlAccessType.FIELD)
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class Description {
private String name;
@XmlElement(name = "features")
private List<String> features;
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "conditions")
@Getter

View File

@@ -29,18 +29,31 @@ import org.mapstruct.factory.Mappers;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
@Mapper
public abstract class PluginCenterDtoMapper {
PluginCenterDtoMapper() {}
static final PluginCenterDtoMapper INSTANCE = Mappers.getMapper(PluginCenterDtoMapper.class);
abstract PluginInformation map(PluginCenterDto.Plugin plugin);
abstract PluginCondition map(PluginCenterDto.Condition condition);
Set<AvailablePlugin> map(PluginCenterDto pluginCenterDto) {
abstract PluginSet map(PluginCenterDto.PluginSet set);
abstract PluginSet.Description map(PluginCenterDto.Description description);
PluginCenterResult map(PluginCenterDto pluginCenterDto) {
Set<AvailablePlugin> plugins = new HashSet<>();
Set<PluginSet> pluginSets = pluginCenterDto
.getEmbedded()
.getPluginSets()
.stream()
.map(this::map)
.collect(Collectors.toSet());
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
@@ -51,7 +64,7 @@ public abstract class PluginCenterDtoMapper {
);
plugins.add(new AvailablePlugin(descriptor));
}
return plugins;
return new PluginCenterResult(plugins, pluginSets);
}
private String getInstallLink(PluginCenterDto.Plugin plugin) {

View File

@@ -33,7 +33,6 @@ import sonia.scm.net.ahc.AdvancedHttpRequest;
import javax.inject.Inject;
import java.util.Collections;
import java.util.Set;
import static sonia.scm.plugin.Tracing.SPAN_KIND;
@@ -64,7 +63,7 @@ class PluginCenterLoader {
this.eventBus = eventBus;
}
Set<AvailablePlugin> load(String url) {
PluginCenterResult load(String url) {
try {
LOG.info("fetch plugins from {}", url);
AdvancedHttpRequest request = client.get(url).spanKind(SPAN_KIND);
@@ -76,7 +75,7 @@ class PluginCenterLoader {
} catch (Exception ex) {
LOG.error("failed to load plugins from plugin center, returning empty list", ex);
eventBus.post(new PluginCenterErrorEvent());
return Collections.emptySet();
return new PluginCenterResult(Collections.emptySet(), Collections.emptySet());
}
}
}

View File

@@ -0,0 +1,37 @@
/*
* 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.AllArgsConstructor;
import lombok.Getter;
import java.util.Set;
@AllArgsConstructor
@Getter
class PluginCenterResult {
private Set<AvailablePlugin> plugins;
private Set<PluginSet> pluginSets;
}

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.plugin;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;
@Singleton
public class PluginSetConfigStore {
private final ConfigurationStore<PluginSetsConfig> pluginSets;
@Inject
PluginSetConfigStore(ConfigurationStoreFactory configurationStoreFactory) {
pluginSets = configurationStoreFactory.withType(PluginSetsConfig.class).withName("pluginSets").build();
}
public Optional<PluginSetsConfig> getPluginSets() {
return pluginSets.getOptional();
}
public void setPluginSets(PluginSetsConfig config) {
this.pluginSets.set(config);
}
}

View File

@@ -0,0 +1,45 @@
/*
* 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.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.Set;
@Data
@XmlRootElement
@AllArgsConstructor
@NoArgsConstructor
@XmlAccessorType(XmlAccessType.FIELD)
public class PluginSetsConfig {
@XmlElement(name = "pluginSets")
Set<String> pluginSets;
}

View File

@@ -29,6 +29,7 @@ import com.google.common.base.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.initialization.InitializationCookieIssuer;
import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util;
@@ -36,16 +37,18 @@ import javax.inject.Inject;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import static sonia.scm.initialization.InitializationWebTokenGenerator.INIT_TOKEN_HEADER;
/**
* Generates cookies and invalidates access token cookies.
*
* @author Sebastian Sdorra
* @since 2.0.0
*/
public final class DefaultAccessTokenCookieIssuer implements AccessTokenCookieIssuer {
public final class DefaultAccessTokenCookieIssuer implements AccessTokenCookieIssuer, InitializationCookieIssuer {
/**
* the logger for DefaultAccessTokenCookieIssuer
@@ -87,6 +90,25 @@ public final class DefaultAccessTokenCookieIssuer implements AccessTokenCookieIs
response.addCookie(c);
}
/**
* Creates a cookie for authentication during the initialization process and attaches it to the response.
*
* @param request http servlet request
* @param response http servlet response
* @param accessToken initialization access token
*/
public void authenticateForInitialization(HttpServletRequest request, HttpServletResponse response, AccessToken accessToken) {
LOG.trace("create and attach cookie for initialization access token {}", accessToken.getId());
Cookie c = new Cookie(INIT_TOKEN_HEADER, accessToken.compact());
c.setPath(contextPath(request));
c.setMaxAge(999999999);
c.setHttpOnly(isHttpOnly());
c.setSecure(isSecure(request));
// attach cookie to response
response.addCookie(c);
}
/**
* Invalidates the authentication cookie.
*
@@ -95,8 +117,20 @@ public final class DefaultAccessTokenCookieIssuer implements AccessTokenCookieIs
*/
public void invalidate(HttpServletRequest request, HttpServletResponse response) {
LOG.trace("invalidates access token cookie");
invalidateCookie(request, response, HttpUtil.COOKIE_BEARER_AUTHENTICATION);
Cookie c = new Cookie(HttpUtil.COOKIE_BEARER_AUTHENTICATION, Util.EMPTY_STRING);
if (request.getCookies() != null && Arrays.stream(request.getCookies()).anyMatch(cookie -> cookie.getName().equals(INIT_TOKEN_HEADER))) {
LOG.trace("invalidates initialization token cookie");
invalidateInitTokenCookie(request, response);
}
}
private void invalidateInitTokenCookie(HttpServletRequest request, HttpServletResponse response) {
invalidateCookie(request, response, INIT_TOKEN_HEADER);
}
private void invalidateCookie(HttpServletRequest request, HttpServletResponse response, String cookieBearerAuthentication) {
Cookie c = new Cookie(cookieBearerAuthentication, Util.EMPTY_STRING);
c.setPath(contextPath(request));
c.setMaxAge(0);
c.setHttpOnly(isHttpOnly());

View File

@@ -0,0 +1,64 @@
/*
* 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.update.plugin;
import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.Extension;
import sonia.scm.plugin.PluginSetConfigStore;
import sonia.scm.plugin.PluginSetsConfig;
import sonia.scm.user.xml.XmlUserDAO;
import sonia.scm.version.Version;
import javax.inject.Inject;
import java.util.Collections;
@Extension
public class PluginSetsConfigInitializationUpdateStep implements UpdateStep {
private final PluginSetConfigStore pluginSetConfigStore;
private final XmlUserDAO userDAO;
@Inject
public PluginSetsConfigInitializationUpdateStep(PluginSetConfigStore pluginSetConfigStore, XmlUserDAO userDAO) {
this.pluginSetConfigStore = pluginSetConfigStore;
this.userDAO = userDAO;
}
@Override
public void doUpdate() throws Exception {
if (!userDAO.getAll().isEmpty() && pluginSetConfigStore.getPluginSets().isEmpty()) {
pluginSetConfigStore.setPluginSets(new PluginSetsConfig(Collections.emptySet()));
}
}
@Override
public Version getTargetVersion() {
return Version.parse("2.0.0");
}
@Override
public String getAffectedDataType() {
return "sonia.scm.plugin.PluginSetsConfig";
}
}