diff --git a/api/src/main/java/run/halo/app/core/extension/Role.java b/api/src/main/java/run/halo/app/core/extension/Role.java index ec01eb7f8ee..ebda43f4d06 100644 --- a/api/src/main/java/run/halo/app/core/extension/Role.java +++ b/api/src/main/java/run/halo/app/core/extension/Role.java @@ -39,6 +39,7 @@ public class Role extends AbstractExtension { public static final String SYSTEM_RESERVED_LABELS = "rbac.authorization.halo.run/system-reserved"; public static final String HIDDEN_LABEL_NAME = "halo.run/hidden"; + public static final String TEMPLATE_LABEL_NAME = "halo.run/role-template"; public static final String UI_PERMISSIONS_AGGREGATED_ANNO = "rbac.authorization.halo.run/ui-permissions-aggregated"; 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 58463a269be..6e5a5ec9ca2 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 @@ -13,6 +13,7 @@ import static run.halo.app.extension.ListResult.generateGenericClass; import static run.halo.app.extension.router.QueryParamBuildUtil.buildParametersFromType; import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate; +import static run.halo.app.security.authorization.AuthorityUtils.authoritiesToRoles; import com.fasterxml.jackson.core.type.TypeReference; import com.google.common.io.Files; @@ -21,6 +22,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.time.Duration; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; @@ -31,8 +33,9 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; +import lombok.Data; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.springdoc.core.fn.builders.requestbody.Builder; @@ -483,45 +486,85 @@ record GrantRequest(Set roles) { @NonNull private Mono getUserPermission(ServerRequest request) { - String name = request.pathVariable("name"); - return ReactiveSecurityContextHolder.getContext() - .map(ctx -> SELF_USER.equals(name) ? ctx.getAuthentication().getName() : name) - .flatMapMany(userService::listRoles) - .reduce(new LinkedHashSet(), (list, role) -> { - list.add(role); - return list; - }) - .flatMap(roles -> uiPermissions(roles) - .collectList() - .map(uiPermissions -> new UserPermission(roles, Set.copyOf(uiPermissions))) - .defaultIfEmpty(new UserPermission(roles, Set.of())) - ) - .flatMap(result -> ServerResponse.ok() - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(result) - ); + 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); + }); + } 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); } - private Flux uiPermissions(Set roles) { - return Flux.fromIterable(roles) - .map(role -> role.getMetadata().getName()) - .collectList() - .flatMapMany(roleNames -> roleService.listDependenciesFlux(Set.copyOf(roleNames))) - .map(role -> { - Map annotations = MetadataUtil.nullSafeAnnotations(role); - String uiPermissionStr = annotations.get(Role.UI_PERMISSIONS_ANNO); - if (StringUtils.isBlank(uiPermissionStr)) { - return new HashSet(); + private Set uiPermissions(Set roles) { + if (CollectionUtils.isEmpty(roles)) { + return Collections.emptySet(); + } + return roles.stream() + .>map(role -> { + var annotations = role.getMetadata().getAnnotations(); + if (annotations == null) { + return Set.of(); } - return JsonUtils.jsonToObject(uiPermissionStr, + var uiPermissionsJson = annotations.get(Role.UI_PERMISSIONS_ANNO); + if (StringUtils.isBlank(uiPermissionsJson)) { + return Set.of(); + } + return JsonUtils.jsonToObject(uiPermissionsJson, new TypeReference>() { }); }) - .flatMapIterable(Function.identity()); + .flatMap(Set::stream) + .collect(Collectors.toSet()); } - record UserPermission(@Schema(requiredMode = REQUIRED) Set roles, - @Schema(requiredMode = REQUIRED) Set uiPermissions) { + @Data + public static class UserPermission { + @Schema(requiredMode = REQUIRED) + private Set roles; + @Schema(requiredMode = REQUIRED) + private List permissions; + @Schema(requiredMode = REQUIRED) + private Set uiPermissions; + } public class ListRequest extends IListRequest.QueryListRequest { diff --git a/application/src/main/java/run/halo/app/core/extension/service/DefaultRoleBindingService.java b/application/src/main/java/run/halo/app/core/extension/service/DefaultRoleBindingService.java index d73c9ad09b9..9152f77319e 100644 --- a/application/src/main/java/run/halo/app/core/extension/service/DefaultRoleBindingService.java +++ b/application/src/main/java/run/halo/app/core/extension/service/DefaultRoleBindingService.java @@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; +import run.halo.app.security.authorization.AuthorityUtils; /** *

Obtain the authorities from the authenticated authentication and construct it as a RoleBinding @@ -21,8 +22,8 @@ */ @Slf4j public class DefaultRoleBindingService implements RoleBindingService { - private static final String SCOPE_AUTHORITY_PREFIX = "SCOPE_"; - private static final String ROLE_AUTHORITY_PREFIX = "ROLE_"; + private static final String SCOPE_AUTHORITY_PREFIX = AuthorityUtils.SCOPE_PREFIX; + private static final String ROLE_AUTHORITY_PREFIX = AuthorityUtils.ROLE_PREFIX; @Override public Set listBoundRoleNames(Collection authorities) { 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 2bef3ca7ec9..4e1ca0b271c 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,6 +1,7 @@ package run.halo.app.core.extension.service; import static run.halo.app.extension.Comparators.compareCreationTimestamp; +import static run.halo.app.security.authorization.AuthorityUtils.containsSuperRole; import com.fasterxml.jackson.core.type.TypeReference; import java.util.Collection; @@ -11,7 +12,6 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.lang.NonNull; import org.springframework.stereotype.Service; @@ -54,45 +54,73 @@ public Mono contains(Collection source, Collection cand if (source.contains(SuperAdminInitializer.SUPER_ROLE_NAME)) { return Mono.just(true); } - return listDependencies(new HashSet<>(source), true) + return listDependencies(new HashSet<>(source), shouldFilterHidden(true)) .map(role -> role.getMetadata().getName()) .collect(Collectors.toSet()) .map(roleNames -> roleNames.containsAll(candidates)); } + @Override + public Flux listPermissions(Set names) { + if (containsSuperRole(names)) { + // search all permissions + return extensionClient.list(Role.class, + shouldFilterHidden(true), + compareCreationTimestamp(true)) + .filter(DefaultRoleService::isRoleTemplate); + } + return listDependencies(names, shouldFilterHidden(true)) + .filter(DefaultRoleService::isRoleTemplate); + } + @Override public Flux listDependenciesFlux(Set names) { - return listDependencies(names, false); + return listDependencies(names, shouldFilterHidden(false)); } - private Flux listRoles(Set names, boolean filterHidden) { + private Flux listRoles(Set names, Predicate additionalPredicate) { if (CollectionUtils.isEmpty(names)) { return Flux.empty(); } - log.debug("Searching roles: {}, filter hidden: {}", names, filterHidden); + Predicate predicate = role -> names.contains(role.getMetadata().getName()); - if (filterHidden) { - Predicate hiddenPredicate = role -> { - var labels = role.getMetadata().getLabels(); - if (labels == null) { - return false; - } - var hiddenValue = labels.get(Role.HIDDEN_LABEL_NAME); - return Boolean.parseBoolean(hiddenValue); - }; - predicate = predicate.and(hiddenPredicate.negate()); + if (additionalPredicate != null) { + predicate = predicate.and(additionalPredicate); } return extensionClient.list(Role.class, predicate, compareCreationTimestamp(true)); } - private Flux listDependencies(Set names, boolean filterHidden) { + private static Predicate shouldFilterHidden(boolean filterHidden) { + if (!filterHidden) { + return r -> true; + } + return role -> { + var labels = role.getMetadata().getLabels(); + if (labels == null) { + return true; + } + var hiddenValue = labels.get(Role.HIDDEN_LABEL_NAME); + return !Boolean.parseBoolean(hiddenValue); + }; + } + + 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) { var visited = new HashSet(); - return listRoles(names, filterHidden) + return listRoles(names, additionalPredicate) .expand(role -> { var name = role.getMetadata().getName(); if (visited.contains(name)) { return Flux.empty(); } + log.debug("Expand role: {}", role.getMetadata().getName()); visited.add(name); var annotations = MetadataUtil.nullSafeAnnotations(role); var dependenciesJson = annotations.get(Role.ROLE_DEPENDENCIES_ANNO); @@ -101,12 +129,13 @@ private Flux listDependencies(Set names, boolean filterHidden) { return Flux.fromIterable(dependencies) .filter(dep -> !visited.contains(dep)) .collect(Collectors.toSet()) - .flatMapMany(deps -> listRoles(deps, filterHidden)); + .flatMapMany(deps -> listRoles(deps, additionalPredicate)); }) - .concatWith(Flux.defer(() -> listAggregatedRoles(visited))); + .concatWith(Flux.defer(() -> listAggregatedRoles(visited, additionalPredicate))); } - private Flux listAggregatedRoles(Set roleNames) { + private Flux listAggregatedRoles(Set roleNames, + Predicate additionalPredicate) { var aggregatedLabelNames = roleNames.stream() .map(roleName -> Role.ROLE_AGGREGATE_LABEL_PREFIX + roleName) .collect(Collectors.toSet()); @@ -118,6 +147,9 @@ private Flux listAggregatedRoles(Set roleNames) { return aggregatedLabelNames.stream() .anyMatch(aggregatedLabel -> Boolean.parseBoolean(labels.get(aggregatedLabel))); }; + if (additionalPredicate != null) { + predicate = predicate.and(additionalPredicate); + } return extensionClient.list(Role.class, predicate, compareCreationTimestamp(true)); } @@ -143,7 +175,10 @@ private static boolean matchSubject(Subject targetSubject, Subject subject) { @Override public Flux list(Set roleNames) { - return Flux.fromIterable(ObjectUtils.defaultIfNull(roleNames, Set.of())) + if (CollectionUtils.isEmpty(roleNames)) { + return Flux.empty(); + } + return Flux.fromIterable(roleNames) .flatMap(roleName -> extensionClient.fetch(Role.class, roleName)); } 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 187701da1d6..e52917dcd45 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 @@ -18,6 +18,15 @@ public interface RoleService { Mono contains(Collection source, Collection candidates); + /** + * This method lists all role templates as permissions recursively according to given role + * name set. + * + * @param names is role name set. + * @return an array of permissions. + */ + Flux listPermissions(Set names); + Flux listDependenciesFlux(Set names); Flux list(Set roleNames); diff --git a/application/src/main/java/run/halo/app/security/SuperAdminInitializer.java b/application/src/main/java/run/halo/app/security/SuperAdminInitializer.java index 1476ee11e82..802d87ff171 100644 --- a/application/src/main/java/run/halo/app/security/SuperAdminInitializer.java +++ b/application/src/main/java/run/halo/app/security/SuperAdminInitializer.java @@ -3,6 +3,7 @@ import lombok.Builder; import lombok.Data; import reactor.core.publisher.Mono; +import run.halo.app.security.authorization.AuthorityUtils; /** * Super admin initializer. @@ -12,7 +13,7 @@ */ public interface SuperAdminInitializer { - String SUPER_ROLE_NAME = "super-role"; + String SUPER_ROLE_NAME = AuthorityUtils.SUPER_ROLE_NAME; /** * Initialize super admin. diff --git a/application/src/main/java/run/halo/app/security/authentication/jwt/JwtScopesAndRolesGrantedAuthoritiesConverter.java b/application/src/main/java/run/halo/app/security/authentication/jwt/JwtScopesAndRolesGrantedAuthoritiesConverter.java index c1e852a4d97..7a328084563 100644 --- a/application/src/main/java/run/halo/app/security/authentication/jwt/JwtScopesAndRolesGrantedAuthoritiesConverter.java +++ b/application/src/main/java/run/halo/app/security/authentication/jwt/JwtScopesAndRolesGrantedAuthoritiesConverter.java @@ -9,6 +9,7 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.util.CollectionUtils; import reactor.core.publisher.Flux; +import run.halo.app.security.authorization.AuthorityUtils; /** * GrantedAuthorities converter for SCOPE_ and ROLE_ prefixes. @@ -33,7 +34,9 @@ public Flux convert(Jwt jwt) { } var roles = jwt.getClaimAsStringList("roles"); if (!CollectionUtils.isEmpty(roles)) { - roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)) + roles.stream() + .map(role -> AuthorityUtils.ROLE_PREFIX + role) + .map(SimpleGrantedAuthority::new) .forEach(grantedAuthorities::add); } return Flux.fromIterable(grantedAuthorities); diff --git a/application/src/main/java/run/halo/app/security/authentication/pat/impl/UserScopedPatHandlerImpl.java b/application/src/main/java/run/halo/app/security/authentication/pat/impl/UserScopedPatHandlerImpl.java index bc1921ea425..e4b293b0996 100644 --- a/application/src/main/java/run/halo/app/security/authentication/pat/impl/UserScopedPatHandlerImpl.java +++ b/application/src/main/java/run/halo/app/security/authentication/pat/impl/UserScopedPatHandlerImpl.java @@ -1,6 +1,5 @@ package run.halo.app.security.authentication.pat.impl; -import static org.apache.commons.lang3.StringUtils.removeStart; import static org.apache.commons.lang3.StringUtils.startsWith; import static run.halo.app.extension.Comparators.compareCreationTimestamp; import static run.halo.app.security.authentication.pat.PatServerWebExchangeMatcher.PAT_TOKEN_PREFIX; @@ -43,11 +42,12 @@ import run.halo.app.security.PersonalAccessToken; import run.halo.app.security.authentication.pat.PatJwkSupplier; import run.halo.app.security.authentication.pat.UserScopedPatHandler; +import run.halo.app.security.authorization.AuthorityUtils; @Service public class UserScopedPatHandlerImpl implements UserScopedPatHandler { - private static final String ROLE_PREFIX = "ROLE_"; + private static final String ROLE_PREFIX = AuthorityUtils.ROLE_PREFIX; private static final String ACCESS_TOKEN_ANNO_NAME = "security.halo.run/access-token"; @@ -120,7 +120,7 @@ public Mono create(ServerRequest request) { var pat = new PersonalAccessToken(); var spec = pat.getSpec(); spec.setUsername(auth.getName()); - spec.setName(spec.getName()); + spec.setName(patSpec.getName()); spec.setDescription(patSpec.getDescription()); spec.setRoles(patSpec.getRoles()); spec.setScopes(patSpec.getScopes()); @@ -226,12 +226,7 @@ private Mono hasSufficientRoles( if (CollectionUtils.isEmpty(requestRoles)) { return Mono.just(true); } - - var grantedRoles = grantedAuthorities.stream() - .map(GrantedAuthority::getAuthority) - .filter(authority -> startsWith(authority, ROLE_PREFIX)) - .map(authority -> removeStart(authority, ROLE_PREFIX)) - .collect(Collectors.toSet()); + var grantedRoles = AuthorityUtils.authoritiesToRoles(grantedAuthorities); return roleService.contains(grantedRoles, requestRoles); } 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 new file mode 100644 index 00000000000..497303b4ddc --- /dev/null +++ b/application/src/main/java/run/halo/app/security/authorization/AuthorityUtils.java @@ -0,0 +1,41 @@ +package run.halo.app.security.authorization; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.springframework.security.core.GrantedAuthority; + +/** + * Utility methods for manipulating GrantedAuthority collection. + * + * @author johnniang + */ +public enum AuthorityUtils { + ; + + public static final String SCOPE_PREFIX = "ROLE_"; + + public static final String ROLE_PREFIX = "ROLE_"; + + public static final String SUPER_ROLE_NAME = "super-role"; + + /** + * Converts an array of GrantedAuthority objects to a role set. + * + * @return a Set of the Strings obtained from each call to + * GrantedAuthority.getAuthority() and filtered by prefix "ROLE_". + */ + public static Set authoritiesToRoles( + Collection authorities) { + return authorities.stream() + .map(GrantedAuthority::getAuthority) + .filter(authority -> StringUtils.startsWith(authority, ROLE_PREFIX)) + .map(authority -> StringUtils.removeStart(authority, ROLE_PREFIX)) + .collect(Collectors.toSet()); + } + + public static boolean containsSuperRole(Collection roles) { + return roles.contains(SUPER_ROLE_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 8db68ba588d..33d09978590 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 @@ -2,8 +2,9 @@ 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.lenient; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -22,7 +23,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; 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; @@ -49,21 +49,21 @@ class ListDependenciesTest { @Test void listDependencies() { // prepare test data - Role role1 = createRole("role1", "role2"); - Role role2 = createRole("role2", "role3"); - Role role3 = createRole("role3"); + var role1 = createRole("role1", "role2"); + var role2 = createRole("role2", "role3"); + var role3 = createRole("role3"); - Set roleNames = Set.of("role1"); + var roleNames = Set.of("role1"); - // setup mocks - when(extensionClient.fetch(Role.class, "role1")).thenReturn(Mono.just(role1)); - when(extensionClient.fetch(Role.class, "role2")).thenReturn(Mono.just(role2)); - when(extensionClient.fetch(Role.class, "role3")).thenReturn(Mono.just(role3)); - when(extensionClient.list(eq(Role.class), any(), any())) + + when(extensionClient.list(same(Role.class), any(), any())) + .thenReturn(Flux.just(role1)) + .thenReturn(Flux.just(role2)) + .thenReturn(Flux.just(role3)) .thenReturn(Flux.empty()); // call the method under test - Flux result = roleService.listDependenciesFlux(roleNames); + var result = roleService.listDependenciesFlux(roleNames); // verify the result StepVerifier.create(result) @@ -73,27 +73,27 @@ void listDependencies() { .verifyComplete(); // verify the mock invocations - verify(extensionClient, times(3)).fetch(eq(Role.class), anyString()); + verify(extensionClient, times(4)).list(same(Role.class), any(), any()); } @Test void listDependenciesWithCycle() { // prepare test data - Role role1 = createRole("role1", "role2"); - Role role2 = createRole("role2", "role3"); - Role role3 = createRole("role3", "role1"); + var role1 = createRole("role1", "role2"); + var role2 = createRole("role2", "role3"); + var role3 = createRole("role3", "role1"); - Set roleNames = Set.of("role1"); + var roleNames = Set.of("role1"); // setup mocks - when(extensionClient.fetch(Role.class, "role1")).thenReturn(Mono.just(role1)); - when(extensionClient.fetch(Role.class, "role2")).thenReturn(Mono.just(role2)); - when(extensionClient.fetch(Role.class, "role3")).thenReturn(Mono.just(role3)); - when(extensionClient.list(eq(Role.class), any(), any())) + when(extensionClient.list(same(Role.class), any(), any())) + .thenReturn(Flux.just(role1)) + .thenReturn(Flux.just(role2)) + .thenReturn(Flux.just(role3)) .thenReturn(Flux.empty()); // call the method under test - Flux result = roleService.listDependenciesFlux(roleNames); + var result = roleService.listDependenciesFlux(roleNames); // verify the result StepVerifier.create(result) @@ -103,7 +103,7 @@ void listDependenciesWithCycle() { .verifyComplete(); // verify the mock invocations - verify(extensionClient, times(3)).fetch(eq(Role.class), anyString()); + verify(extensionClient, times(4)).list(same(Role.class), any(), any()); } @Test @@ -111,23 +111,23 @@ void listDependenciesWithMiddleCycle() { // prepare test data // role1 -> role2 -> role3 -> role4 // \<-----| - Role role1 = createRole("role1", "role2"); - Role role2 = createRole("role2", "role3"); - Role role3 = createRole("role3", "role2", "role4"); - Role role4 = createRole("role4"); + var role1 = createRole("role1", "role2"); + var role2 = createRole("role2", "role3"); + var role3 = createRole("role3", "role2", "role4"); + var role4 = createRole("role4"); - Set roleNames = Set.of("role1"); + var roleNames = Set.of("role1"); - // setup mocks - when(extensionClient.fetch(Role.class, "role1")).thenReturn(Mono.just(role1)); - when(extensionClient.fetch(Role.class, "role2")).thenReturn(Mono.just(role2)); - when(extensionClient.fetch(Role.class, "role3")).thenReturn(Mono.just(role3)); - when(extensionClient.fetch(Role.class, "role4")).thenReturn(Mono.just(role4)); - when(extensionClient.list(eq(Role.class), any(), any())) - .thenReturn(Flux.empty()); + when(extensionClient.list(same(Role.class), any(), any())) + .thenReturn(Flux.just(role1)) + .thenReturn(Flux.just(role2)) + .thenReturn(Flux.just(role3)) + .thenReturn(Flux.just(role4)) + .thenReturn(Flux.empty()) + ; // call the method under test - Flux result = roleService.listDependenciesFlux(roleNames); + var result = roleService.listDependenciesFlux(roleNames); // verify the result StepVerifier.create(result) @@ -138,7 +138,7 @@ void listDependenciesWithMiddleCycle() { .verifyComplete(); // verify the mock invocations - verify(extensionClient, times(4)).fetch(eq(Role.class), anyString()); + verify(extensionClient, times(5)).list(same(Role.class), any(), any()); } @Test @@ -153,16 +153,14 @@ void listDependenciesWithCycleAndSequence() { Set roleNames = Set.of("role1"); - // setup mocks - when(extensionClient.fetch(Role.class, "role1")).thenReturn(Mono.just(role1)); - when(extensionClient.fetch(Role.class, "role2")).thenReturn(Mono.just(role2)); - when(extensionClient.fetch(Role.class, "role3")).thenReturn(Mono.just(role3)); - when(extensionClient.fetch(Role.class, "role4")).thenReturn(Mono.just(role4)); - when(extensionClient.list(eq(Role.class), any(), any())) + when(extensionClient.list(same(Role.class), any(), any())) + .thenReturn(Flux.just(role1)) + .thenReturn(Flux.just(role4, role2)) + .thenReturn(Flux.just(role3)) .thenReturn(Flux.empty()); // call the method under test - Flux result = roleService.listDependenciesFlux(roleNames); + var result = roleService.listDependenciesFlux(roleNames); // verify the result StepVerifier.create(result) @@ -173,7 +171,7 @@ void listDependenciesWithCycleAndSequence() { .verifyComplete(); // verify the mock invocations - verify(extensionClient, times(4)).fetch(eq(Role.class), anyString()); + verify(extensionClient, times(4)).list(same(Role.class), any(), any()); } @Test @@ -188,16 +186,13 @@ void listDependenciesAfterCycle() { Set roleNames = Set.of("role2"); - // setup mocks - lenient().when(extensionClient.fetch(Role.class, "role1")).thenReturn(Mono.just(role1)); - when(extensionClient.fetch(Role.class, "role2")).thenReturn(Mono.just(role2)); - when(extensionClient.fetch(Role.class, "role3")).thenReturn(Mono.just(role3)); - lenient().when(extensionClient.fetch(Role.class, "role4")).thenReturn(Mono.just(role4)); - when(extensionClient.list(eq(Role.class), any(), any())) + when(extensionClient.list(same(Role.class), any(), any())) + .thenReturn(Flux.just(role2)) + .thenReturn(Flux.just(role3)) .thenReturn(Flux.empty()); // call the method under test - Flux result = roleService.listDependenciesFlux(roleNames); + var result = roleService.listDependenciesFlux(roleNames); // verify the result StepVerifier.create(result) @@ -206,12 +201,17 @@ void listDependenciesAfterCycle() { .verifyComplete(); // verify the mock invocations - verify(extensionClient, times(2)).fetch(eq(Role.class), anyString()); + verify(extensionClient, times(3)).list(same(Role.class), any(), any()); } @Test void listDependenciesWithNullParam() { - Flux result = roleService.listDependenciesFlux(null); + 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,26 +221,25 @@ void listDependenciesWithNullParam() { .verifyComplete(); // verify the mock invocations - verify(extensionClient, times(0)).fetch(eq(Role.class), anyString()); + verify(extensionClient, never()).fetch(eq(Role.class), anyString()); } @Test void listDependenciesAndSomeOneNotFound() { - Role role1 = createRole("role1", "role2"); - Role role2 = createRole("role2", "role3", "role4"); - Role role4 = createRole("role4"); + var role1 = createRole("role1", "role2"); + var role2 = createRole("role2", "role3", "role4"); + var role4 = createRole("role4"); - Set roleNames = Set.of("role1"); + var roleNames = Set.of("role1"); - // setup mocks - when(extensionClient.fetch(Role.class, "role1")).thenReturn(Mono.just(role1)); - when(extensionClient.fetch(Role.class, "role2")).thenReturn(Mono.just(role2)); - when(extensionClient.fetch(Role.class, "role3")).thenReturn(Mono.empty()); - when(extensionClient.fetch(Role.class, "role4")).thenReturn(Mono.just(role4)); - when(extensionClient.list(eq(Role.class), any(), any())) - .thenReturn(Flux.empty()); + when(extensionClient.list(same(Role.class), any(), any())) + .thenReturn(Flux.just(role1)) + .thenReturn(Flux.just(role2)) + .thenReturn(Flux.just(role4)) + .thenReturn(Flux.empty()) + ; - Flux result = roleService.listDependenciesFlux(roleNames); + var result = roleService.listDependenciesFlux(roleNames); // verify the result StepVerifier.create(result) .expectNext(role1) @@ -249,7 +248,7 @@ void listDependenciesAndSomeOneNotFound() { .verifyComplete(); // verify the mock invocations - verify(extensionClient, times(4)).fetch(eq(Role.class), anyString()); + verify(extensionClient, times(4)).list(same(Role.class), any(), any()); } @Test diff --git a/application/src/test/java/run/halo/app/security/authentication/pat/PatTest.java b/application/src/test/java/run/halo/app/security/authentication/pat/PatTest.java new file mode 100644 index 00000000000..6a8637af71f --- /dev/null +++ b/application/src/test/java/run/halo/app/security/authentication/pat/PatTest.java @@ -0,0 +1,39 @@ +package run.halo.app.security.authentication.pat; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.reactive.server.WebTestClient; +import run.halo.app.security.PersonalAccessToken; + +@SpringBootTest +@AutoConfigureWebTestClient +class PatTest { + + @Autowired + WebTestClient webClient; + + @Test + @WithMockUser(username = "faker", password = "${noop}password", roles = "super-role") + void generatePat() { + var requestPat = new PersonalAccessToken(); + var spec = requestPat.getSpec(); + spec.setRoles(List.of("super-role")); + webClient.post() + .uri("/apis/api.console.security.halo.run/v1alpha1/users/-/personalaccesstokens") + .bodyValue(requestPat) + .exchange() + .expectStatus().isOk() + .expectBody(PersonalAccessToken.class) + .value(pat -> { + var annotations = pat.getMetadata().getAnnotations(); + assertTrue(annotations.containsKey("security.halo.run/access-token")); + }); + } + +}