Merge branch 'develop' into bugfix/api-key-to-access-token

This commit is contained in:
Konstantin Schaper
2020-11-03 14:48:39 +01:00
committed by GitHub
117 changed files with 2003 additions and 833 deletions

View File

@@ -46,8 +46,6 @@ public class ManagerDaoAdapter<T extends ModelObject> {
if (notModified != null) {
permissionCheck.apply(notModified).check();
doThrow().violation("type must not be changed").when(!notModified.getType().equals(object.getType()));
AssertUtil.assertIsValid(object);
beforeUpdate.handle(notModified);

View File

@@ -35,16 +35,14 @@ import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import static sonia.scm.repository.Branch.VALID_BRANCH_NAMES;
@Getter
@Setter
@NoArgsConstructor
@SuppressWarnings("java:S2160") // we do not need this for dto
public class BranchDto extends HalRepresentation {
private static final String VALID_CHARACTERS_AT_START_AND_END = "\\w-,;\\]{}@&+=$#`|<>";
private static final String VALID_CHARACTERS = VALID_CHARACTERS_AT_START_AND_END + "/.";
static final String VALID_BRANCH_NAMES = "[" + VALID_CHARACTERS_AT_START_AND_END + "]([" + VALID_CHARACTERS + "]*[" + VALID_CHARACTERS_AT_START_AND_END + "])?";
@NotEmpty
@Length(min = 1, max = 100)
@Pattern(regexp = VALID_BRANCH_NAMES)

View File

@@ -21,7 +21,7 @@
* 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.Getter;
@@ -31,7 +31,7 @@ import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import static sonia.scm.api.v2.resources.BranchDto.VALID_BRANCH_NAMES;
import static sonia.scm.repository.Branch.VALID_BRANCH_NAMES;
@Getter
@Setter

View File

@@ -56,6 +56,7 @@ public class ConfigDto extends HalRepresentation {
private String pluginUrl;
private long loginAttemptLimitTimeout;
private boolean enabledXsrfProtection;
private boolean enabledUserConverter;
private String namespaceStrategy;
private String loginInfoUrl;
private String releaseFeedUrl;

View File

@@ -102,7 +102,7 @@ public class MeDtoFactory extends HalAppenderMapper {
if (UserPermissions.changePublicKeys(user).isPermitted()) {
linksBuilder.single(link("publicKeys", resourceLinks.user().publicKeys(user.getName())));
}
if (userManager.isTypeDefault(user) && UserPermissions.changePassword(user).isPermitted()) {
if (!user.isExternal() && UserPermissions.changePassword(user).isPermitted()) {
linksBuilder.single(link("password", resourceLinks.me().passwordChange()));
}
if (UserPermissions.changeApiKeys(user).isPermitted()) {

View File

@@ -123,6 +123,14 @@ class ResourceLinks {
return userLinkBuilder.method("getUserResource").parameters(name).method("overwritePassword").parameters().href();
}
public String toExternal(String name) {
return userLinkBuilder.method("getUserResource").parameters(name).method("toExternal").parameters().href();
}
public String toInternal(String name) {
return userLinkBuilder.method("getUserResource").parameters(name).method("toInternal").parameters().href();
}
public String publicKeys(String name) {
return publicKeyLinkBuilder.method("findAll").parameters(name).href();
}

View File

@@ -41,6 +41,7 @@ import java.time.Instant;
@NoArgsConstructor @Getter @Setter
public class UserDto extends HalRepresentation {
private boolean active;
private boolean external;
private Instant creationDate;
@NotEmpty
private String displayName;

View File

@@ -186,6 +186,71 @@ public class UserResource {
return Response.noContent().build();
}
/**
* This Endpoint is for Admin user to convert external user to internal.
* The oldPassword property of the DTO is not needed here. it will be ignored.
* The oldPassword property is needed in the MeResources when the actual user change the own password.
*
* <strong>Note:</strong> This method requires "user:modify" privilege to modify the password of other users.
*
* @param name name of the user to be modified
* @param passwordOverwrite change password object to modify password. the old password is here not required
*/
@PUT
@Path("convert-to-internal")
@Consumes(VndMediaType.USER)
@Operation(summary = "Converts an external user to internal", description = "Converts an external user to an internal one and set the new password.", tags = "User")
@ApiResponse(responseCode = "204", description = "update success")
@ApiResponse(responseCode = "400", description = "invalid body, e.g. the new password is missing")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"user\" privilege")
@ApiResponse(
responseCode = "404",
description = "not found, no user with the specified id/name available",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
))
@ApiResponse(responseCode = "500", description = "internal server error")
public Response toInternal(@PathParam("id") String name, @Valid PasswordOverwriteDto passwordOverwrite) {
UserDto dto = userToDtoMapper.map(userManager.get(name));
dto.setExternal(false);
adapter.update(name, existing -> dtoToUserMapper.map(dto, existing.getPassword()));
userManager.overwritePassword(name, passwordService.encryptPassword(passwordOverwrite.getNewPassword()));
return Response.noContent().build();
}
/**
* This Endpoint is for Admin user to convert internal user to external.
*
* <strong>Note:</strong> This method requires "user:modify" privilege to modify the password of other users.
*
* @param name name of the user to be modified
*/
@PUT
@Path("convert-to-external")
@Consumes(VndMediaType.USER)
@Operation(summary = "Converts an internal user to external", description = "Converts an internal user to an external one and removes the local password.", tags = "User")
@ApiResponse(responseCode = "204", description = "update success")
@ApiResponse(responseCode = "400", description = "invalid body, e.g. the new password is missing")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"user\" privilege")
@ApiResponse(
responseCode = "404",
description = "not found, no user with the specified id/name available",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
))
@ApiResponse(responseCode = "500", description = "internal server error")
public Response toExternal(@PathParam("id") String name) {
userManager.overwritePassword(name, null);
UserDto dto = userToDtoMapper.map(userManager.get(name));
dto.setExternal(true);
adapter.update(name, existing -> dtoToUserMapper.map(dto, existing.getPassword()));
return Response.noContent().build();
}
@Path("permissions")
public UserPermissionResource permissions() {
return userPermissionResource;

View File

@@ -21,7 +21,7 @@
* 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;
@@ -66,8 +66,11 @@ public abstract class UserToUserDtoMapper extends BaseMapper<User, UserDto> {
if (UserPermissions.modify(user).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.user().update(user.getName())));
linksBuilder.single(link("publicKeys", resourceLinks.user().publicKeys(user.getName())));
if (userManager.isTypeDefault(user)) {
if (user.isExternal()) {
linksBuilder.single(link("convertToInternal", resourceLinks.user().toInternal(user.getName())));
} else {
linksBuilder.single(link("password", resourceLinks.user().passwordChange(user.getName())));
linksBuilder.single(link("convertToExternal", resourceLinks.user().toExternal(user.getName())));
}
}
if (PermissionPermissions.read().isPermitted()) {

View File

@@ -389,8 +389,8 @@ public class DefaultUserManager extends AbstractUserManager
if (user == null) {
throw new NotFoundException(User.class, userId);
}
if (!isTypeDefault(user) || isAnonymousUser(user)) {
throw new ChangePasswordNotAllowedException(ContextEntry.ContextBuilder.entity("PasswordChange", "-").in(User.class, user.getName()), user.getType());
if (isAnonymousUser(user) || user.isExternal()) {
throw new ChangePasswordNotAllowedException(ContextEntry.ContextBuilder.entity("PasswordChange", "-").in(User.class, user.getName()), "external");
}
user.setPassword(newPassword);
this.modify(user);

View File

@@ -0,0 +1,56 @@
/*
* 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.user;
import lombok.extern.slf4j.Slf4j;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.plugin.Extension;
import javax.inject.Inject;
@Slf4j
@Extension
public class InternalToExternalUserConverter implements ExternalUserConverter{
private final ScmConfiguration scmConfiguration;
@Inject
public InternalToExternalUserConverter(ScmConfiguration scmConfiguration) {
this.scmConfiguration = scmConfiguration;
}
public User convert(User user) {
if (shouldConvertUser(user)) {
log.info("Convert internal user {} to external", user.getId());
user.setExternal(true);
user.setPassword(null);
}
return user;
}
private boolean shouldConvertUser(User user) {
return !user.isExternal() && scmConfiguration.isEnabledUserConverter();
}
}

View File

@@ -41,7 +41,7 @@ public class BrowserUserAgentProvider implements UserAgentProvider
/** Field description */
@VisibleForTesting
static final UserAgent CHROME = UserAgent.builder(
static final UserAgent CHROME = UserAgent.browser(
"Chrome").basicAuthenticationCharset(
Charsets.UTF_8).build();
@@ -50,21 +50,21 @@ public class BrowserUserAgentProvider implements UserAgentProvider
/** Field description */
@VisibleForTesting
static final UserAgent FIREFOX = UserAgent.builder("Firefox").build();
static final UserAgent FIREFOX = UserAgent.browser("Firefox").build();
/** Field description */
private static final String FIREFOX_PATTERN = "firefox";
/** Field description */
@VisibleForTesting
static final UserAgent MSIE = UserAgent.builder("Internet Explorer").build();
static final UserAgent MSIE = UserAgent.browser("Internet Explorer").build();
/** Field description */
private static final String MSIE_PATTERN = "msie";
/** Field description */
@VisibleForTesting // todo check charset
static final UserAgent SAFARI = UserAgent.builder("Safari").build();
static final UserAgent SAFARI = UserAgent.browser("Safari").build();
/** Field description */
private static final String OPERA_PATTERN = "opera";
@@ -74,7 +74,7 @@ public class BrowserUserAgentProvider implements UserAgentProvider
/** Field description */
@VisibleForTesting // todo check charset
static final UserAgent OPERA = UserAgent.builder(
static final UserAgent OPERA = UserAgent.browser(
"Opera").basicAuthenticationCharset(
Charsets.UTF_8).build();

View File

@@ -74,10 +74,7 @@ public class HttpProtocolServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
UserAgent userAgent = userAgentParser.parse(request);
if (userAgent.isBrowser()) {
log.trace("dispatch browser request for user agent {}", userAgent);
dispatcher.dispatch(request, response, request.getRequestURI());
} else {
if (userAgent.isScmClient()) {
String pathInfo = request.getPathInfo();
Optional<NamespaceAndName> namespaceAndName = pathExtractor.fromUri(pathInfo);
if (namespaceAndName.isPresent()) {
@@ -86,6 +83,9 @@ public class HttpProtocolServlet extends HttpServlet {
log.debug("namespace and name not found in request path {}", pathInfo);
response.setStatus(HttpStatus.SC_BAD_REQUEST);
}
} else {
log.trace("dispatch non-scm-client request for user agent {}", userAgent);
dispatcher.dispatch(request, response, request.getRequestURI());
}
}