mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-02-06 06:39:15 +01:00
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:
committed by
Eduard Heimbuch
parent
6216945f0d
commit
1b18191c57
@@ -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() {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user