mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-02-01 04:09:08 +01:00
Merge branch 'develop' into bugfix/api-key-to-access-token
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user