mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-18 03:01:05 +01:00
Create custom initial user (#1707)
Using a default user with a default password has the implicit risk, that this user is not changed and therefore this system can be compromised. With this change, SCM-Manager does not create the default user with the default password on startup any more, but it shows an initial form where the initial values for the administration user have to be entered by the user. To secure this form, a random token is created on startup and printed in the log. To implement this form, the concept of an InitializationStep is introduced. This extension point can be implemented to offer different setup tasks. The creation of the administration user is the first implementation, others might be things like first plugin selections or the like. Frontend components are selected by the name of these initialization steps, whose names will be added to the index resource (whichever is active at the moment) and will be show accordingly. Co-authored-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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 lombok.Data;
|
||||
import org.apache.shiro.authz.UnauthenticatedException;
|
||||
import sonia.scm.initialization.InitializationStepResource;
|
||||
import sonia.scm.lifecycle.AdminAccountStartupAction;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.security.AllowAnonymousAccess;
|
||||
import sonia.scm.util.ValidationUtil;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.Email;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.Pattern;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
|
||||
import static de.otto.edison.hal.Link.link;
|
||||
import static sonia.scm.ScmConstraintViolationException.Builder.doThrow;
|
||||
|
||||
@AllowAnonymousAccess
|
||||
@Extension
|
||||
public class AdminAccountStartupResource implements InitializationStepResource {
|
||||
|
||||
private final AdminAccountStartupAction adminAccountStartupAction;
|
||||
private final ResourceLinks resourceLinks;
|
||||
|
||||
@Inject
|
||||
public AdminAccountStartupResource(AdminAccountStartupAction adminAccountStartupAction, ResourceLinks resourceLinks) {
|
||||
this.adminAccountStartupAction = adminAccountStartupAction;
|
||||
this.resourceLinks = resourceLinks;
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("")
|
||||
@Consumes("application/json")
|
||||
public void postAdminInitializationData(@Valid AdminInitializationData data) {
|
||||
verifyInInitialization();
|
||||
verifyToken(data);
|
||||
createAdminUser(data);
|
||||
}
|
||||
|
||||
private void verifyInInitialization() {
|
||||
doThrow()
|
||||
.violation("initialization not necessary")
|
||||
.when(adminAccountStartupAction.done());
|
||||
}
|
||||
|
||||
private void verifyToken(AdminInitializationData data) {
|
||||
String givenStartupToken = data.getStartupToken();
|
||||
|
||||
if (!adminAccountStartupAction.isCorrectToken(givenStartupToken)) {
|
||||
throw new UnauthenticatedException("wrong password");
|
||||
}
|
||||
}
|
||||
|
||||
private void createAdminUser(AdminInitializationData data) {
|
||||
String userName = data.getUserName();
|
||||
String displayName = data.getDisplayName();
|
||||
String email = data.getEmail();
|
||||
String password = data.getPassword();
|
||||
String passwordConfirmation = data.getPasswordConfirmation();
|
||||
|
||||
verifyPasswordConfirmation(password, passwordConfirmation);
|
||||
|
||||
adminAccountStartupAction.createAdminUser(userName, displayName, email, password);
|
||||
}
|
||||
|
||||
private void verifyPasswordConfirmation(String password, String passwordConfirmation) {
|
||||
doThrow()
|
||||
.violation("password and confirmation differ", "password")
|
||||
.when(!password.equals(passwordConfirmation));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupIndex(Links.Builder builder, Embedded.Builder embeddedBuilder) {
|
||||
String link = resourceLinks.initialAdminAccount().indexLink(name());
|
||||
builder.single(link("initialAdminUser", link));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return adminAccountStartupAction.name();
|
||||
}
|
||||
|
||||
@Data
|
||||
static class AdminInitializationData {
|
||||
@NotEmpty
|
||||
private String startupToken;
|
||||
@Pattern(regexp = ValidationUtil.REGEX_NAME)
|
||||
private String userName;
|
||||
@NotEmpty
|
||||
private String displayName;
|
||||
@Email
|
||||
private String email;
|
||||
@NotEmpty
|
||||
private String password;
|
||||
@NotEmpty
|
||||
private String passwordConfirmation;
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,10 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import de.otto.edison.hal.Embedded;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
@@ -34,8 +35,16 @@ public class IndexDto extends HalRepresentation {
|
||||
|
||||
private final String version;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
private final String initialization;
|
||||
|
||||
IndexDto(Links links, Embedded embedded, String version) {
|
||||
this(links, embedded, version, null);
|
||||
}
|
||||
|
||||
IndexDto(Links links, Embedded embedded, String version, String initialization) {
|
||||
super(links, embedded);
|
||||
this.version = version;
|
||||
this.initialization = initialization;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.config.ConfigurationPermissions;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.group.GroupPermissions;
|
||||
import sonia.scm.initialization.InitializationFinisher;
|
||||
import sonia.scm.initialization.InitializationStep;
|
||||
import sonia.scm.plugin.PluginPermissions;
|
||||
import sonia.scm.security.AnonymousMode;
|
||||
import sonia.scm.security.Authentications;
|
||||
@@ -52,20 +54,32 @@ public class IndexDtoGenerator extends HalAppenderMapper {
|
||||
private final ResourceLinks resourceLinks;
|
||||
private final SCMContextProvider scmContextProvider;
|
||||
private final ScmConfiguration configuration;
|
||||
private final InitializationFinisher initializationFinisher;
|
||||
|
||||
@Inject
|
||||
public IndexDtoGenerator(ResourceLinks resourceLinks, SCMContextProvider scmContextProvider, ScmConfiguration configuration) {
|
||||
public IndexDtoGenerator(ResourceLinks resourceLinks, SCMContextProvider scmContextProvider, ScmConfiguration configuration, InitializationFinisher initializationFinisher) {
|
||||
this.resourceLinks = resourceLinks;
|
||||
this.scmContextProvider = scmContextProvider;
|
||||
this.configuration = configuration;
|
||||
this.initializationFinisher = initializationFinisher;
|
||||
}
|
||||
|
||||
public IndexDto generate() {
|
||||
Links.Builder builder = Links.linkingTo();
|
||||
List<Link> autoCompleteLinks = Lists.newArrayList();
|
||||
Embedded.Builder embeddedBuilder = embeddedBuilder();
|
||||
|
||||
builder.self(resourceLinks.index().self());
|
||||
builder.single(link("uiPlugins", resourceLinks.uiPluginCollection().self()));
|
||||
|
||||
if (initializationFinisher.isFullyInitialized()) {
|
||||
return handleNormalIndex(builder, embeddedBuilder);
|
||||
} else {
|
||||
return handleInitialization(builder, embeddedBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
private IndexDto handleNormalIndex(Links.Builder builder, Embedded.Builder embeddedBuilder) {
|
||||
List<Link> autoCompleteLinks = Lists.newArrayList();
|
||||
String loginInfoUrl = configuration.getLoginInfoUrl();
|
||||
if (!Strings.isNullOrEmpty(loginInfoUrl)) {
|
||||
builder.single(link("loginInfo", loginInfoUrl));
|
||||
@@ -121,12 +135,19 @@ public class IndexDtoGenerator extends HalAppenderMapper {
|
||||
builder.single(link("login", resourceLinks.authentication().jsonLogin()));
|
||||
}
|
||||
|
||||
Embedded.Builder embeddedBuilder = embeddedBuilder();
|
||||
applyEnrichers(new EdisonHalAppender(builder, embeddedBuilder), new Index());
|
||||
|
||||
return new IndexDto(builder.build(), embeddedBuilder.build(), scmContextProvider.getVersion());
|
||||
}
|
||||
|
||||
private IndexDto handleInitialization(Links.Builder builder, Embedded.Builder embeddedBuilder) {
|
||||
Links.Builder initializationLinkBuilder = Links.linkingTo();
|
||||
Embedded.Builder initializationEmbeddedBuilder = embeddedBuilder();
|
||||
InitializationStep initializationStep = initializationFinisher.missingInitialization();
|
||||
initializationFinisher.getResource(initializationStep.name()).setupIndex(initializationLinkBuilder, initializationEmbeddedBuilder);
|
||||
embeddedBuilder.with(initializationStep.name(), new InitializationDto(initializationLinkBuilder.build(), initializationEmbeddedBuilder.build()));
|
||||
return new IndexDto(builder.build(), embeddedBuilder.build(), scmContextProvider.getVersion(), initializationStep.name());
|
||||
}
|
||||
|
||||
private boolean shouldAppendSubjectRelatedLinks() {
|
||||
return isAuthenticatedSubjectNotAnonymous()
|
||||
|| isAuthenticatedSubjectAllowedToBeAnonymous();
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
|
||||
public class InitializationDto extends HalRepresentation {
|
||||
|
||||
public InitializationDto(Links links, Embedded embedded) {
|
||||
super(links, embedded);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.initialization.InitializationStep;
|
||||
import sonia.scm.initialization.InitializationStepResource;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import java.util.Set;
|
||||
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
import static sonia.scm.NotFoundException.notFound;
|
||||
|
||||
@Path("v2/initialization/")
|
||||
public class InitializationResource {
|
||||
|
||||
private final Set<InitializationStepResource> steps;
|
||||
|
||||
@Inject
|
||||
public InitializationResource(Set<InitializationStepResource> steps) {
|
||||
this.steps = steps;
|
||||
}
|
||||
|
||||
@Path("{stepName}")
|
||||
public InitializationStepResource step(@PathParam("stepName") String stepName) {
|
||||
return steps.stream()
|
||||
.filter(step -> stepName.equals(step.name()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> notFound(entity(InitializationStep.class, stepName)));
|
||||
}
|
||||
}
|
||||
@@ -1112,4 +1112,23 @@ class ResourceLinks {
|
||||
return metricsLinkBuilder.method("metrics").parameters(type).href();
|
||||
}
|
||||
}
|
||||
|
||||
public InitialAdminAccountLinks initialAdminAccount() {
|
||||
return new InitialAdminAccountLinks(new LinkBuilder(scmPathInfoStore.get(), InitializationResource.class, AdminAccountStartupResource.class));
|
||||
}
|
||||
|
||||
public static class InitialAdminAccountLinks {
|
||||
private final LinkBuilder initializationLinkBuilder;
|
||||
|
||||
private InitialAdminAccountLinks(LinkBuilder initializationLinkBuilder) {
|
||||
this.initializationLinkBuilder = initializationLinkBuilder;
|
||||
}
|
||||
|
||||
public String indexLink(String stepName) {
|
||||
return initializationLinkBuilder
|
||||
.method("step").parameters(stepName)
|
||||
.method("postAdminInitializationData").parameters()
|
||||
.href();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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.EagerSingleton;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static java.util.Comparator.comparing;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
@EagerSingleton
|
||||
public class DefaultInitializationFinisher implements InitializationFinisher {
|
||||
|
||||
private final List<InitializationStep> steps;
|
||||
private final Provider<Set<InitializationStepResource>> resources;
|
||||
|
||||
@Inject
|
||||
public DefaultInitializationFinisher(Set<InitializationStep> steps, Provider<Set<InitializationStepResource>> resources) {
|
||||
this.steps = steps.stream().sorted(comparing(InitializationStep::sequence)).collect(toList());
|
||||
this.resources = resources;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFullyInitialized() {
|
||||
return steps.stream().allMatch(InitializationStep::done);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InitializationStep missingInitialization() {
|
||||
return steps
|
||||
.stream()
|
||||
.filter(step -> !step.done()).findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("all steps initialized"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public InitializationStepResource getResource(String name) {
|
||||
return resources.get()
|
||||
.stream()
|
||||
.filter(resource -> name.equals(resource.name()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("resource not found for initialization step " + name));
|
||||
}
|
||||
}
|
||||
@@ -25,45 +25,111 @@
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
import org.apache.shiro.authc.credential.PasswordService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.SCMContext;
|
||||
import sonia.scm.initialization.InitializationStep;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.security.PermissionAssigner;
|
||||
import sonia.scm.security.PermissionDescriptor;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserManager;
|
||||
import sonia.scm.web.security.AdministrationContext;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Collections;
|
||||
|
||||
import static sonia.scm.ScmConstraintViolationException.Builder.doThrow;
|
||||
|
||||
@Extension
|
||||
public class AdminAccountStartupAction implements PrivilegedStartupAction {
|
||||
@Singleton
|
||||
public class AdminAccountStartupAction implements InitializationStep {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AdminAccountStartupAction.class);
|
||||
|
||||
private static final String INITIAL_PASSWORD_PROPERTY = "scm.initialPassword";
|
||||
|
||||
private final PasswordService passwordService;
|
||||
private final UserManager userManager;
|
||||
private final PermissionAssigner permissionAssigner;
|
||||
private final RandomPasswordGenerator randomPasswordGenerator;
|
||||
private final AdministrationContext context;
|
||||
|
||||
private String initialToken;
|
||||
|
||||
@Inject
|
||||
public AdminAccountStartupAction(PasswordService passwordService, UserManager userManager, PermissionAssigner permissionAssigner) {
|
||||
public AdminAccountStartupAction(PasswordService passwordService, UserManager userManager, PermissionAssigner permissionAssigner, RandomPasswordGenerator randomPasswordGenerator, AdministrationContext context) {
|
||||
this.passwordService = passwordService;
|
||||
this.userManager = userManager;
|
||||
this.permissionAssigner = permissionAssigner;
|
||||
this.randomPasswordGenerator = randomPasswordGenerator;
|
||||
this.context = context;
|
||||
|
||||
initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (shouldCreateAdminAccount()) {
|
||||
createAdminAccount();
|
||||
private void initialize() {
|
||||
context.runAsAdmin((PrivilegedStartupAction)() -> {
|
||||
if (shouldCreateAdminAccount() && !adminUserCreatedWithGivenPassword()) {
|
||||
createStartupToken();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean adminUserCreatedWithGivenPassword() {
|
||||
String startupTokenByProperty = System.getProperty(INITIAL_PASSWORD_PROPERTY);
|
||||
if (startupTokenByProperty != null) {
|
||||
context.runAsAdmin((PrivilegedStartupAction) () ->
|
||||
createAdminUser("scmadmin", "SCM Administrator", "scm-admin@scm-manager.org", startupTokenByProperty));
|
||||
LOG.info("=================================================");
|
||||
LOG.info("== ==");
|
||||
LOG.info("== Created user 'scmadmin' with given password ==");
|
||||
LOG.info("== ==");
|
||||
LOG.info("=================================================");
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void createAdminAccount() {
|
||||
User scmadmin = new User("scmadmin", "SCM Administrator", "scm-admin@scm-manager.org");
|
||||
String password = passwordService.encryptPassword("scmadmin");
|
||||
scmadmin.setPassword(password);
|
||||
userManager.create(scmadmin);
|
||||
@Override
|
||||
public String name() {
|
||||
return "adminAccount";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int sequence() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean done() {
|
||||
return initialToken == null;
|
||||
}
|
||||
|
||||
public void createAdminUser(String userName, String displayName, String email, String password) {
|
||||
User admin = new User(userName, displayName, email);
|
||||
String encryptedPassword = passwordService.encryptPassword(password);
|
||||
admin.setPassword(encryptedPassword);
|
||||
doThrow().violation("invalid user name").when(!admin.isValid());
|
||||
PermissionDescriptor descriptor = new PermissionDescriptor("*");
|
||||
permissionAssigner.setPermissionsForUser("scmadmin", Collections.singleton(descriptor));
|
||||
context.runAsAdmin((PrivilegedStartupAction) () -> {
|
||||
userManager.create(admin);
|
||||
permissionAssigner.setPermissionsForUser(userName, Collections.singleton(descriptor));
|
||||
initialToken = null;
|
||||
});
|
||||
}
|
||||
|
||||
private void createStartupToken() {
|
||||
initialToken = randomPasswordGenerator.createRandomPassword();
|
||||
LOG.warn("====================================================");
|
||||
LOG.warn("== ==");
|
||||
LOG.warn("== Startup token for initial user creation ==");
|
||||
LOG.warn("== ==");
|
||||
LOG.warn("== {} ==", initialToken);
|
||||
LOG.warn("== ==");
|
||||
LOG.warn("====================================================");
|
||||
}
|
||||
|
||||
private boolean shouldCreateAdminAccount() {
|
||||
@@ -73,4 +139,8 @@ public class AdminAccountStartupAction implements PrivilegedStartupAction {
|
||||
private boolean onlyAnonymousUserExists() {
|
||||
return userManager.getAll().size() == 1 && userManager.contains(SCMContext.USER_ANONYMOUS);
|
||||
}
|
||||
|
||||
public boolean isCorrectToken(String givenStartupToken) {
|
||||
return initialToken.equals(givenStartupToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.lifecycle;
|
||||
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
final class RandomPasswordGenerator {
|
||||
|
||||
String createRandomPassword() {
|
||||
try {
|
||||
SecureRandom random = SecureRandom.getInstanceStrong();
|
||||
return RandomStringUtils.random(20, 0, 0, true, true, null, random);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("Every Java distribution is required to support a strong secure random generator; this should not have happened", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,8 @@ import sonia.scm.group.GroupDisplayManager;
|
||||
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.InitializationFinisher;
|
||||
import sonia.scm.metrics.MeterRegistryProvider;
|
||||
import sonia.scm.migration.MigrationDAO;
|
||||
import sonia.scm.net.SSLContextProvider;
|
||||
@@ -275,6 +277,8 @@ class ScmServletModule extends ServletModule {
|
||||
bind(HealthCheckService.class).to(DefaultHealthCheckService.class);
|
||||
|
||||
bind(NotificationSender.class).to(DefaultNotificationSender.class);
|
||||
|
||||
bind(InitializationFinisher.class).to(DefaultInitializationFinisher.class);
|
||||
}
|
||||
|
||||
private <T> void bind(Class<T> clazz, Class<? extends T> defaultImplementation) {
|
||||
|
||||
@@ -21,9 +21,10 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -80,7 +81,8 @@ public class JwtAccessTokenRefresher {
|
||||
}
|
||||
|
||||
private boolean canBeRefreshed(JwtAccessToken oldToken) {
|
||||
return tokenIsValid(oldToken) && tokenCanBeRefreshed(oldToken);
|
||||
return tokenIsValid(oldToken) && tokenCanBeRefreshed(oldToken)
|
||||
&& SecurityUtils.getSubject().getPrincipals() != null;
|
||||
}
|
||||
|
||||
private boolean shouldBeRefreshed(JwtAccessToken oldToken) {
|
||||
|
||||
Reference in New Issue
Block a user