Skip to content

Commit

Permalink
Support permissions listing
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnNiang committed Sep 20, 2023
1 parent f711a11 commit 591da09
Show file tree
Hide file tree
Showing 11 changed files with 301 additions and 134 deletions.
1 change: 1 addition & 0 deletions api/src/main/java/run/halo/app/core/extension/Role.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -483,45 +486,85 @@ record GrantRequest(Set<String> roles) {

@NonNull
private Mono<ServerResponse> 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<Role>(), (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> 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<String> uiPermissions(Set<Role> roles) {
return Flux.fromIterable(roles)
.map(role -> role.getMetadata().getName())
.collectList()
.flatMapMany(roleNames -> roleService.listDependenciesFlux(Set.copyOf(roleNames)))
.map(role -> {
Map<String, String> annotations = MetadataUtil.nullSafeAnnotations(role);
String uiPermissionStr = annotations.get(Role.UI_PERMISSIONS_ANNO);
if (StringUtils.isBlank(uiPermissionStr)) {
return new HashSet<String>();
private Set<String> uiPermissions(Set<Role> roles) {
if (CollectionUtils.isEmpty(roles)) {
return Collections.emptySet();
}
return roles.stream()
.<Set<String>>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<LinkedHashSet<String>>() {
});
})
.flatMapIterable(Function.identity());
.flatMap(Set::stream)
.collect(Collectors.toSet());
}

record UserPermission(@Schema(requiredMode = REQUIRED) Set<Role> roles,
@Schema(requiredMode = REQUIRED) Set<String> uiPermissions) {
@Data
public static class UserPermission {
@Schema(requiredMode = REQUIRED)
private Set<Role> roles;
@Schema(requiredMode = REQUIRED)
private List<Role> permissions;
@Schema(requiredMode = REQUIRED)
private Set<String> uiPermissions;

}

public class ListRequest extends IListRequest.QueryListRequest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
* <p>Obtain the authorities from the authenticated authentication and construct it as a RoleBinding
Expand All @@ -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<String> listBoundRoleNames(Collection<? extends GrantedAuthority> authorities) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -54,45 +54,73 @@ public Mono<Boolean> contains(Collection<String> source, Collection<String> 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<Role> listPermissions(Set<String> 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<Role> listDependenciesFlux(Set<String> names) {
return listDependencies(names, false);
return listDependencies(names, shouldFilterHidden(false));
}

private Flux<Role> listRoles(Set<String> names, boolean filterHidden) {
private Flux<Role> listRoles(Set<String> names, Predicate<Role> additionalPredicate) {
if (CollectionUtils.isEmpty(names)) {
return Flux.empty();
}
log.debug("Searching roles: {}, filter hidden: {}", names, filterHidden);

Predicate<Role> predicate = role -> names.contains(role.getMetadata().getName());
if (filterHidden) {
Predicate<Role> 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<Role> listDependencies(Set<String> names, boolean filterHidden) {
private static Predicate<Role> 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<Role> listDependencies(Set<String> names, Predicate<Role> additionalPredicate) {
var visited = new HashSet<String>();
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);
Expand All @@ -101,12 +129,13 @@ private Flux<Role> listDependencies(Set<String> 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<Role> listAggregatedRoles(Set<String> roleNames) {
private Flux<Role> listAggregatedRoles(Set<String> roleNames,
Predicate<Role> additionalPredicate) {
var aggregatedLabelNames = roleNames.stream()
.map(roleName -> Role.ROLE_AGGREGATE_LABEL_PREFIX + roleName)
.collect(Collectors.toSet());
Expand All @@ -118,6 +147,9 @@ private Flux<Role> listAggregatedRoles(Set<String> 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));
}

Expand All @@ -143,7 +175,10 @@ private static boolean matchSubject(Subject targetSubject, Subject subject) {

@Override
public Flux<Role> list(Set<String> 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));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ public interface RoleService {

Mono<Boolean> contains(Collection<String> source, Collection<String> 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<Role> listPermissions(Set<String> names);

Flux<Role> listDependenciesFlux(Set<String> names);

Flux<Role> list(Set<String> roleNames);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -12,7 +13,7 @@
*/
public interface SuperAdminInitializer {

String SUPER_ROLE_NAME = "super-role";
String SUPER_ROLE_NAME = AuthorityUtils.SUPER_ROLE_NAME;

/**
* Initialize super admin.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -33,7 +34,9 @@ public Flux<GrantedAuthority> 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);
Expand Down
Loading

0 comments on commit 591da09

Please sign in to comment.