Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for personal access token mechanism #4598

Merged
merged 2 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 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 @@ -38,6 +38,8 @@ 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
53 changes: 53 additions & 0 deletions api/src/main/java/run/halo/app/security/PersonalAccessToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package run.halo.app.security;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;

@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@GVK(group = "security.halo.run", version = "v1alpha1", kind = PersonalAccessToken.KIND,
plural = "personalaccesstokens", singular = "personalaccesstoken")
public class PersonalAccessToken extends AbstractExtension {

public static final String KIND = "PersonalAccessToken";

private Spec spec = new Spec();

@Data
@Schema(name = "PatSpec")
public static class Spec {

@Schema(requiredMode = REQUIRED)
private String name;

private String description;

private Instant expiresAt;

private List<String> roles;

private List<String> scopes;

@Schema(requiredMode = REQUIRED)
private String username;

private boolean revoked;

private Instant revokesAt;

private Instant lastUsed;

@Schema(requiredMode = REQUIRED)
private String tokenId;

}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package run.halo.app.config;

import static org.springframework.security.config.Customizer.withDefaults;
import static org.springframework.security.web.server.authentication.ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.builder;
import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;

import java.util.Set;
Expand All @@ -26,6 +27,7 @@
import org.springframework.web.reactive.function.server.ServerResponse;
import run.halo.app.core.extension.service.RoleService;
import run.halo.app.core.extension.service.UserService;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.AnonymousUserConst;
import run.halo.app.infra.properties.HaloProperties;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
Expand All @@ -37,6 +39,9 @@
import run.halo.app.security.authentication.login.PublicKeyRouteBuilder;
import run.halo.app.security.authentication.login.RsaKeyScheduledGenerator;
import run.halo.app.security.authentication.login.impl.RsaKeyService;
import run.halo.app.security.authentication.pat.PatAuthenticationManager;
import run.halo.app.security.authentication.pat.PatJwkSupplier;
import run.halo.app.security.authentication.pat.PatServerWebExchangeMatcher;
import run.halo.app.security.authorization.RequestInfoAuthorizationManager;

/**
Expand All @@ -55,7 +60,9 @@ SecurityWebFilterChain apiFilterChain(ServerHttpSecurity http,
RoleService roleService,
ObjectProvider<SecurityConfigurer> securityConfigurers,
ServerSecurityContextRepository securityContextRepository,
ExtensionGetter extensionGetter) {
ExtensionGetter extensionGetter,
ReactiveExtensionClient client,
PatJwkSupplier patJwkSupplier) {

http.securityMatcher(pathMatchers("/api/**", "/apis/**", "/oauth2/**",
"/login/**", "/logout", "/actuator/**"))
Expand All @@ -68,6 +75,14 @@ SecurityWebFilterChain apiFilterChain(ServerHttpSecurity http,
})
.securityContextRepository(securityContextRepository)
.httpBasic(withDefaults())
.oauth2ResourceServer(oauth2 -> {
var authManagerResolver = builder().add(
new PatServerWebExchangeMatcher(),
new PatAuthenticationManager(client, patJwkSupplier))
// TODO Add other authentication mangers here. e.g.: JwtAuthentiationManager.
.build();
oauth2.authenticationManagerResolver(authManagerResolver);
})
.exceptionHandling(
spec -> spec.authenticationEntryPoint(new DefaultServerAuthenticationEntryPoint()));

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,86 @@ 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
Loading