Skip to content

Commit

Permalink
Support for personal access token mechanism (#4598)
Browse files Browse the repository at this point in the history
#### What type of PR is this?

/kind feature
/kind api-change
/area core

#### What this PR does / why we need it:

Support for personal access token mechanism.

#### Which issue(s) this PR fixes:

Fixes #1309

#### Special notes for your reviewer:

#### Does this PR introduce a user-facing change?

```release-note
提供个人访问令牌机制
```
  • Loading branch information
JohnNiang authored Sep 25, 2023
1 parent 2d5e7bd commit a29c608
Show file tree
Hide file tree
Showing 49 changed files with 3,743 additions and 554 deletions.
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

0 comments on commit a29c608

Please sign in to comment.