diff --git a/api-docs/openapi/v3_0/aggregated.json b/api-docs/openapi/v3_0/aggregated.json index 22c51fda8..b2ef456a4 100644 --- a/api-docs/openapi/v3_0/aggregated.json +++ b/api-docs/openapi/v3_0/aggregated.json @@ -22881,14 +22881,12 @@ } }, "roles": { - "uniqueItems": true, "type": "array", "items": { "$ref": "#/components/schemas/Role" } }, "uiPermissions": { - "uniqueItems": true, "type": "array", "items": { "type": "string" diff --git a/api-docs/openapi/v3_0/apis_console.api_v1alpha1.json b/api-docs/openapi/v3_0/apis_console.api_v1alpha1.json index b87b68809..595b83bd1 100644 --- a/api-docs/openapi/v3_0/apis_console.api_v1alpha1.json +++ b/api-docs/openapi/v3_0/apis_console.api_v1alpha1.json @@ -6236,14 +6236,12 @@ } }, "roles": { - "uniqueItems": true, "type": "array", "items": { "$ref": "#/components/schemas/Role" } }, "uiPermissions": { - "uniqueItems": true, "type": "array", "items": { "type": "string" diff --git a/api/src/main/java/run/halo/app/core/extension/RoleBinding.java b/api/src/main/java/run/halo/app/core/extension/RoleBinding.java index 1d559522e..09ad63827 100644 --- a/api/src/main/java/run/halo/app/core/extension/RoleBinding.java +++ b/api/src/main/java/run/halo/app/core/extension/RoleBinding.java @@ -13,6 +13,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; +import org.springframework.util.StringUtils; import run.halo.app.extension.AbstractExtension; import run.halo.app.extension.ExtensionOperator; import run.halo.app.extension.GVK; @@ -118,6 +119,14 @@ public class RoleBinding extends AbstractExtension { && User.GROUP.equals(subject.apiGroup) && usernames.contains(subject.getName()); } + + @Override + public String toString() { + if (StringUtils.hasText(apiGroup)) { + return apiGroup + "/" + kind + "/" + name; + } + return kind + "/" + name; + } } public static RoleBinding create(String username, String roleName) { diff --git a/api/src/main/java/run/halo/app/core/extension/User.java b/api/src/main/java/run/halo/app/core/extension/User.java index 1d796b3eb..f925bc9ba 100644 --- a/api/src/main/java/run/halo/app/core/extension/User.java +++ b/api/src/main/java/run/halo/app/core/extension/User.java @@ -46,6 +46,8 @@ public class User extends AbstractExtension { public static final String HIDDEN_USER_LABEL = "halo.run/hidden-user"; + public static final String REQUEST_TO_UPDATE = "halo.run/request-to-update"; + @Schema(requiredMode = REQUIRED) private UserSpec spec = new UserSpec(); diff --git a/api/src/main/java/run/halo/app/extension/ExtensionUtil.java b/api/src/main/java/run/halo/app/extension/ExtensionUtil.java index a1f4d5cf9..cb50cd365 100644 --- a/api/src/main/java/run/halo/app/extension/ExtensionUtil.java +++ b/api/src/main/java/run/halo/app/extension/ExtensionUtil.java @@ -3,6 +3,9 @@ package run.halo.app.extension; import java.util.Collections; import java.util.HashSet; import java.util.Set; +import org.springframework.data.domain.Sort; +import run.halo.app.extension.index.query.Query; +import run.halo.app.extension.index.query.QueryFactory; public enum ExtensionUtil { ; @@ -34,4 +37,25 @@ public enum ExtensionUtil { return removed; } + /** + * Query for not deleting. + * + * @return Query + */ + public static Query notDeleting() { + return QueryFactory.isNull("metadata.deletionTimestamp"); + } + + /** + * Default sort by creation timestamp desc and name asc. + * + * @return Sort + */ + public static Sort defaultSort() { + return Sort.by( + Sort.Order.desc("metadata.creationTimestamp"), + Sort.Order.asc("metadata.name") + ); + } + } diff --git a/application/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java b/application/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java index a09df32cb..932a3925b 100644 --- a/application/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java +++ b/application/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java @@ -10,9 +10,9 @@ import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuil import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder; import static org.springframework.web.reactive.function.server.RequestPredicates.contentType; import static run.halo.app.extension.ListResult.generateGenericClass; -import static run.halo.app.extension.index.query.QueryFactory.and; import static run.halo.app.extension.index.query.QueryFactory.contains; import static run.halo.app.extension.index.query.QueryFactory.equal; +import static run.halo.app.extension.index.query.QueryFactory.in; import static run.halo.app.extension.index.query.QueryFactory.or; import static run.halo.app.extension.router.QueryParamBuildUtil.sortParameter; import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions; @@ -28,13 +28,15 @@ import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import java.security.Principal; import java.time.Duration; -import java.util.Collections; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; @@ -83,8 +85,6 @@ import run.halo.app.extension.MetadataUtil; import run.halo.app.extension.PageRequestImpl; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.router.IListRequest; -import run.halo.app.extension.router.selector.FieldSelector; -import run.halo.app.infra.AnonymousUserConst; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemSetting; import run.halo.app.infra.ValidationUtils; @@ -457,11 +457,14 @@ public class UserEndpoint implements CustomEndpoint { private Mono getUserByName(ServerRequest request) { final var name = request.pathVariable("name"); return userService.getUser(name) - .flatMap(this::toDetailedUser) - .flatMap(user -> ServerResponse.ok() - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(user) - ); + .flatMap(user -> roleService.getRolesByUsername(name) + .collectList() + .flatMap(roleNames -> roleService.list(new HashSet<>(roleNames), true) + .collectList() + .map(roles -> new DetailedUser(user, roles)) + ) + ) + .flatMap(detailedUser -> ServerResponse.ok().bodyValue(detailedUser)); } record CreateUserRequest(@Schema(requiredMode = REQUIRED) String name, @@ -600,33 +603,16 @@ public class UserEndpoint implements CustomEndpoint { Mono me(ServerRequest request) { return ReactiveSecurityContextHolder.getContext() .map(SecurityContext::getAuthentication) - .filter(obj -> !(obj instanceof TwoFactorAuthentication)) - .map(Authentication::getName) - .defaultIfEmpty(AnonymousUserConst.PRINCIPAL) - .flatMap(userService::getUser) - .flatMap(this::toDetailedUser) - .flatMap(user -> ServerResponse.ok() - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(user)); - } - - private Mono toDetailedUser(User user) { - Set roleNames = roleNames(user); - return roleService.list(roleNames) - .collectList() - .map(roles -> new DetailedUser(user, roles)) - .defaultIfEmpty(new DetailedUser(user, List.of())); - } - - Set roleNames(User user) { - Assert.notNull(user, "User must not be null"); - Map annotations = MetadataUtil.nullSafeAnnotations(user); - String roleNamesJson = annotations.get(User.ROLE_NAMES_ANNO); - if (StringUtils.isBlank(roleNamesJson)) { - return Set.of(); - } - return JsonUtils.jsonToObject(roleNamesJson, new TypeReference<>() { - }); + .filter(auth -> !(auth instanceof TwoFactorAuthentication)) + .flatMap(auth -> userService.getUser(auth.getName()) + .flatMap(user -> { + var roleNames = authoritiesToRoles(auth.getAuthorities()); + return roleService.list(roleNames, true) + .collectList() + .map(roles -> new DetailedUser(user, roles)); + }) + ) + .flatMap(detailedUser -> ServerResponse.ok().bodyValue(detailedUser)); } record DetailedUser(@Schema(requiredMode = REQUIRED) User user, @@ -649,85 +635,55 @@ public class UserEndpoint implements CustomEndpoint { @NonNull private Mono getUserPermission(ServerRequest request) { - var name = request.pathVariable("name"); - Mono userPermission; - if (SELF_USER.equals(name)) { - userPermission = ReactiveSecurityContextHolder.getContext() - .map(SecurityContext::getAuthentication) - .flatMap(auth -> { - var roleNames = authoritiesToRoles(auth.getAuthorities()); - var up = new UserPermission(); - var roles = roleService.list(roleNames) - .collect(Collectors.toSet()) - .doOnNext(up::setRoles) - .then(); - var permissions = roleService.listPermissions(roleNames) - .distinct() - .collectList() - .doOnNext(up::setPermissions) - .doOnNext(perms -> { - var uiPermissions = uiPermissions(new HashSet<>(perms)); - up.setUiPermissions(uiPermissions); - }) - .then(); - return roles.and(permissions).thenReturn(up); + var username = request.pathVariable("name"); + return Mono.defer(() -> { + if (SELF_USER.equals(username)) { + return ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .map(auth -> authoritiesToRoles(auth.getAuthorities())); + } + return roleService.getRolesByUsername(username) + .collect(Collectors.toCollection(LinkedHashSet::new)); + }).flatMap(roleNames -> { + var up = new UserPermission(); + var setRoles = roleService.list(roleNames, true) + .distinct() + .collectSortedList() + .doOnNext(up::setRoles); + var setPerms = roleService.listPermissions(roleNames) + .distinct() + .collectSortedList() + .doOnNext(permissions -> { + up.setPermissions(permissions); + up.setUiPermissions(uiPermissions(permissions)); }); - } else { - // get roles from username - userPermission = userService.listRoles(name) - .collect(Collectors.toSet()) - .flatMap(roles -> { - var up = new UserPermission(); - var setRoles = Mono.fromRunnable(() -> up.setRoles(roles)).then(); - var roleNames = roles.stream() - .map(role -> role.getMetadata().getName()) - .collect(Collectors.toSet()); - var setPermissions = roleService.listPermissions(roleNames) - .distinct() - .collectList() - .doOnNext(up::setPermissions) - .doOnNext(perms -> { - var uiPermissions = uiPermissions(new HashSet<>(perms)); - up.setUiPermissions(uiPermissions); - }) - .then(); - return setRoles.and(setPermissions).thenReturn(up); - }); - } - - return ServerResponse.ok().body(userPermission, UserPermission.class); + return Mono.when(setRoles, setPerms).thenReturn(up); + }).flatMap(userPermission -> ServerResponse.ok().bodyValue(userPermission)); } - private Set uiPermissions(Set roles) { + private List uiPermissions(Collection roles) { if (CollectionUtils.isEmpty(roles)) { - return Collections.emptySet(); + return List.of(); } - return roles.stream() - .>map(role -> { - var annotations = role.getMetadata().getAnnotations(); - if (annotations == null) { - return Set.of(); - } - var uiPermissionsJson = annotations.get(Role.UI_PERMISSIONS_ANNO); - if (StringUtils.isBlank(uiPermissionsJson)) { - return Set.of(); - } - return JsonUtils.jsonToObject(uiPermissionsJson, - new TypeReference>() { - }); - }) - .flatMap(Set::stream) - .collect(Collectors.toSet()); + var uiPerms = new LinkedList(); + roles.forEach(role -> Optional.ofNullable(role.getMetadata().getAnnotations()) + .map(annotations -> annotations.get(Role.UI_PERMISSIONS_ANNO)) + .filter(StringUtils::isNotBlank) + .map(json -> JsonUtils.jsonToObject(json, new TypeReference>() { + })) + .ifPresent(uiPerms::addAll) + ); + return uiPerms.stream().distinct().sorted().toList(); } @Data public static class UserPermission { @Schema(requiredMode = REQUIRED) - private Set roles; + private List roles; @Schema(requiredMode = REQUIRED) private List permissions; @Schema(requiredMode = REQUIRED) - private Set uiPermissions; + private List uiPermissions; } @@ -767,29 +723,23 @@ public class UserEndpoint implements CustomEndpoint { * Converts query parameters to list options. */ public ListOptions toListOptions() { - var listOptions = + var defaultListOptions = labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector()); - var fieldQuery = listOptions.getFieldSelector().query(); - if (StringUtils.isNotBlank(getKeyword())) { - fieldQuery = and( - fieldQuery, - or( - contains("spec.displayName", getKeyword()), - equal("metadata.name", getKeyword()) - ) - ); - } + var builder = ListOptions.builder(defaultListOptions); - if (StringUtils.isNotBlank(getRole())) { - fieldQuery = and( - fieldQuery, - equal(User.USER_RELATED_ROLES_INDEX, getRole()) - ); - } + Optional.ofNullable(getKeyword()) + .filter(StringUtils::isNotBlank) + .ifPresent(keyword -> builder.andQuery(or( + contains("spec.displayName", keyword), + equal("metadata.name", keyword) + ))); - listOptions.setFieldSelector(FieldSelector.of(fieldQuery)); - return listOptions; + Optional.ofNullable(getRole()) + .filter(StringUtils::isNotBlank) + .ifPresent(role -> builder.andQuery(in(User.USER_RELATED_ROLES_INDEX, role))); + + return builder.build(); } public static void buildParameters(Builder builder) { @@ -829,17 +779,28 @@ public class UserEndpoint implements CustomEndpoint { } private Mono> toListedUser(ListResult listResult) { - return Flux.fromStream(listResult.get()) - .concatMap(user -> { - Set roleNames = roleNames(user); - return roleService.list(roleNames) - .collectList() - .map(roles -> new ListedUser(user, roles)) - .defaultIfEmpty(new ListedUser(user, List.of())); - }) - .collectList() - .map(items -> convertFrom(listResult, items)) - .defaultIfEmpty(convertFrom(listResult, List.of())); + var usernames = listResult.getItems().stream() + .map(user -> user.getMetadata().getName()) + .collect(Collectors.toList()); + return roleService.getRolesByUsernames(usernames) + .flatMap(usernameRolesMap -> { + var allRoleNames = new HashSet(); + usernameRolesMap.values().forEach(allRoleNames::addAll); + return roleService.list(allRoleNames) + .collectMap(role -> role.getMetadata().getName()) + .map(roleMap -> { + var listedUsers = listResult.getItems().stream() + .map(user -> { + var username = user.getMetadata().getName(); + var roles = Optional.ofNullable(usernameRolesMap.get(username)) + .map(roleNames -> roleNames.stream().map(roleMap::get).toList()) + .orElseGet(List::of); + return new ListedUser(user, roles); + }) + .toList(); + return convertFrom(listResult, listedUsers); + }); + }); } ListResult convertFrom(ListResult listResult, List items) { diff --git a/application/src/main/java/run/halo/app/core/extension/reconciler/RoleBindingReconciler.java b/application/src/main/java/run/halo/app/core/extension/reconciler/RoleBindingReconciler.java deleted file mode 100644 index 427fdbec4..000000000 --- a/application/src/main/java/run/halo/app/core/extension/reconciler/RoleBindingReconciler.java +++ /dev/null @@ -1,82 +0,0 @@ -package run.halo.app.core.extension.reconciler; - -import static run.halo.app.core.extension.RoleBinding.containsUser; - -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.util.Lazy; -import org.springframework.stereotype.Component; -import run.halo.app.core.extension.Role; -import run.halo.app.core.extension.RoleBinding; -import run.halo.app.core.extension.RoleBinding.Subject; -import run.halo.app.core.extension.User; -import run.halo.app.extension.ExtensionClient; -import run.halo.app.extension.controller.Controller; -import run.halo.app.extension.controller.ControllerBuilder; -import run.halo.app.extension.controller.Reconciler; -import run.halo.app.extension.controller.Reconciler.Request; -import run.halo.app.infra.utils.JsonUtils; - -@Slf4j -@Component -public class RoleBindingReconciler implements Reconciler { - - private final ExtensionClient client; - - public RoleBindingReconciler(ExtensionClient client) { - this.client = client; - } - - @Override - public Result reconcile(Request request) { - client.fetch(RoleBinding.class, request.name()).ifPresent(roleBinding -> { - // get all usernames; - var usernames = roleBinding.getSubjects().stream() - .filter(subject -> User.KIND.equals(subject.getKind())) - .map(Subject::getName) - .collect(Collectors.toSet()); - - // get all role-bindings lazily - var bindings = - Lazy.of(() -> client.list(RoleBinding.class, containsUser(usernames), null)); - - usernames.forEach(username -> { - var roleNames = bindings.get().stream() - .filter(containsUser(username)) - .map(RoleBinding::getRoleRef) - .filter(roleRef -> Objects.equals(roleRef.getKind(), Role.KIND)) - .map(RoleBinding.RoleRef::getName) - .sorted() - // we have to use LinkedHashSet below to make sure the sorted above functional - .collect(Collectors.toCollection(LinkedHashSet::new)); - // we should update the role names even if the role names are empty - client.fetch(User.class, username).ifPresent(user -> { - var annotations = user.getMetadata().getAnnotations(); - if (annotations == null) { - annotations = new HashMap<>(); - } - var oldAnnotations = Map.copyOf(annotations); - annotations.put(User.ROLE_NAMES_ANNO, JsonUtils.objectToJson(roleNames)); - user.getMetadata().setAnnotations(annotations); - if (!Objects.deepEquals(oldAnnotations, annotations)) { - // update user - client.update(user); - } - }); - }); - }); - return new Result(false, null); - } - - @Override - public Controller setupWith(ControllerBuilder builder) { - return builder - .extension(new RoleBinding()) - .build(); - } - -} diff --git a/application/src/main/java/run/halo/app/core/extension/reconciler/UserReconciler.java b/application/src/main/java/run/halo/app/core/extension/reconciler/UserReconciler.java index 2ebcf6742..c74d087b1 100644 --- a/application/src/main/java/run/halo/app/core/extension/reconciler/UserReconciler.java +++ b/application/src/main/java/run/halo/app/core/extension/reconciler/UserReconciler.java @@ -1,20 +1,24 @@ package run.halo.app.core.extension.reconciler; -import static run.halo.app.core.extension.User.GROUP; -import static run.halo.app.core.extension.User.KIND; +import static run.halo.app.extension.ExtensionUtil.addFinalizers; +import static run.halo.app.extension.ExtensionUtil.defaultSort; +import static run.halo.app.extension.ExtensionUtil.isDeleted; +import static run.halo.app.extension.ExtensionUtil.removeFinalizers; +import static run.halo.app.extension.index.query.QueryFactory.equal; import java.net.URI; -import java.util.HashSet; +import java.time.Duration; +import java.util.HashMap; import java.util.List; -import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.springframework.retry.support.RetryTemplate; import org.springframework.stereotype.Component; -import run.halo.app.core.extension.Role; -import run.halo.app.core.extension.RoleBinding; +import org.springframework.util.CollectionUtils; +import org.springframework.web.util.UriComponentsBuilder; import run.halo.app.core.extension.User; import run.halo.app.core.extension.UserConnection; import run.halo.app.core.extension.attachment.Attachment; @@ -22,8 +26,7 @@ import run.halo.app.core.extension.service.AttachmentService; import run.halo.app.core.extension.service.RoleService; import run.halo.app.core.extension.service.UserService; import run.halo.app.extension.ExtensionClient; -import run.halo.app.extension.GroupKind; -import run.halo.app.extension.MetadataUtil; +import run.halo.app.extension.ListOptions; import run.halo.app.extension.controller.Controller; import run.halo.app.extension.controller.ControllerBuilder; import run.halo.app.extension.controller.Reconciler; @@ -32,7 +35,6 @@ import run.halo.app.extension.controller.RequeueException; import run.halo.app.infra.AnonymousUserConst; import run.halo.app.infra.ExternalUrlSupplier; import run.halo.app.infra.utils.JsonUtils; -import run.halo.app.infra.utils.PathUtils; @Slf4j @Component @@ -43,26 +45,21 @@ public class UserReconciler implements Reconciler { private final ExternalUrlSupplier externalUrlSupplier; private final RoleService roleService; private final AttachmentService attachmentService; - private final RetryTemplate retryTemplate = RetryTemplate.builder() - .maxAttempts(20) - .fixedBackoff(300) - .retryOn(IllegalStateException.class) - .build(); private final UserService userService; @Override public Result reconcile(Request request) { client.fetch(User.class, request.name()).ifPresent(user -> { - if (user.getMetadata().getDeletionTimestamp() != null) { - cleanUpResourcesAndRemoveFinalizer(request.name()); + if (isDeleted(user)) { + deleteUserConnections(request.name()); + removeFinalizers(user.getMetadata(), Set.of(FINALIZER_NAME)); + client.update(user); return; } - - addFinalizerIfNecessary(user); - ensureRoleNamesAnno(request.name()); - updatePermalink(request.name()); - handleAvatar(request.name()); - + addFinalizers(user.getMetadata(), Set.of(FINALIZER_NAME)); + ensureRoleNamesAnno(user); + updatePermalink(user); + handleAvatar(user); checkVerifiedEmail(user); client.update(user); }); @@ -92,147 +89,102 @@ public class UserReconciler implements Reconciler { .orElse(false); } - private void handleAvatar(String name) { - client.fetch(User.class, name).ifPresent(user -> { - Map annotations = MetadataUtil.nullSafeAnnotations(user); + private void handleAvatar(User user) { + var annotations = Optional.ofNullable(user.getMetadata().getAnnotations()) + .orElseGet(HashMap::new); + user.getMetadata().setAnnotations(annotations); - String avatarAttachmentName = annotations.get(User.AVATAR_ATTACHMENT_NAME_ANNO); - String oldAvatarAttachmentName = annotations.get(User.LAST_AVATAR_ATTACHMENT_NAME_ANNO); + var avatarAttachmentName = annotations.get(User.AVATAR_ATTACHMENT_NAME_ANNO); + var oldAvatarAttachmentName = + annotations.get(User.LAST_AVATAR_ATTACHMENT_NAME_ANNO); + // remove old avatar if needed + if (StringUtils.isNotBlank(oldAvatarAttachmentName) + && !StringUtils.equals(avatarAttachmentName, oldAvatarAttachmentName)) { + client.fetch(Attachment.class, oldAvatarAttachmentName) + .ifPresent(client::delete); + annotations.remove(User.LAST_AVATAR_ATTACHMENT_NAME_ANNO); + } - if (StringUtils.isNotBlank(oldAvatarAttachmentName) - && !StringUtils.equals(oldAvatarAttachmentName, avatarAttachmentName)) { - client.fetch(Attachment.class, oldAvatarAttachmentName) - .ifPresent(client::delete); - annotations.remove(User.LAST_AVATAR_ATTACHMENT_NAME_ANNO); - oldAvatarAttachmentName = null; + var spec = user.getSpec(); + if (StringUtils.isBlank(avatarAttachmentName)) { + if (StringUtils.isNotBlank(spec.getAvatar())) { + log.info("Remove avatar for user({})", user.getMetadata().getName()); } - - if (StringUtils.isNotBlank(avatarAttachmentName)) { - client.fetch(Attachment.class, avatarAttachmentName) - .ifPresent(attachment -> { - URI avatarUri = attachmentService.getPermalink(attachment).block(); - if (avatarUri == null) { - log.warn("Failed to get avatar permalink for user [{}] with attachment " - + "[{}], re-enqueuing...", name, avatarAttachmentName); - throw new RequeueException(new Result(true, null), - "Failed to get avatar permalink."); - } - user.getSpec().setAvatar(avatarUri.toString()); - }); - } else if (StringUtils.isNotBlank(oldAvatarAttachmentName)) { - user.getSpec().setAvatar(null); - } - - if (StringUtils.isBlank(oldAvatarAttachmentName) - && StringUtils.isNotBlank(avatarAttachmentName)) { - annotations.put(User.LAST_AVATAR_ATTACHMENT_NAME_ANNO, avatarAttachmentName); - } - - client.update(user); - }); - } - - private void ensureRoleNamesAnno(String name) { - client.fetch(User.class, name).ifPresent(user -> { - Map annotations = MetadataUtil.nullSafeAnnotations(user); - Map oldAnnotations = Map.copyOf(annotations); - - List roleNames = listRoleNamesRef(name); - annotations.put(User.ROLE_NAMES_ANNO, JsonUtils.objectToJson(roleNames)); - - if (!oldAnnotations.equals(annotations)) { - client.update(user); - } - }); - } - - List listRoleNamesRef(String username) { - var subject = new RoleBinding.Subject(KIND, username, GROUP); - return roleService.listRoleRefs(subject) - .filter(this::isRoleRef) - .map(RoleBinding.RoleRef::getName) - .distinct() - .collectList() - .blockOptional() - .orElse(List.of()); - } - - private boolean isRoleRef(RoleBinding.RoleRef roleRef) { - var roleGvk = new Role().groupVersionKind(); - var gk = new GroupKind(roleRef.getApiGroup(), roleRef.getKind()); - return gk.equals(roleGvk.groupKind()); - } - - private void updatePermalink(String name) { - client.fetch(User.class, name).ifPresent(user -> { - if (AnonymousUserConst.isAnonymousUser(name)) { - // anonymous user is not allowed to have permalink - return; - } - if (user.getStatus() == null) { - user.setStatus(new User.UserStatus()); - } - User.UserStatus status = user.getStatus(); - String oldPermalink = status.getPermalink(); - - status.setPermalink(getUserPermalink(user)); - - if (!StringUtils.equals(oldPermalink, status.getPermalink())) { - client.update(user); - } - }); - } - - private String getUserPermalink(User user) { - return externalUrlSupplier.get() - .resolve(PathUtils.combinePath("authors", user.getMetadata().getName())) - .normalize().toString(); - } - - private void addFinalizerIfNecessary(User oldUser) { - Set finalizers = oldUser.getMetadata().getFinalizers(); - if (finalizers != null && finalizers.contains(FINALIZER_NAME)) { + spec.setAvatar(null); return; } - client.fetch(User.class, oldUser.getMetadata().getName()) - .ifPresent(user -> { - Set newFinalizers = user.getMetadata().getFinalizers(); - if (newFinalizers == null) { - newFinalizers = new HashSet<>(); - user.getMetadata().setFinalizers(newFinalizers); + client.fetch(Attachment.class, avatarAttachmentName) + .flatMap(attachment -> attachmentService.getPermalink(attachment) + .blockOptional(Duration.ofMinutes(1)) + ) + .map(URI::toString) + .ifPresentOrElse(avatar -> { + if (!Objects.equals(avatar, spec.getAvatar())) { + log.info( + "Update avatar for user({}) to {}", + user.getMetadata().getName(), avatar + ); } - newFinalizers.add(FINALIZER_NAME); - client.update(user); + spec.setAvatar(avatar); + // reset last avatar + annotations.put( + User.LAST_AVATAR_ATTACHMENT_NAME_ANNO, + avatarAttachmentName + ); + }, () -> { + throw new RequeueException( + new Result(true, null), + "Avatar permalink(%s) is not available yet." + .formatted(avatarAttachmentName) + ); }); } - private void cleanUpResourcesAndRemoveFinalizer(String userName) { - client.fetch(User.class, userName).ifPresent(user -> { - // wait for dependent resources to be deleted - deleteUserConnections(userName); - - // remove finalizer - if (user.getMetadata().getFinalizers() != null) { - user.getMetadata().getFinalizers().remove(FINALIZER_NAME); - } - client.update(user); - }); + private void ensureRoleNamesAnno(User user) { + roleService.getRolesByUsername(user.getMetadata().getName()) + .collectList() + .map(JsonUtils::objectToJson) + .doOnNext(roleNamesJson -> { + var annotations = Optional.ofNullable(user.getMetadata().getAnnotations()) + .orElseGet(HashMap::new); + user.getMetadata().setAnnotations(annotations); + annotations.put(User.ROLE_NAMES_ANNO, roleNamesJson); + }) + .block(Duration.ofMinutes(1)); } - void deleteUserConnections(String userName) { - listConnectionsByUsername(userName).forEach(client::delete); - // wait for user connection to be deleted - retryTemplate.execute(callback -> { - if (listConnectionsByUsername(userName).size() > 0) { - throw new IllegalStateException("User connection is not deleted yet"); - } - return null; - }); + private void updatePermalink(User user) { + var name = user.getMetadata().getName(); + if (AnonymousUserConst.isAnonymousUser(name)) { + // anonymous user is not allowed to have permalink + return; + } + var status = Optional.ofNullable(user.getStatus()) + .orElseGet(User.UserStatus::new); + user.setStatus(status); + status.setPermalink(getUserPermalink(user)); + } + + private String getUserPermalink(User user) { + return UriComponentsBuilder.fromUri(externalUrlSupplier.get()) + .pathSegment("authors", user.getMetadata().getName()) + .toUriString(); + } + + void deleteUserConnections(String username) { + var userConnections = listConnectionsByUsername(username); + if (CollectionUtils.isEmpty(userConnections)) { + return; + } + userConnections.forEach(client::delete); + throw new RequeueException(new Result(true, null), "User connections are not deleted yet"); } List listConnectionsByUsername(String username) { - return client.list(UserConnection.class, - connection -> connection.getSpec().getUsername().equals(username), null); + var listOptions = ListOptions.builder() + .andQuery(equal("spec.username", username)) + .build(); + return client.listAll(UserConnection.class, listOptions, defaultSort()); } @Override diff --git a/application/src/main/java/run/halo/app/core/extension/service/DefaultRoleService.java b/application/src/main/java/run/halo/app/core/extension/service/DefaultRoleService.java index 2feb50f87..6351643c3 100644 --- a/application/src/main/java/run/halo/app/core/extension/service/DefaultRoleService.java +++ b/application/src/main/java/run/halo/app/core/extension/service/DefaultRoleService.java @@ -1,13 +1,17 @@ package run.halo.app.core.extension.service; -import static run.halo.app.extension.Comparators.compareCreationTimestamp; +import static run.halo.app.extension.ExtensionUtil.defaultSort; +import static run.halo.app.extension.ExtensionUtil.notDeleting; import static run.halo.app.security.authorization.AuthorityUtils.containsSuperRole; import com.fasterxml.jackson.core.type.TypeReference; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -22,8 +26,12 @@ import run.halo.app.core.extension.Role; import run.halo.app.core.extension.RoleBinding; import run.halo.app.core.extension.RoleBinding.RoleRef; import run.halo.app.core.extension.RoleBinding.Subject; +import run.halo.app.core.extension.User; +import run.halo.app.extension.ExtensionUtil; +import run.halo.app.extension.ListOptions; import run.halo.app.extension.MetadataUtil; import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.extension.index.query.QueryFactory; import run.halo.app.infra.utils.JsonUtils; import run.halo.app.security.SuperAdminInitializer; @@ -35,18 +43,58 @@ import run.halo.app.security.SuperAdminInitializer; @Service public class DefaultRoleService implements RoleService { - private final ReactiveExtensionClient extensionClient; + private final ReactiveExtensionClient client; - public DefaultRoleService(ReactiveExtensionClient extensionClient) { - this.extensionClient = extensionClient; + public DefaultRoleService(ReactiveExtensionClient client) { + this.client = client; + } + + private Flux listRoleRefs(Subject subject) { + return listRoleBindings(subject).map(RoleBinding::getRoleRef); } @Override - public Flux listRoleRefs(Subject subject) { - return extensionClient.list(RoleBinding.class, - binding -> binding.getSubjects().contains(subject), - null) - .map(RoleBinding::getRoleRef); + public Flux listRoleBindings(Subject subject) { + var listOptions = ListOptions.builder() + .andQuery(notDeleting()) + .andQuery(QueryFactory.in("subjects", subject.toString())) + .build(); + return client.listAll(RoleBinding.class, listOptions, defaultSort()); + } + + @Override + public Flux getRolesByUsername(String username) { + return listRoleRefs(toUserSubject(username)) + .filter(DefaultRoleService::isRoleKind) + .map(RoleRef::getName); + } + + @Override + public Mono>> getRolesByUsernames(Collection usernames) { + if (CollectionUtils.isEmpty(usernames)) { + return Mono.empty(); + } + var subjects = usernames.stream().map(DefaultRoleService::toUserSubject) + .map(Object::toString) + .collect(Collectors.toSet()); + var listOptions = ListOptions.builder() + .andQuery(notDeleting()) + .andQuery(QueryFactory.in("subjects", subjects)) + .build(); + + return client.listAll(RoleBinding.class, listOptions, defaultSort()) + .collect(HashMap::new, (map, roleBinding) -> { + for (Subject subject : roleBinding.getSubjects()) { + if (subjects.contains(subject.toString())) { + var username = subject.getName(); + var roleRef = roleBinding.getRoleRef(); + if (isRoleKind(roleRef)) { + var roleName = roleRef.getName(); + map.computeIfAbsent(username, k -> new HashSet<>()).add(roleName); + } + } + } + }); } @Override @@ -54,7 +102,7 @@ public class DefaultRoleService implements RoleService { if (source.contains(SuperAdminInitializer.SUPER_ROLE_NAME)) { return Mono.just(true); } - return listDependencies(new HashSet<>(source), shouldFilterHidden(false)) + return listWithDependencies(new HashSet<>(source), shouldExcludeHidden(false)) .map(role -> role.getMetadata().getName()) .collect(Collectors.toSet()) .map(roleNames -> roleNames.containsAll(candidates)); @@ -64,55 +112,58 @@ public class DefaultRoleService implements RoleService { public Flux listPermissions(Set names) { if (containsSuperRole(names)) { // search all permissions - return extensionClient.list(Role.class, - shouldFilterHidden(true), - compareCreationTimestamp(true)); + return client.listAll(Role.class, + shouldExcludeHidden(true), + ExtensionUtil.defaultSort()); } - return listDependencies(names, shouldFilterHidden(true)); + return listWithDependencies(names, shouldExcludeHidden(true)); } @Override public Flux listDependenciesFlux(Set names) { - return listDependencies(names, shouldFilterHidden(false)); + return listWithDependencies(names, shouldExcludeHidden(false)); } - private Flux listRoles(Set names, Predicate additionalPredicate) { + private static boolean isRoleKind(RoleRef roleRef) { + return Role.GROUP.equals(roleRef.getApiGroup()) && Role.KIND.equals(roleRef.getKind()); + } + + private static Subject toUserSubject(String username) { + var subject = new Subject(); + subject.setApiGroup(User.GROUP); + subject.setKind(User.KIND); + subject.setName(username); + return subject; + } + + private Flux listRoles(Set names, ListOptions additionalListOptions) { if (CollectionUtils.isEmpty(names)) { return Flux.empty(); } - Predicate predicate = role -> names.contains(role.getMetadata().getName()); - if (additionalPredicate != null) { - predicate = predicate.and(additionalPredicate); - } - return extensionClient.list(Role.class, predicate, compareCreationTimestamp(true)); + var listOptions = Optional.ofNullable(additionalListOptions) + .map(ListOptions::builder) + .orElseGet(ListOptions::builder) + .andQuery(notDeleting()) + .andQuery(QueryFactory.in("metadata.name", names)) + .build(); + + return client.listAll(Role.class, listOptions, ExtensionUtil.defaultSort()); } - private static Predicate shouldFilterHidden(boolean filterHidden) { - if (!filterHidden) { - return r -> true; + private static ListOptions shouldExcludeHidden(boolean excludeHidden) { + if (!excludeHidden) { + return null; } - return role -> { - var labels = role.getMetadata().getLabels(); - if (labels == null) { - return true; - } - var hiddenValue = labels.get(Role.HIDDEN_LABEL_NAME); - return !Boolean.parseBoolean(hiddenValue); - }; + return ListOptions.builder().labelSelector() + .notEq(Role.HIDDEN_LABEL_NAME, Boolean.TRUE.toString()) + .end() + .build(); } - private static boolean isRoleTemplate(Role role) { - var labels = role.getMetadata().getLabels(); - if (labels == null) { - return false; - } - return Boolean.parseBoolean(labels.get(Role.TEMPLATE_LABEL_NAME)); - } - - private Flux listDependencies(Set names, Predicate additionalPredicate) { + private Flux listWithDependencies(Set names, ListOptions additionalListOptions) { var visited = new HashSet(); - return listRoles(names, additionalPredicate) + return listRoles(names, additionalListOptions) .expand(role -> { var name = role.getMetadata().getName(); if (visited.contains(name)) { @@ -128,29 +179,26 @@ public class DefaultRoleService implements RoleService { return Flux.fromIterable(dependencies) .filter(dep -> !visited.contains(dep)) - .collect(Collectors.toSet()) - .flatMapMany(deps -> listRoles(deps, additionalPredicate)); + .collect(Collectors.toSet()) + .flatMapMany(deps -> listRoles(deps, additionalListOptions)); }) - .concatWith(Flux.defer(() -> listAggregatedRoles(visited, additionalPredicate))); + .concatWith(Flux.defer(() -> listAggregatedRoles(visited, additionalListOptions))); } private Flux listAggregatedRoles(Set roleNames, - Predicate additionalPredicate) { - var aggregatedLabelNames = roleNames.stream() - .map(roleName -> Role.ROLE_AGGREGATE_LABEL_PREFIX + roleName) - .collect(Collectors.toSet()); - Predicate predicate = role -> { - var labels = role.getMetadata().getLabels(); - if (labels == null) { - return false; - } - return aggregatedLabelNames.stream() - .anyMatch(aggregatedLabel -> Boolean.parseBoolean(labels.get(aggregatedLabel))); - }; - if (additionalPredicate != null) { - predicate = predicate.and(additionalPredicate); + ListOptions additionalListOptions) { + if (CollectionUtils.isEmpty(roleNames)) { + return Flux.empty(); } - return extensionClient.list(Role.class, predicate, compareCreationTimestamp(true)); + var listOptionsBuilder = Optional.ofNullable(additionalListOptions) + .map(ListOptions::builder) + .orElseGet(ListOptions::builder); + roleNames.stream() + .map(roleName -> Role.ROLE_AGGREGATE_LABEL_PREFIX + roleName) + .forEach( + label -> listOptionsBuilder.labelSelector().eq(label, Boolean.TRUE.toString()) + ); + return client.listAll(Role.class, listOptionsBuilder.build(), ExtensionUtil.defaultSort()); } Predicate getRoleBindingPredicate(Subject targetSubject) { @@ -175,11 +223,21 @@ public class DefaultRoleService implements RoleService { @Override public Flux list(Set roleNames) { + return list(roleNames, false); + } + + @Override + public Flux list(Set roleNames, boolean excludeHidden) { if (CollectionUtils.isEmpty(roleNames)) { return Flux.empty(); } - return Flux.fromIterable(roleNames) - .flatMap(roleName -> extensionClient.fetch(Role.class, roleName)); + var builder = ListOptions.builder() + .andQuery(notDeleting()) + .andQuery(QueryFactory.in("metadata.name", roleNames)); + if (excludeHidden) { + builder.labelSelector().notEq(Role.HIDDEN_LABEL_NAME, Boolean.TRUE.toString()); + } + return client.listAll(Role.class, builder.build(), defaultSort()); } @NonNull diff --git a/application/src/main/java/run/halo/app/core/extension/service/RoleService.java b/application/src/main/java/run/halo/app/core/extension/service/RoleService.java index e52917dcd..d22beff91 100644 --- a/application/src/main/java/run/halo/app/core/extension/service/RoleService.java +++ b/application/src/main/java/run/halo/app/core/extension/service/RoleService.java @@ -1,11 +1,12 @@ package run.halo.app.core.extension.service; import java.util.Collection; +import java.util.Map; import java.util.Set; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import run.halo.app.core.extension.Role; -import run.halo.app.core.extension.RoleBinding.RoleRef; +import run.halo.app.core.extension.RoleBinding; import run.halo.app.core.extension.RoleBinding.Subject; /** @@ -14,7 +15,11 @@ import run.halo.app.core.extension.RoleBinding.Subject; */ public interface RoleService { - Flux listRoleRefs(Subject subject); + Flux listRoleBindings(Subject subject); + + Flux getRolesByUsername(String username); + + Mono>> getRolesByUsernames(Collection usernames); Mono contains(Collection source, Collection candidates); @@ -29,5 +34,20 @@ public interface RoleService { Flux listDependenciesFlux(Set names); + /** + * List roles by role names. + * + * @param roleNames role names + * @return roles + */ Flux list(Set roleNames); + + /** + * List roles by role names. + * + * @param roleNames role names + * @param excludeHidden should exclude hidden roles + * @return roles + */ + Flux list(Set roleNames, boolean excludeHidden); } diff --git a/application/src/main/java/run/halo/app/core/extension/service/UserService.java b/application/src/main/java/run/halo/app/core/extension/service/UserService.java index f7d825000..f252d1536 100644 --- a/application/src/main/java/run/halo/app/core/extension/service/UserService.java +++ b/application/src/main/java/run/halo/app/core/extension/service/UserService.java @@ -3,7 +3,6 @@ package run.halo.app.core.extension.service; import java.util.Set; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import run.halo.app.core.extension.Role; import run.halo.app.core.extension.User; public interface UserService { @@ -16,8 +15,6 @@ public interface UserService { Mono updateWithRawPassword(String username, String rawPassword); - Flux listRoles(String username); - Mono grantRoles(String username, Set roles); Mono signUp(User user, String password); diff --git a/application/src/main/java/run/halo/app/core/extension/service/UserServiceImpl.java b/application/src/main/java/run/halo/app/core/extension/service/UserServiceImpl.java index f81cbbf75..1924b37b2 100644 --- a/application/src/main/java/run/halo/app/core/extension/service/UserServiceImpl.java +++ b/application/src/main/java/run/halo/app/core/extension/service/UserServiceImpl.java @@ -2,15 +2,19 @@ package run.halo.app.core.extension.service; import static org.springframework.data.domain.Sort.Order.asc; import static org.springframework.data.domain.Sort.Order.desc; -import static run.halo.app.core.extension.RoleBinding.containsUser; import static run.halo.app.extension.index.query.QueryFactory.equal; +import java.time.Clock; +import java.time.Duration; +import java.util.HashMap; import java.util.HashSet; import java.util.Objects; +import java.util.Optional; import java.util.Set; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.BooleanUtils; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.domain.Sort; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -21,6 +25,7 @@ import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebInputException; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; import run.halo.app.core.extension.Role; import run.halo.app.core.extension.RoleBinding; import run.halo.app.core.extension.User; @@ -48,11 +53,18 @@ public class UserServiceImpl implements UserService { private final ApplicationEventPublisher eventPublisher; + private final RoleService roleService; + + private Clock clock = Clock.systemUTC(); + + void setClock(Clock clock) { + this.clock = clock; + } + @Override public Mono getUser(String username) { return client.get(User.class, username) - .onErrorMap(ExtensionNotFoundException.class, - e -> new UserNotFoundException(username)); + .onErrorMap(ExtensionNotFoundException.class, e -> new UserNotFoundException(username)); } @Override @@ -91,22 +103,17 @@ public class UserServiceImpl implements UserService { } @Override - public Flux listRoles(String name) { - return client.list(RoleBinding.class, containsUser(name), null) - .filter(roleBinding -> Role.KIND.equals(roleBinding.getRoleRef().getKind())) - .map(roleBinding -> roleBinding.getRoleRef().getName()) - .flatMap(roleName -> client.fetch(Role.class, roleName)); - } - - @Override - @Transactional public Mono grantRoles(String username, Set roles) { return client.get(User.class, username) .flatMap(user -> { var bindingsToUpdate = new HashSet(); var bindingsToDelete = new HashSet(); var existingRoles = new HashSet(); - return client.list(RoleBinding.class, RoleBinding.containsUser(username), null) + var subject = new RoleBinding.Subject(); + subject.setKind(User.KIND); + subject.setApiGroup(User.GROUP); + subject.setName(username); + return roleService.listRoleBindings(subject) .doOnNext(binding -> { var roleName = binding.getRoleRef().getName(); if (roles.contains(roleName)) { @@ -129,7 +136,13 @@ public class UserServiceImpl implements UserService { return mutableRoles.stream() .map(roleName -> RoleBinding.create(username, roleName)); }).flatMap(client::create)) - .then(Mono.just(user)); + .then(Mono.defer(() -> { + var annotations = Optional.ofNullable(user.getMetadata().getAnnotations()) + .orElseGet(HashMap::new); + user.getMetadata().setAnnotations(annotations); + annotations.put(User.REQUEST_TO_UPDATE, clock.instant().toString()); + return client.update(user); + })); }); } @@ -184,7 +197,12 @@ public class UserServiceImpl implements UserService { .then(); }) .then(Mono.defer(() -> client.create(user) - .flatMap(newUser -> grantRoles(user.getMetadata().getName(), roleNames))) + .flatMap(newUser -> grantRoles(user.getMetadata().getName(), roleNames) + .retryWhen( + Retry.backoff(5, Duration.ofMillis(100)) + .filter(OptimisticLockingFailureException.class::isInstance) + ) + )) ); } diff --git a/application/src/main/java/run/halo/app/infra/SchemeInitializer.java b/application/src/main/java/run/halo/app/infra/SchemeInitializer.java index fbe4f77d0..898627bff 100644 --- a/application/src/main/java/run/halo/app/infra/SchemeInitializer.java +++ b/application/src/main/java/run/halo/app/infra/SchemeInitializer.java @@ -7,7 +7,9 @@ import static run.halo.app.extension.index.IndexAttributeFactory.multiValueAttri import static run.halo.app.extension.index.IndexAttributeFactory.simpleAttribute; import com.fasterxml.jackson.core.type.TypeReference; +import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; @@ -31,6 +33,7 @@ import run.halo.app.core.extension.Setting; import run.halo.app.core.extension.Theme; import run.halo.app.core.extension.User; import run.halo.app.core.extension.UserConnection; +import run.halo.app.core.extension.UserConnection.UserConnectionSpec; import run.halo.app.core.extension.attachment.Attachment; import run.halo.app.core.extension.attachment.Group; import run.halo.app.core.extension.attachment.Policy; @@ -51,6 +54,7 @@ import run.halo.app.core.extension.notification.Subscription; import run.halo.app.extension.ConfigMap; import run.halo.app.extension.DefaultSchemeManager; import run.halo.app.extension.DefaultSchemeWatcherManager; +import run.halo.app.extension.MetadataOperator; import run.halo.app.extension.MetadataUtil; import run.halo.app.extension.Secret; import run.halo.app.extension.index.IndexSpec; @@ -89,7 +93,22 @@ public class SchemeInitializer implements ApplicationListener { + is.add(new IndexSpec() + .setName("roleRef.name") + .setIndexFunc(simpleAttribute(RoleBinding.class, + roleBinding -> roleBinding.getRoleRef().getName()) + ) + ); + is.add(new IndexSpec() + .setName("subjects") + .setIndexFunc(multiValueAttribute(RoleBinding.class, + roleBinding -> roleBinding.getSubjects().stream() + .map(RoleBinding.Subject::toString) + .collect(Collectors.toSet())) + ) + ); + }); schemeManager.register(User.class, indexSpecs -> { indexSpecs.add(new IndexSpec() .setName("spec.displayName") @@ -103,16 +122,16 @@ public class SchemeInitializer implements ApplicationListener { - var roleNamesAnno = MetadataUtil.nullSafeAnnotations(user) - .get(User.ROLE_NAMES_ANNO); - if (StringUtils.isBlank(roleNamesAnno)) { - return Set.of(); - } - return JsonUtils.jsonToObject(roleNamesAnno, - new TypeReference<>() { - }); - }))); + .setIndexFunc(multiValueAttribute(User.class, user -> + Optional.ofNullable(user.getMetadata()) + .map(MetadataOperator::getAnnotations) + .map(annotations -> annotations.get(User.ROLE_NAMES_ANNO)) + .filter(StringUtils::isNotBlank) + .map(rolesJson -> JsonUtils.jsonToObject(rolesJson, + new TypeReference>() { + }) + ) + .orElseGet(Set::of)))); }); schemeManager.register(ReverseProxy.class); schemeManager.register(Setting.class); @@ -453,7 +472,15 @@ public class SchemeInitializer implements ApplicationListener { + is.add(new IndexSpec() + .setName("spec.username") + .setIndexFunc(simpleAttribute(UserConnection.class, + connection -> Optional.ofNullable(connection.getSpec()) + .map(UserConnectionSpec::getUsername) + .orElse(null) + ))); + }); // security.halo.run schemeManager.register(PersonalAccessToken.class); diff --git a/application/src/main/java/run/halo/app/security/DefaultUserDetailService.java b/application/src/main/java/run/halo/app/security/DefaultUserDetailService.java index 9d9a63e9b..b1bf1e564 100644 --- a/application/src/main/java/run/halo/app/security/DefaultUserDetailService.java +++ b/application/src/main/java/run/halo/app/security/DefaultUserDetailService.java @@ -1,8 +1,6 @@ package run.halo.app.security; import static java.util.Objects.requireNonNullElse; -import static run.halo.app.core.extension.User.GROUP; -import static run.halo.app.core.extension.User.KIND; import static run.halo.app.security.authorization.AuthorityUtils.ANONYMOUS_ROLE_NAME; import static run.halo.app.security.authorization.AuthorityUtils.AUTHENTICATED_ROLE_NAME; import static run.halo.app.security.authorization.AuthorityUtils.ROLE_PREFIX; @@ -15,12 +13,8 @@ import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import reactor.core.publisher.Mono; -import run.halo.app.core.extension.Role; -import run.halo.app.core.extension.RoleBinding.RoleRef; -import run.halo.app.core.extension.RoleBinding.Subject; import run.halo.app.core.extension.service.RoleService; import run.halo.app.core.extension.service.UserService; -import run.halo.app.extension.GroupKind; import run.halo.app.infra.exception.UserNotFoundException; import run.halo.app.security.authentication.login.HaloUser; import run.halo.app.security.authentication.twofactor.TwoFactorUtils; @@ -56,13 +50,10 @@ public class DefaultUserDetailService e -> new BadCredentialsException("Invalid Credentials")) .flatMap(user -> { var name = user.getMetadata().getName(); - var subject = new Subject(KIND, name, GROUP); var userBuilder = User.withUsername(name) .password(user.getSpec().getPassword()) .disabled(requireNonNullElse(user.getSpec().getDisabled(), false)); - var setAuthorities = roleService.listRoleRefs(subject) - .filter(this::isRoleRef) - .map(RoleRef::getName) + var setAuthorities = roleService.getRolesByUsername(name) // every authenticated user should have authenticated and anonymous roles. .concatWithValues(AUTHENTICATED_ROLE_NAME, ANONYMOUS_ROLE_NAME) .map(roleName -> new SimpleGrantedAuthority(ROLE_PREFIX + roleName)) @@ -82,12 +73,6 @@ public class DefaultUserDetailService }); } - private boolean isRoleRef(RoleRef roleRef) { - var roleGvk = new Role().groupVersionKind(); - var gk = new GroupKind(roleRef.getApiGroup(), roleRef.getKind()); - return gk.equals(roleGvk.groupKind()); - } - private UserDetails withNewPassword(UserDetails userDetails, String newPassword) { return User.withUserDetails(userDetails) .password(newPassword) diff --git a/application/src/main/java/run/halo/app/security/authorization/AuthorityUtils.java b/application/src/main/java/run/halo/app/security/authorization/AuthorityUtils.java index b4424be33..64460ea9c 100644 --- a/application/src/main/java/run/halo/app/security/authorization/AuthorityUtils.java +++ b/application/src/main/java/run/halo/app/security/authorization/AuthorityUtils.java @@ -39,8 +39,11 @@ public enum AuthorityUtils { Collection authorities) { return authorities.stream() .map(GrantedAuthority::getAuthority) - .filter(authority -> StringUtils.startsWith(authority, ROLE_PREFIX)) - .map(authority -> StringUtils.removeStart(authority, ROLE_PREFIX)) + .map(authority -> { + authority = StringUtils.removeStart(authority, SCOPE_PREFIX); + authority = StringUtils.removeStart(authority, ROLE_PREFIX); + return authority; + }) .collect(Collectors.toSet()); } diff --git a/application/src/main/java/run/halo/app/security/authorization/DefaultRuleResolver.java b/application/src/main/java/run/halo/app/security/authorization/DefaultRuleResolver.java index 0ff51ae86..00cfdc14a 100644 --- a/application/src/main/java/run/halo/app/security/authorization/DefaultRuleResolver.java +++ b/application/src/main/java/run/halo/app/security/authorization/DefaultRuleResolver.java @@ -1,14 +1,10 @@ package run.halo.app.security.authorization; -import java.util.Collection; -import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; import org.springframework.util.CollectionUtils; import reactor.core.publisher.Mono; import run.halo.app.core.extension.service.RoleService; @@ -30,7 +26,7 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver { @Override public Mono visitRules(Authentication authentication, RequestInfo requestInfo) { - var roleNames = listBoundRoleNames(authentication.getAuthorities()); + var roleNames = AuthorityUtils.authoritiesToRoles(authentication.getAuthorities()); var record = new AttributesRecord(authentication, requestInfo); var visitor = new AuthorizingVisitor(record); @@ -75,15 +71,4 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver { return String.format("Binding role [%s] to [%s]", roleName, subject); } - private static Set listBoundRoleNames( - Collection authorities) { - return authorities.stream() - .map(GrantedAuthority::getAuthority) - .map(authority -> { - authority = StringUtils.removeStart(authority, AuthorityUtils.SCOPE_PREFIX); - authority = StringUtils.removeStart(authority, AuthorityUtils.ROLE_PREFIX); - return authority; - }) - .collect(Collectors.toSet()); - } } diff --git a/application/src/test/java/run/halo/app/core/extension/RoleBindingTest.java b/application/src/test/java/run/halo/app/core/extension/RoleBindingTest.java index 1eca22462..cacf7ba2e 100644 --- a/application/src/test/java/run/halo/app/core/extension/RoleBindingTest.java +++ b/application/src/test/java/run/halo/app/core/extension/RoleBindingTest.java @@ -1,5 +1,6 @@ package run.halo.app.core.extension; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -40,4 +41,20 @@ class RoleBindingTest { assertFalse(RoleBinding.containsUser("non-exist-fake-name").test(binding)); } + @Test + void subjectToStringTest() { + assertEquals("User/fake-name", createSubject("fake-name", "", "User").toString()); + assertEquals( + "fake.group/User/fake-name", + createSubject("fake-name", "fake.group", "User").toString() + ); + } + + RoleBinding.Subject createSubject(String name, String apiGroup, String kind) { + var subject = new RoleBinding.Subject(); + subject.setName(name); + subject.setApiGroup(apiGroup); + subject.setKind(kind); + return subject; + } } \ No newline at end of file diff --git a/application/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointIntegrationTest.java b/application/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointIntegrationTest.java index 253580090..b83f2f6db 100644 --- a/application/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointIntegrationTest.java +++ b/application/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointIntegrationTest.java @@ -6,6 +6,8 @@ import static org.springframework.security.test.web.reactive.server.SecurityMock import java.time.Instant; import java.util.List; +import java.util.Map; +import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -17,6 +19,7 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.web.reactive.server.WebTestClient; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import run.halo.app.core.extension.Role; import run.halo.app.core.extension.User; import run.halo.app.core.extension.service.RoleService; @@ -46,7 +49,7 @@ public class UserEndpointIntegrationTest { .build(); var role = new Role(); role.setMetadata(new Metadata()); - role.getMetadata().setName("super-role"); + role.getMetadata().setName("fake-super-role"); role.setRules(List.of(rule)); when(roleService.listDependenciesFlux(anySet())).thenReturn(Flux.just(role)); webClient = webClient.mutateWith(csrf()); @@ -68,6 +71,9 @@ public class UserEndpointIntegrationTest { client.create(unexpectedUser2).block(); when(roleService.list(anySet())).thenReturn(Flux.empty()); + when(roleService.getRolesByUsernames( + List.of("fake-user-2") + )).thenReturn(Mono.just(Map.of("fake-user-2", Set.of("fake-super-role")))); webClient.get().uri("/apis/api.console.halo.run/v1alpha1/users?keyword=Expected") .exchange() @@ -92,6 +98,8 @@ public class UserEndpointIntegrationTest { client.create(unexpectedUser2).block(); when(roleService.list(anySet())).thenReturn(Flux.empty()); + when(roleService.getRolesByUsernames(List.of("fake-user"))) + .thenReturn(Mono.just(Map.of("fake-user", Set.of("fake-super-role")))); webClient.get().uri("/apis/api.console.halo.run/v1alpha1/users?keyword=fake-user") .exchange() diff --git a/application/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointTest.java b/application/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointTest.java index 0b05a51a1..9396cfe66 100644 --- a/application/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointTest.java +++ b/application/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointTest.java @@ -88,6 +88,8 @@ class UserEndpointTest { @Test void shouldListEmptyUsersWhenNoUsers() { + when(roleService.getRolesByUsernames(any())).thenReturn(Mono.just(Map.of())); + when(roleService.list(any())).thenReturn(Flux.empty()); when(client.listBy(same(User.class), any(), any(PageRequest.class))) .thenReturn(Mono.just(ListResult.emptyResult())); @@ -109,6 +111,7 @@ class UserEndpointTest { createUser("fake-user-3") ); var expectResult = new ListResult<>(users); + when(roleService.getRolesByUsernames(any())).thenReturn(Mono.just(Map.of())); when(roleService.list(anySet())).thenReturn(Flux.empty()); when(client.listBy(same(User.class), any(), any(PageRequest.class))) .thenReturn(Mono.just(expectResult)); @@ -144,6 +147,7 @@ class UserEndpointTest { var expectResult = new ListResult<>(users); when(client.listBy(same(User.class), any(), any(PageRequest.class))) .thenReturn(Mono.just(expectResult)); + when(roleService.getRolesByUsernames(any())).thenReturn(Mono.just(Map.of())); when(roleService.list(anySet())).thenReturn(Flux.empty()); bindToRouterFunction(endpoint.endpoint()) @@ -160,6 +164,7 @@ class UserEndpointTest { var expectResult = new ListResult<>(List.of(expectUser)); when(client.listBy(same(User.class), any(), any(PageRequest.class))) .thenReturn(Mono.just(expectResult)); + when(roleService.getRolesByUsernames(any())).thenReturn(Mono.just(Map.of())); when(roleService.list(anySet())).thenReturn(Flux.empty()); bindToRouterFunction(endpoint.endpoint()) @@ -219,22 +224,19 @@ class UserEndpointTest { metadata.setName("fake-user"); var user = new User(); user.setMetadata(metadata); - Map annotations = - Map.of(User.ROLE_NAMES_ANNO, JsonUtils.objectToJson(Set.of("role-A"))); - user.getMetadata().setAnnotations(annotations); when(userService.getUser("fake-user")).thenReturn(Mono.just(user)); Role role = new Role(); role.setMetadata(new Metadata()); - role.getMetadata().setName("role-A"); + role.getMetadata().setName("fake-super-role"); role.setRules(List.of()); - when(roleService.list(anySet())).thenReturn(Flux.just(role)); + when(roleService.list(Set.of("fake-super-role"), true)).thenReturn(Flux.just(role)); webClient.get().uri("/users/-") .exchange() .expectStatus().isOk() .expectHeader().contentType(MediaType.APPLICATION_JSON) .expectBody(UserEndpoint.DetailedUser.class) .isEqualTo(new UserEndpoint.DetailedUser(user, List.of(role))); - verify(roleService).list(eq(Set.of("role-A"))); + // verify(roleService).list(eq(Set.of("role-A"))); } } @@ -385,16 +387,17 @@ class UserEndpointTest { "metadata": { "name": "test-A", "annotations": { - "rbac.authorization.halo.run/ui-permissions": "[\\"permission-A\\"]" + "rbac.authorization.halo.run/ui-permissions": \ + "[\\"permission-A\\", \\"permission-A\\"]" } }, "rules": [] } """, Role.class); when(roleService.listPermissions(eq(Set.of("test-A")))).thenReturn(Flux.just(roleA)); - when(userService.listRoles(eq("fake-user"))).thenReturn( - Flux.fromIterable(List.of(roleA))); when(roleService.listDependenciesFlux(anySet())).thenReturn(Flux.just(roleA)); + when(roleService.getRolesByUsername("fake-user")).thenReturn(Flux.just("test-A")); + when(roleService.list(Set.of("test-A"), true)).thenReturn(Flux.just(roleA)); webClient.get().uri("/users/fake-user/permissions") .exchange() @@ -402,12 +405,10 @@ class UserEndpointTest { .isOk() .expectBody(UserEndpoint.UserPermission.class) .value(userPermission -> { - assertEquals(Set.of(roleA), userPermission.getRoles()); + assertEquals(List.of(roleA), userPermission.getRoles()); assertEquals(List.of(roleA), userPermission.getPermissions()); - assertEquals(Set.of("permission-A"), userPermission.getUiPermissions()); + assertEquals(List.of("permission-A"), userPermission.getUiPermissions()); }); - - verify(userService, times(1)).listRoles(eq("fake-user")); } } diff --git a/application/src/test/java/run/halo/app/core/extension/reconciler/RoleBindingReconcilerTest.java b/application/src/test/java/run/halo/app/core/extension/reconciler/RoleBindingReconcilerTest.java deleted file mode 100644 index 660850947..000000000 --- a/application/src/test/java/run/halo/app/core/extension/reconciler/RoleBindingReconcilerTest.java +++ /dev/null @@ -1,195 +0,0 @@ -package run.halo.app.core.extension.reconciler; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isA; -import static org.mockito.ArgumentMatchers.same; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static run.halo.app.core.extension.User.ROLE_NAMES_ANNO; - -import java.time.Instant; -import java.util.List; -import java.util.Optional; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import run.halo.app.core.extension.RoleBinding; -import run.halo.app.core.extension.RoleBinding.RoleRef; -import run.halo.app.core.extension.RoleBinding.Subject; -import run.halo.app.core.extension.User; -import run.halo.app.extension.ExtensionClient; -import run.halo.app.extension.Metadata; -import run.halo.app.extension.controller.Reconciler; -import run.halo.app.extension.controller.Reconciler.Result; - -@ExtendWith(MockitoExtension.class) -class RoleBindingReconcilerTest { - - @Mock - ExtensionClient client; - - @InjectMocks - RoleBindingReconciler reconciler; - - final Result doNotReEnQueue = new Result(false, null); - - @Test - void shouldDoNothingIfRequestNotFound() { - var bindingName = "fake-binding-name"; - when(client.fetch(RoleBinding.class, bindingName)) - .thenReturn(Optional.empty()); - - var result = reconciler.reconcile(new Reconciler.Request(bindingName)); - assertEquals(doNotReEnQueue, result); - - verify(client, times(1)).fetch(same(RoleBinding.class), anyString()); - verify(client, never()).list(same(RoleBinding.class), any(), any()); - verify(client, never()).fetch(same(User.class), anyString()); - verify(client, never()).update(isA(User.class)); - } - - @Test - void shouldDoNothingIfNotContainAnyUserSubject() { - var bindingName = "fake-binding-name"; - var binding = mock(RoleBinding.class); - - when(client.fetch(RoleBinding.class, bindingName)) - .thenReturn(Optional.of(binding)); - when(binding.getSubjects()).thenReturn(List.of()); - - var result = reconciler.reconcile(new Reconciler.Request(bindingName)); - assertEquals(doNotReEnQueue, result); - - verify(client, times(1)).fetch(same(RoleBinding.class), anyString()); - verify(client, never()).list(same(RoleBinding.class), any(), any()); - verify(client, never()).fetch(same(User.class), anyString()); - verify(client, never()).update(isA(User.class)); - } - - @Test - void shouldDoNothingIfUserNotFound() { - var bindingName = "fake-binding-name"; - var userName = "fake-user-name"; - var binding = mock(RoleBinding.class); - var subject = mock(Subject.class); - - when(client.fetch(RoleBinding.class, bindingName)) - .thenReturn(Optional.of(binding)); - when(binding.getSubjects()).thenReturn(List.of(subject)); - when(subject.getKind()).thenReturn("User"); - when(subject.getName()).thenReturn(userName); - when(client.fetch(User.class, userName)).thenReturn(Optional.empty()); - - var result = reconciler.reconcile(new Reconciler.Request(bindingName)); - assertEquals(doNotReEnQueue, result); - - verify(client, times(1)).fetch(same(RoleBinding.class), anyString()); - verify(client, times(1)).list(same(RoleBinding.class), any(), any()); - verify(client, times(1)).fetch(same(User.class), eq(userName)); - - verify(client, never()).update(isA(User.class)); - } - - @Test - void shouldUpdateRoleNamesIfNoBindingRelatedTheUser() { - var bindingName = "fake-binding-name"; - var userName = "fake-user-name"; - var subject = mock(Subject.class); - var binding = createRoleBinding("Role", "fake-role", false, subject); - var user = mock(User.class); - var userMetadata = mock(Metadata.class); - - when(client.fetch(RoleBinding.class, bindingName)) - .thenReturn(Optional.of(binding)); - when(binding.getSubjects()).thenReturn(List.of(subject)); - when(subject.getKind()).thenReturn("User"); - when(subject.getName()).thenReturn(userName); - when(client.fetch(User.class, userName)).thenReturn(Optional.of(user)); - when(user.getMetadata()).thenReturn(userMetadata); - when(client.list(same(RoleBinding.class), any(), any())).thenReturn(List.of()); - - var result = reconciler.reconcile(new Reconciler.Request(bindingName)); - assertEquals(doNotReEnQueue, result); - - verify(client, times(1)).fetch(same(RoleBinding.class), anyString()); - verify(client, times(1)).list(same(RoleBinding.class), any(), any()); - verify(client, times(1)).fetch(same(User.class), anyString()); - verify(client, times(1)).update(isA(User.class)); - - verify(userMetadata).setAnnotations(argThat(annotation -> { - String roleNames = annotation.get(ROLE_NAMES_ANNO); - return roleNames != null && roleNames.equals("[]"); - })); - } - - @Test - void shouldUpdateRoleNames() { - var bindingName = "fake-binding-name"; - var userName = "fake-user-name"; - var subject = mock(Subject.class); - when(subject.getKind()).thenReturn("User"); - when(subject.getName()).thenReturn(userName); - when(subject.getApiGroup()).thenReturn(""); - - var user = mock(User.class); - var userMetadata = mock(Metadata.class); - var binding = createRoleBinding("Role", "fake-role", false, subject); - - when(client.fetch(RoleBinding.class, bindingName)) - .thenReturn(Optional.of(binding)); - when(binding.getSubjects()).thenReturn(List.of(subject)); - - when(client.fetch(User.class, userName)).thenReturn(Optional.of(user)); - when(user.getMetadata()).thenReturn(userMetadata); - var bindings = List.of( - createRoleBinding("Role", "fake-role-01", false, subject), - createRoleBinding("Role", "fake-role-03", false, subject), - createRoleBinding("Role", "fake-role-02", false, subject), - createRoleBinding("NotRole", "fake-role-04", false, subject), - createRoleBinding("Role", "fake-role-05", true, subject) - ); - when(client.list(same(RoleBinding.class), any(), any())).thenReturn(bindings); - - var result = reconciler.reconcile(new Reconciler.Request(bindingName)); - assertEquals(doNotReEnQueue, result); - - verify(client, times(1)).fetch(same(RoleBinding.class), anyString()); - verify(client, times(1)).list(same(RoleBinding.class), any(), any()); - verify(client, times(1)).fetch(same(User.class), anyString()); - verify(client, times(1)).update(isA(User.class)); - - verify(userMetadata).setAnnotations(argThat(annotation -> { - var roleNames = annotation.get(ROLE_NAMES_ANNO); - return roleNames != null && roleNames.equals(""" - ["fake-role-01","fake-role-02","fake-role-03"]"""); - })); - } - - RoleBinding createRoleBinding(String roleRefKind, - String roleRefName, - boolean deleting, - Subject subject) { - var binding = mock(RoleBinding.class); - var roleRef = mock(RoleRef.class); - var metadata = mock(Metadata.class); - lenient().when(roleRef.getKind()).thenReturn(roleRefKind); - lenient().when(roleRef.getName()).thenReturn(roleRefName); - lenient().when(roleRef.getApiGroup()).thenReturn(""); - lenient().when(metadata.getDeletionTimestamp()).thenReturn(deleting ? Instant.now() : null); - lenient().when(binding.getRoleRef()).thenReturn(roleRef); - lenient().when(binding.getMetadata()).thenReturn(metadata); - lenient().when(binding.getSubjects()).thenReturn(List.of(subject)); - return binding; - } - -} diff --git a/application/src/test/java/run/halo/app/core/extension/reconciler/UserReconcilerTest.java b/application/src/test/java/run/halo/app/core/extension/reconciler/UserReconcilerTest.java index 4782dced7..f54997d69 100644 --- a/application/src/test/java/run/halo/app/core/extension/reconciler/UserReconcilerTest.java +++ b/application/src/test/java/run/halo/app/core/extension/reconciler/UserReconcilerTest.java @@ -1,14 +1,12 @@ package run.halo.app.core.extension.reconciler; -import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.assertArg; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static run.halo.app.core.extension.User.GROUP; -import static run.halo.app.core.extension.User.KIND; import java.net.URI; import java.net.URISyntaxException; @@ -17,14 +15,11 @@ import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import run.halo.app.core.extension.Role; -import run.halo.app.core.extension.RoleBinding; import run.halo.app.core.extension.User; import run.halo.app.core.extension.service.RoleService; import run.halo.app.extension.ExtensionClient; @@ -60,58 +55,51 @@ class UserReconcilerTest { @BeforeEach void setUp() { lenient().when(notificationCenter.unsubscribe(any(), any())).thenReturn(Mono.empty()); - lenient().when(roleService.listRoleRefs(any())).thenReturn(Flux.empty()); } @Test void permalinkForFakeUser() throws URISyntaxException { when(externalUrlSupplier.get()).thenReturn(new URI("http://localhost:8090")); + when(roleService.getRolesByUsername("fake-user")) + .thenReturn(Flux.empty()); + when(client.fetch(eq(User.class), eq("fake-user"))) .thenReturn(Optional.of(user("fake-user"))); userReconciler.reconcile(new Reconciler.Request("fake-user")); - verify(client, times(4)).update(any(User.class)); - ArgumentCaptor captor = ArgumentCaptor.forClass(User.class); - verify(client, times(4)).update(captor.capture()); - assertThat(captor.getValue().getStatus().getPermalink()) - .isEqualTo("http://localhost:8090/authors/fake-user"); + verify(client).update(assertArg(user -> + assertEquals( + "http://localhost:8090/authors/fake-user", + user.getStatus().getPermalink() + ) + )); } @Test void permalinkForAnonymousUser() { when(client.fetch(eq(User.class), eq(AnonymousUserConst.PRINCIPAL))) .thenReturn(Optional.of(user(AnonymousUserConst.PRINCIPAL))); + when(roleService.getRolesByUsername(AnonymousUserConst.PRINCIPAL)).thenReturn(Flux.empty()); userReconciler.reconcile(new Reconciler.Request(AnonymousUserConst.PRINCIPAL)); - verify(client, times(3)).update(any(User.class)); + verify(client).update(any(User.class)); } @Test void ensureRoleNamesAnno() { - RoleBinding.RoleRef roleRef = new RoleBinding.RoleRef(); - roleRef.setName("fake-role"); - roleRef.setKind(Role.KIND); - - roleRef.setApiGroup(Role.GROUP); - RoleBinding.RoleRef notworkRef = new RoleBinding.RoleRef(); - notworkRef.setName("super-role"); - notworkRef.setKind("Fake"); - notworkRef.setApiGroup("fake.halo.run"); - - RoleBinding.Subject subject = new RoleBinding.Subject(KIND, "fake-user", GROUP); - when(roleService.listRoleRefs(eq(subject))).thenReturn(Flux.just(roleRef, notworkRef)); - + when(roleService.getRolesByUsername("fake-user")).thenReturn(Flux.just("fake-role")); when(client.fetch(eq(User.class), eq("fake-user"))) .thenReturn(Optional.of(user("fake-user"))); - when(externalUrlSupplier.get()).thenReturn(URI.create("/")); userReconciler.reconcile(new Reconciler.Request("fake-user")); - ArgumentCaptor captor = ArgumentCaptor.forClass(User.class); - verify(client, times(4)).update(captor.capture()); - User user = captor.getAllValues().get(1); - assertThat(user.getMetadata().getAnnotations().get(User.ROLE_NAMES_ANNO)) - .isEqualTo("[\"fake-role\"]"); + + verify(client).update(assertArg(user -> { + assertEquals(""" + ["fake-role"]\ + """, + user.getMetadata().getAnnotations().get(User.ROLE_NAMES_ANNO)); + })); } User user(String name) { diff --git a/application/src/test/java/run/halo/app/core/extension/service/DefaultRoleServiceTest.java b/application/src/test/java/run/halo/app/core/extension/service/DefaultRoleServiceTest.java index 33d099785..b2ca0f179 100644 --- a/application/src/test/java/run/halo/app/core/extension/service/DefaultRoleServiceTest.java +++ b/application/src/test/java/run/halo/app/core/extension/service/DefaultRoleServiceTest.java @@ -1,7 +1,6 @@ package run.halo.app.core.extension.service; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; @@ -22,10 +21,12 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Sort; import reactor.core.publisher.Flux; import reactor.test.StepVerifier; import run.halo.app.core.extension.Role; import run.halo.app.core.extension.RoleBinding; +import run.halo.app.extension.ListOptions; import run.halo.app.extension.Metadata; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.infra.utils.JsonUtils; @@ -39,7 +40,7 @@ import run.halo.app.infra.utils.JsonUtils; @ExtendWith(MockitoExtension.class) class DefaultRoleServiceTest { @Mock - private ReactiveExtensionClient extensionClient; + private ReactiveExtensionClient client; @InjectMocks private DefaultRoleService roleService; @@ -56,7 +57,7 @@ class DefaultRoleServiceTest { var roleNames = Set.of("role1"); - when(extensionClient.list(same(Role.class), any(), any())) + when(client.listAll(same(Role.class), any(ListOptions.class), any(Sort.class))) .thenReturn(Flux.just(role1)) .thenReturn(Flux.just(role2)) .thenReturn(Flux.just(role3)) @@ -73,7 +74,11 @@ class DefaultRoleServiceTest { .verifyComplete(); // verify the mock invocations - verify(extensionClient, times(4)).list(same(Role.class), any(), any()); + verify(client, times(4)).listAll( + same(Role.class), + any(ListOptions.class), + any(Sort.class) + ); } @Test @@ -86,7 +91,7 @@ class DefaultRoleServiceTest { var roleNames = Set.of("role1"); // setup mocks - when(extensionClient.list(same(Role.class), any(), any())) + when(client.listAll(same(Role.class), any(ListOptions.class), any(Sort.class))) .thenReturn(Flux.just(role1)) .thenReturn(Flux.just(role2)) .thenReturn(Flux.just(role3)) @@ -103,7 +108,11 @@ class DefaultRoleServiceTest { .verifyComplete(); // verify the mock invocations - verify(extensionClient, times(4)).list(same(Role.class), any(), any()); + verify(client, times(4)).listAll( + same(Role.class), + any(ListOptions.class), + any(Sort.class) + ); } @Test @@ -118,13 +127,12 @@ class DefaultRoleServiceTest { var roleNames = Set.of("role1"); - when(extensionClient.list(same(Role.class), any(), any())) + when(client.listAll(same(Role.class), any(ListOptions.class), any(Sort.class))) .thenReturn(Flux.just(role1)) .thenReturn(Flux.just(role2)) .thenReturn(Flux.just(role3)) .thenReturn(Flux.just(role4)) - .thenReturn(Flux.empty()) - ; + .thenReturn(Flux.empty()); // call the method under test var result = roleService.listDependenciesFlux(roleNames); @@ -138,7 +146,11 @@ class DefaultRoleServiceTest { .verifyComplete(); // verify the mock invocations - verify(extensionClient, times(5)).list(same(Role.class), any(), any()); + verify(client, times(5)).listAll( + same(Role.class), + any(ListOptions.class), + any(Sort.class) + ); } @Test @@ -153,7 +165,7 @@ class DefaultRoleServiceTest { Set roleNames = Set.of("role1"); - when(extensionClient.list(same(Role.class), any(), any())) + when(client.listAll(same(Role.class), any(ListOptions.class), any(Sort.class))) .thenReturn(Flux.just(role1)) .thenReturn(Flux.just(role4, role2)) .thenReturn(Flux.just(role3)) @@ -171,7 +183,7 @@ class DefaultRoleServiceTest { .verifyComplete(); // verify the mock invocations - verify(extensionClient, times(4)).list(same(Role.class), any(), any()); + verify(client, times(4)).listAll(same(Role.class), any(), any()); } @Test @@ -186,7 +198,7 @@ class DefaultRoleServiceTest { Set roleNames = Set.of("role2"); - when(extensionClient.list(same(Role.class), any(), any())) + when(client.listAll(same(Role.class), any(ListOptions.class), any(Sort.class))) .thenReturn(Flux.just(role2)) .thenReturn(Flux.just(role3)) .thenReturn(Flux.empty()); @@ -201,17 +213,17 @@ class DefaultRoleServiceTest { .verifyComplete(); // verify the mock invocations - verify(extensionClient, times(3)).list(same(Role.class), any(), any()); + verify(client, times(3)).listAll( + same(Role.class), + any(ListOptions.class), + any(Sort.class) + ); } @Test void listDependenciesWithNullParam() { var result = roleService.listDependenciesFlux(null); - when(extensionClient.list(same(Role.class), any(), any())) - .thenReturn(Flux.empty()) - .thenReturn(Flux.empty()); - // verify the result StepVerifier.create(result) .verifyComplete(); @@ -221,7 +233,11 @@ class DefaultRoleServiceTest { .verifyComplete(); // verify the mock invocations - verify(extensionClient, never()).fetch(eq(Role.class), anyString()); + verify(client, never()).listAll( + eq(Role.class), + any(ListOptions.class), + any(Sort.class) + ); } @Test @@ -232,7 +248,7 @@ class DefaultRoleServiceTest { var roleNames = Set.of("role1"); - when(extensionClient.list(same(Role.class), any(), any())) + when(client.listAll(same(Role.class), any(ListOptions.class), any(Sort.class))) .thenReturn(Flux.just(role1)) .thenReturn(Flux.just(role2)) .thenReturn(Flux.just(role4)) @@ -248,7 +264,11 @@ class DefaultRoleServiceTest { .verifyComplete(); // verify the mock invocations - verify(extensionClient, times(4)).list(same(Role.class), any(), any()); + verify(client, times(4)).listAll( + same(Role.class), + any(ListOptions.class), + any(Sort.class) + ); } @Test diff --git a/application/src/test/java/run/halo/app/core/extension/service/UserServiceImplTest.java b/application/src/test/java/run/halo/app/core/extension/service/UserServiceImplTest.java index d1de208a4..8b63e0435 100644 --- a/application/src/test/java/run/halo/app/core/extension/service/UserServiceImplTest.java +++ b/application/src/test/java/run/halo/app/core/extension/service/UserServiceImplTest.java @@ -8,9 +8,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; -import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -19,7 +17,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static run.halo.app.extension.GroupVersionKind.fromExtension; -import java.util.List; import java.util.Set; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -35,6 +32,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import run.halo.app.core.extension.Role; import run.halo.app.core.extension.RoleBinding; +import run.halo.app.core.extension.RoleBinding.Subject; import run.halo.app.core.extension.User; import run.halo.app.event.user.PasswordChangedEvent; import run.halo.app.extension.Metadata; @@ -45,7 +43,6 @@ import run.halo.app.infra.SystemSetting; import run.halo.app.infra.exception.AccessDeniedException; import run.halo.app.infra.exception.DuplicateNameException; import run.halo.app.infra.exception.UserNotFoundException; -import run.halo.app.infra.utils.JsonUtils; @ExtendWith(MockitoExtension.class) class UserServiceImplTest { @@ -62,6 +59,9 @@ class UserServiceImplTest { @Mock ApplicationEventPublisher eventPublisher; + @Mock + RoleService roleService; + @InjectMocks UserServiceImpl userService; @@ -108,119 +108,6 @@ class UserServiceImplTest { verify(eventPublisher).publishEvent(any(PasswordChangedEvent.class)); } - @Test - void shouldListRolesIfUserFoundInExtension() { - User fakeUser = new User(); - Metadata metadata = new Metadata(); - metadata.setName("faker"); - fakeUser.setMetadata(metadata); - fakeUser.setSpec(new User.UserSpec()); - - when(client.list(eq(RoleBinding.class), any(), any())).thenReturn( - Flux.fromIterable(getRoleBindings())); - Role roleA = new Role(); - Metadata metadataA = new Metadata(); - metadataA.setName("test-A"); - roleA.setMetadata(metadataA); - - Role roleB = new Role(); - Metadata metadataB = new Metadata(); - metadataB.setName("test-B"); - roleB.setMetadata(metadataB); - - Role roleC = new Role(); - Metadata metadataC = new Metadata(); - metadataC.setName("ddd"); - roleC.setMetadata(metadataC); - - when(client.fetch(eq(Role.class), eq("test-A"))).thenReturn(Mono.just(roleA)); - when(client.fetch(eq(Role.class), eq("test-B"))).thenReturn(Mono.just(roleB)); - lenient().when(client.fetch(eq(Role.class), eq("ddd"))).thenReturn(Mono.just(roleC)); - - StepVerifier.create(userService.listRoles("faker")) - .expectNext(roleA) - .expectNext(roleB) - .verifyComplete(); - - verify(client, times(1)).list(eq(RoleBinding.class), any(), any()); - - verify(client, times(1)).fetch(eq(Role.class), eq("test-A")); - verify(client, times(1)).fetch(eq(Role.class), eq("test-B")); - verify(client, times(0)).fetch(eq(Role.class), eq("ddd")); - } - - List getRoleBindings() { - String bindA = """ - { - - "apiVersion": "v1alpha1", - "kind": "RoleBinding", - "metadata": { - "name": "bind-A" - }, - "subjects": [{ - "kind": "User", - "name": "faker", - "apiGroup": "" - }], - "roleRef": { - "kind": "Role", - "name": "test-A", - "apiGroup": "" - } - } - """; - - String bindB = """ - { - - "apiVersion": "v1alpha1", - "kind": "RoleBinding", - "metadata": { - "name": "bind-B" - }, - "subjects": [{ - "kind": "User", - "name": "faker", - "apiGroup": "" - }, - { - "kind": "User", - "name": "zhangsan", - "apiGroup": "" - }], - "roleRef": { - "kind": "Role", - "name": "test-B", - "apiGroup": "" - } - } - """; - - String bindC = """ - { - "apiVersion": "v1alpha1", - "kind": "RoleBinding", - "metadata": { - "name": "bind-C" - }, - "subjects": [{ - "kind": "User", - "name": "faker", - "apiGroup": "" - }], - "roleRef": { - "kind": "Fake", - "name": "ddd", - "apiGroup": "" - } - } - """; - return List.of(JsonUtils.jsonToObject(bindA, RoleBinding.class), - JsonUtils.jsonToObject(bindB, RoleBinding.class), - JsonUtils.jsonToObject(bindC, RoleBinding.class)); - } - @Nested @DisplayName("UpdateWithRawPassword") class UpdateWithRawPasswordTest { @@ -312,6 +199,9 @@ class UserServiceImplTest { User createUser(String password) { var user = new User(); + Metadata metadata = new Metadata(); + metadata.setName("fake-user"); + user.setMetadata(metadata); user.setSpec(new User.UserSpec()); user.getSpec().setPassword(password); return user; @@ -336,46 +226,47 @@ class UserServiceImplTest { @Test void shouldCreateRoleBindingIfNotExist() { + var user = createUser("fake-password"); when(client.get(User.class, "fake-user")) - .thenReturn(Mono.just(createUser("fake-password"))); - when(client.list(same(RoleBinding.class), any(), any())).thenReturn(Flux.empty()); + .thenReturn(Mono.just(user)); + when(roleService.listRoleBindings(any(Subject.class))).thenReturn(Flux.empty()); when(client.create(isA(RoleBinding.class))).thenReturn( Mono.just(mock(RoleBinding.class))); + when(client.update(user)).thenReturn(Mono.just(user)); var grantRolesMono = userService.grantRoles("fake-user", Set.of("fake-role")); StepVerifier.create(grantRolesMono) .expectNextCount(1) .verifyComplete(); - verify(client).get(User.class, "fake-user"); - verify(client).list(same(RoleBinding.class), any(), any()); verify(client).create(isA(RoleBinding.class)); } @Test void shouldDeleteRoleBindingIfNotProvided() { - when(client.get(User.class, "fake-user")).thenReturn(Mono.just(mock(User.class))); + var user = createUser("fake-password"); + when(client.get(User.class, "fake-user")).thenReturn(Mono.just(user)); var notProvidedRoleBinding = RoleBinding.create("fake-user", "non-provided-fake-role"); var existingRoleBinding = RoleBinding.create("fake-user", "fake-role"); - when(client.list(same(RoleBinding.class), any(), any())).thenReturn( - Flux.fromIterable(List.of(notProvidedRoleBinding, existingRoleBinding))); + when(roleService.listRoleBindings(any(Subject.class))) + .thenReturn(Flux.just(notProvidedRoleBinding, existingRoleBinding)); when(client.delete(isA(RoleBinding.class))) .thenReturn(Mono.just(mock(RoleBinding.class))); + when(client.update(user)).thenReturn(Mono.just(user)); StepVerifier.create(userService.grantRoles("fake-user", Set.of("fake-role"))) .expectNextCount(1) .verifyComplete(); - verify(client).get(User.class, "fake-user"); - verify(client).list(same(RoleBinding.class), any(), any()); verify(client).delete(notProvidedRoleBinding); } @Test void shouldUpdateRoleBindingIfExists() { - when(client.get(User.class, "fake-user")).thenReturn(Mono.just(mock(User.class))); + var user = createUser("fake-password"); + when(client.get(User.class, "fake-user")).thenReturn(Mono.just(user)); // add another subject - var anotherSubject = new RoleBinding.Subject(); + var anotherSubject = new Subject(); anotherSubject.setName("another-fake-user"); anotherSubject.setKind(User.KIND); anotherSubject.setApiGroup(User.GROUP); @@ -384,17 +275,16 @@ class UserServiceImplTest { var existingRoleBinding = RoleBinding.create("fake-user", "fake-role"); - when(client.list(same(RoleBinding.class), any(), any())).thenReturn( - Flux.fromIterable(List.of(notProvidedRoleBinding, existingRoleBinding))); + when(roleService.listRoleBindings(any(Subject.class))) + .thenReturn(Flux.just(notProvidedRoleBinding, existingRoleBinding)); when(client.update(isA(RoleBinding.class))) .thenReturn(Mono.just(mock(RoleBinding.class))); + when(client.update(user)).thenReturn(Mono.just(user)); StepVerifier.create(userService.grantRoles("fake-user", Set.of("fake-role"))) .expectNextCount(1) .verifyComplete(); - verify(client).get(User.class, "fake-user"); - verify(client).list(same(RoleBinding.class), any(), any()); verify(client).update(notProvidedRoleBinding); } } diff --git a/application/src/test/java/run/halo/app/security/DefaultUserDetailServiceTest.java b/application/src/test/java/run/halo/app/security/DefaultUserDetailServiceTest.java index 58aa4fea1..123f4362b 100644 --- a/application/src/test/java/run/halo/app/security/DefaultUserDetailServiceTest.java +++ b/application/src/test/java/run/halo/app/security/DefaultUserDetailServiceTest.java @@ -4,7 +4,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -24,8 +23,6 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import run.halo.app.core.extension.Role; -import run.halo.app.core.extension.RoleBinding.RoleRef; -import run.halo.app.core.extension.RoleBinding.Subject; import run.halo.app.core.extension.service.RoleService; import run.halo.app.core.extension.service.UserService; import run.halo.app.extension.Metadata; @@ -85,17 +82,8 @@ class DefaultUserDetailServiceTest { void shouldFindUserDetailsByExistingUsername() { var foundUser = createFakeUser(); - var roleGvk = new Role().groupVersionKind(); - var roleRef = new RoleRef(); - roleRef.setKind(roleGvk.kind()); - roleRef.setApiGroup(roleGvk.group()); - roleRef.setName("fake-role"); - - var userGvk = foundUser.groupVersionKind(); - var subject = new Subject(userGvk.kind(), "faker", userGvk.group()); - when(userService.getUser("faker")).thenReturn(Mono.just(foundUser)); - when(roleService.listRoleRefs(subject)).thenReturn(Flux.just(roleRef)); + when(roleService.getRolesByUsername("faker")).thenReturn(Flux.just("fake-role")); var userDetailsMono = userDetailService.findByUsername("faker"); @@ -115,7 +103,7 @@ class DefaultUserDetailServiceTest { void shouldFindHaloUserDetailsWith2faDisabledWhen2faNotEnabled() { var fakeUser = createFakeUser(); when(userService.getUser("faker")).thenReturn(Mono.just(fakeUser)); - when(roleService.listRoleRefs(any())).thenReturn(Flux.empty()); + when(roleService.getRolesByUsername("faker")).thenReturn(Flux.empty()); userDetailService.findByUsername("faker") .as(StepVerifier::create) .assertNext(userDetails -> { @@ -130,7 +118,7 @@ class DefaultUserDetailServiceTest { var fakeUser = createFakeUser(); fakeUser.getSpec().setTwoFactorAuthEnabled(true); when(userService.getUser("faker")).thenReturn(Mono.just(fakeUser)); - when(roleService.listRoleRefs(any())).thenReturn(Flux.empty()); + when(roleService.getRolesByUsername("faker")).thenReturn(Flux.empty()); userDetailService.findByUsername("faker") .as(StepVerifier::create) .assertNext(userDetails -> { @@ -146,7 +134,7 @@ class DefaultUserDetailServiceTest { fakeUser.getSpec().setTwoFactorAuthEnabled(true); fakeUser.getSpec().setTotpEncryptedSecret("fake-totp-encrypted-secret"); when(userService.getUser("faker")).thenReturn(Mono.just(fakeUser)); - when(roleService.listRoleRefs(any())).thenReturn(Flux.empty()); + when(roleService.getRolesByUsername("faker")).thenReturn(Flux.empty()); userDetailService.findByUsername("faker") .as(StepVerifier::create) .assertNext(userDetails -> { @@ -163,7 +151,7 @@ class DefaultUserDetailServiceTest { fakeUser.getSpec().setTwoFactorAuthEnabled(true); fakeUser.getSpec().setTotpEncryptedSecret("fake-totp-encrypted-secret"); when(userService.getUser("faker")).thenReturn(Mono.just(fakeUser)); - when(roleService.listRoleRefs(any())).thenReturn(Flux.empty()); + when(roleService.getRolesByUsername("faker")).thenReturn(Flux.empty()); userDetailService.findByUsername("faker") .as(StepVerifier::create) .assertNext(userDetails -> { @@ -173,50 +161,14 @@ class DefaultUserDetailServiceTest { .verifyComplete(); } - @Test - void shouldFindUserDetailsByExistingUsernameButKindOfRoleRefIsNotRole() { - var foundUser = createFakeUser(); - - var roleRef = new RoleRef(); - roleRef.setKind("FakeRole"); - roleRef.setApiGroup("fake.halo.run"); - roleRef.setName("fake-role"); - - var userGvk = foundUser.groupVersionKind(); - var subject = new Subject(userGvk.kind(), "faker", userGvk.group()); - - when(userService.getUser("faker")).thenReturn(Mono.just(foundUser)); - when(roleService.listRoleRefs(subject)).thenReturn(Flux.just(roleRef)); - - var userDetailsMono = userDetailService.findByUsername("faker"); - - StepVerifier.create(userDetailsMono) - .expectSubscription() - .assertNext(gotUser -> { - assertEquals(foundUser.getMetadata().getName(), gotUser.getUsername()); - assertEquals(foundUser.getSpec().getPassword(), gotUser.getPassword()); - assertEquals(2, gotUser.getAuthorities().size()); - assertEquals( - Set.of("ROLE_anonymous", "ROLE_authenticated"), - authorityListToSet(gotUser.getAuthorities()) - ); - }) - .verifyComplete(); - } - @Test void shouldFindUserDetailsByExistingUsernameButWithoutAnyRoles() { var foundUser = createFakeUser(); - var userGvk = foundUser.groupVersionKind(); - var subject = new Subject(userGvk.kind(), "faker", userGvk.group()); - when(userService.getUser("faker")).thenReturn(Mono.just(foundUser)); - when(roleService.listRoleRefs(subject)).thenReturn(Flux.empty()); + when(roleService.getRolesByUsername("faker")).thenReturn(Flux.empty()); - var userDetailsMono = userDetailService.findByUsername("faker"); - - StepVerifier.create(userDetailsMono) + StepVerifier.create(userDetailService.findByUsername("faker")) .expectSubscription() .assertNext(gotUser -> { assertEquals(foundUser.getMetadata().getName(), gotUser.getUsername()); @@ -240,6 +192,13 @@ class DefaultUserDetailServiceTest { .verify(); } + Role createRole(String roleName) { + var role = new Role(); + role.setMetadata(new Metadata()); + role.getMetadata().setName(roleName); + return role; + } + UserDetails createFakeUserDetails() { return User.builder() .username("faker") diff --git a/application/src/test/java/run/halo/app/security/authorization/AuthorityUtilsTest.java b/application/src/test/java/run/halo/app/security/authorization/AuthorityUtilsTest.java index 4ac5082d3..f9266434b 100644 --- a/application/src/test/java/run/halo/app/security/authorization/AuthorityUtilsTest.java +++ b/application/src/test/java/run/halo/app/security/authorization/AuthorityUtilsTest.java @@ -29,7 +29,7 @@ class AuthorityUtilsTest { var roles = authoritiesToRoles(authorities); - assertEquals(Set.of("admin", "owner", "manager"), roles); + assertEquals(Set.of("admin", "owner", "manager", "faker", "system:read"), roles); } @Test