mirror of https://github.com/halo-dev/halo
Add user support for authentication and authorization (#2163)
* Move role and rolebinding extensions into core package * Add UserExtensionUserDetailService to find user and update password * Rename DefaultUserDetailService * Fix test errors * Add SuperAdminInitializer to initialize first user * Add unit tests for DefaultUserDetailService and UserService * Add test for verifying SuperAdminInitializer * Fix unstable test due to database reusability Signed-off-by: johnniang <johnniang@fastmail.com>pull/2161/head
parent
e52db6859f
commit
f5d629d2bf
|
@ -0,0 +1,18 @@
|
||||||
|
package run.halo.app.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
public class HaloConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
Jackson2ObjectMapperBuilderCustomizer objectMapperCustomizer() {
|
||||||
|
return builder -> {
|
||||||
|
builder.serializationInclusion(JsonInclude.Include.NON_NULL);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -14,9 +14,7 @@ import org.springframework.http.codec.ServerCodecConfigurer;
|
||||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||||
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
|
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
|
||||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||||
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
|
|
||||||
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
||||||
import org.springframework.security.core.userdetails.User;
|
|
||||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||||
|
@ -27,11 +25,13 @@ import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder;
|
||||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||||
import org.springframework.security.web.server.csrf.CookieServerCsrfTokenRepository;
|
import org.springframework.security.web.server.csrf.CookieServerCsrfTokenRepository;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
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.infra.properties.JwtProperties;
|
import run.halo.app.infra.properties.JwtProperties;
|
||||||
|
import run.halo.app.security.DefaultUserDetailService;
|
||||||
import run.halo.app.security.authentication.jwt.LoginAuthenticationFilter;
|
import run.halo.app.security.authentication.jwt.LoginAuthenticationFilter;
|
||||||
import run.halo.app.security.authentication.jwt.LoginAuthenticationManager;
|
import run.halo.app.security.authentication.jwt.LoginAuthenticationManager;
|
||||||
import run.halo.app.security.authorization.RequestInfoAuthorizationManager;
|
import run.halo.app.security.authorization.RequestInfoAuthorizationManager;
|
||||||
import run.halo.app.security.authorization.RoleGetter;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Security configuration for WebFlux.
|
* Security configuration for WebFlux.
|
||||||
|
@ -52,15 +52,18 @@ public class WebServerSecurityConfig {
|
||||||
SecurityWebFilterChain apiFilterChain(ServerHttpSecurity http,
|
SecurityWebFilterChain apiFilterChain(ServerHttpSecurity http,
|
||||||
ServerCodecConfigurer codec,
|
ServerCodecConfigurer codec,
|
||||||
ServerResponse.Context context,
|
ServerResponse.Context context,
|
||||||
RoleGetter roleGetter) {
|
UserService userService,
|
||||||
|
RoleService roleService) {
|
||||||
http.csrf().disable()
|
http.csrf().disable()
|
||||||
.securityMatcher(pathMatchers("/api/**", "/apis/**"))
|
.securityMatcher(pathMatchers("/api/**", "/apis/**"))
|
||||||
.authorizeExchange(exchanges ->
|
.authorizeExchange(exchanges ->
|
||||||
exchanges.anyExchange().access(new RequestInfoAuthorizationManager(roleGetter)))
|
exchanges.anyExchange().access(new RequestInfoAuthorizationManager(roleService)))
|
||||||
// for reuse the JWT authentication
|
// for reuse the JWT authentication
|
||||||
.oauth2ResourceServer().jwt();
|
.oauth2ResourceServer().jwt();
|
||||||
|
|
||||||
var loginManager = new LoginAuthenticationManager(userDetailsService(), passwordEncoder());
|
var loginManager = new LoginAuthenticationManager(
|
||||||
|
userDetailsService(userService, roleService),
|
||||||
|
passwordEncoder());
|
||||||
var loginFilter = new LoginAuthenticationFilter(loginManager,
|
var loginFilter = new LoginAuthenticationFilter(loginManager,
|
||||||
codec,
|
codec,
|
||||||
jwtEncoder(),
|
jwtEncoder(),
|
||||||
|
@ -92,15 +95,9 @@ public class WebServerSecurityConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
ReactiveUserDetailsService userDetailsService() {
|
ReactiveUserDetailsService userDetailsService(UserService userService,
|
||||||
//TODO Implement details service when User Extension is ready.
|
RoleService roleService) {
|
||||||
return new MapReactiveUserDetailsService(
|
return new DefaultUserDetailService(userService, roleService);
|
||||||
// for test
|
|
||||||
User.withDefaultPasswordEncoder().username("user").password("password").roles("USER")
|
|
||||||
.build(),
|
|
||||||
// for test
|
|
||||||
User.withDefaultPasswordEncoder().username("admin").password("password").roles("ADMIN")
|
|
||||||
.build());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
package run.halo.app.core.extension;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
@GVK(group = "",
|
||||||
|
version = "v1alpha1",
|
||||||
|
kind = "Role",
|
||||||
|
plural = "roles",
|
||||||
|
singular = "role")
|
||||||
|
public class Role extends AbstractExtension {
|
||||||
|
|
||||||
|
@Schema(minLength = 1)
|
||||||
|
List<PolicyRule> rules;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PolicyRule holds information that describes a policy rule, but does not contain information
|
||||||
|
* about whom the rule applies to or which namespace the rule applies to.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class PolicyRule {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APIGroups is the name of the APIGroup that contains the resources.
|
||||||
|
* If multiple API groups are specified, any action requested against one of the enumerated
|
||||||
|
* resources in any API group will be allowed.
|
||||||
|
*/
|
||||||
|
String[] apiGroups;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resources is a list of resources this rule applies to. '*' represents all resources in
|
||||||
|
* the specified apiGroups.
|
||||||
|
* '*/foo' represents the subresource 'foo' for all resources in
|
||||||
|
* the specified apiGroups.
|
||||||
|
*/
|
||||||
|
String[] resources;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ResourceNames is an optional white list of names that the rule applies to. An empty set
|
||||||
|
* means that everything is allowed.
|
||||||
|
*/
|
||||||
|
String[] resourceNames;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NonResourceURLs is a set of partial urls that a user should have access to.
|
||||||
|
* *s are allowed, but only as the full, final step in the path
|
||||||
|
* If an action is not a resource API request, then the URL is split on '/' and is checked
|
||||||
|
* against the NonResourceURLs to look for a match.
|
||||||
|
* Since non-resource URLs are not namespaced, this field is only applicable for
|
||||||
|
* ClusterRoles referenced from a ClusterRoleBinding.
|
||||||
|
* Rules can either apply to API resources (such as "pods" or "secrets") or non-resource
|
||||||
|
* URL paths (such as "/api"), but not both.
|
||||||
|
*/
|
||||||
|
String[] nonResourceURLs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* about who the rule applies to or which namespace the rule applies to.
|
||||||
|
*/
|
||||||
|
String[] verbs;
|
||||||
|
|
||||||
|
public PolicyRule() {
|
||||||
|
this(null, null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PolicyRule(String[] apiGroups, String[] resources, String[] resourceNames,
|
||||||
|
String[] nonResourceURLs, String[] verbs) {
|
||||||
|
this.apiGroups = nullElseEmpty(apiGroups);
|
||||||
|
this.resources = nullElseEmpty(resources);
|
||||||
|
this.resourceNames = nullElseEmpty(resourceNames);
|
||||||
|
this.nonResourceURLs = nullElseEmpty(nonResourceURLs);
|
||||||
|
this.verbs = nullElseEmpty(verbs);
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] nullElseEmpty(String... items) {
|
||||||
|
if (items == null) {
|
||||||
|
return new String[] {};
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
String[] apiGroups;
|
||||||
|
String[] resources;
|
||||||
|
String[] resourceNames;
|
||||||
|
String[] nonResourceURLs;
|
||||||
|
String[] verbs;
|
||||||
|
|
||||||
|
public Builder apiGroups(String... apiGroups) {
|
||||||
|
this.apiGroups = apiGroups;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder resources(String... resources) {
|
||||||
|
this.resources = resources;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder resourceNames(String... resourceNames) {
|
||||||
|
this.resourceNames = resourceNames;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder nonResourceURLs(String... nonResourceURLs) {
|
||||||
|
this.nonResourceURLs = nonResourceURLs;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder verbs(String... verbs) {
|
||||||
|
this.verbs = verbs;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PolicyRule build() {
|
||||||
|
return new PolicyRule(apiGroups, resources, resourceNames, nonResourceURLs, verbs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
package run.halo.app.core.extension;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
import run.halo.app.extension.AbstractExtension;
|
||||||
|
import run.halo.app.extension.GVK;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RoleBinding references a role, but does not contain it.
|
||||||
|
* It can reference a Role in the global.
|
||||||
|
* It adds who information via Subjects.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
@GVK(group = "",
|
||||||
|
version = "v1alpha1",
|
||||||
|
kind = "RoleBinding",
|
||||||
|
plural = "rolebindings",
|
||||||
|
singular = "rolebinding")
|
||||||
|
public class RoleBinding extends AbstractExtension {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subjects holds references to the objects the role applies to.
|
||||||
|
*/
|
||||||
|
List<Subject> subjects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RoleRef can reference a Role in the current namespace or a ClusterRole in the global
|
||||||
|
* namespace.
|
||||||
|
* If the RoleRef cannot be resolved, the Authorizer must return an error.
|
||||||
|
*/
|
||||||
|
RoleRef roleRef;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RoleRef contains information that points to the role being used.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class RoleRef {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kind is the type of resource being referenced.
|
||||||
|
*/
|
||||||
|
String kind;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name is the name of resource being referenced.
|
||||||
|
*/
|
||||||
|
String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APIGroup is the group for the resource being referenced.
|
||||||
|
*/
|
||||||
|
String apiGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class Subject {
|
||||||
|
/**
|
||||||
|
* Kind of object being referenced. Values defined by this API group are "User", "Group",
|
||||||
|
* and "ServiceAccount".
|
||||||
|
* If the Authorizer does not recognize the kind value, the Authorizer should report
|
||||||
|
* an error.
|
||||||
|
*/
|
||||||
|
String kind;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the object being referenced.
|
||||||
|
*/
|
||||||
|
String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APIGroup holds the API group of the referenced subject.
|
||||||
|
* Defaults to "" for ServiceAccount subjects.
|
||||||
|
* Defaults to "rbac.authorization.halo.run" for User and Group subjects.
|
||||||
|
*/
|
||||||
|
String apiGroup;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
package run.halo.app.core.extension;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The extension represents user details of Halo.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@GVK(group = "",
|
||||||
|
version = "v1alpha1",
|
||||||
|
kind = "User",
|
||||||
|
singular = "user",
|
||||||
|
plural = "users")
|
||||||
|
public class User extends AbstractExtension {
|
||||||
|
|
||||||
|
@Schema(required = true)
|
||||||
|
private UserSpec spec;
|
||||||
|
|
||||||
|
private UserStatus status;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class UserSpec {
|
||||||
|
|
||||||
|
@Schema(required = true)
|
||||||
|
private String displayName;
|
||||||
|
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
@Schema(required = true)
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
private String bio;
|
||||||
|
|
||||||
|
private Instant registeredAt;
|
||||||
|
|
||||||
|
private Boolean twoFactorAuthEnabled;
|
||||||
|
|
||||||
|
private Boolean disabled;
|
||||||
|
|
||||||
|
private Integer loginHistoryLimit;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class UserStatus {
|
||||||
|
|
||||||
|
private Instant lastLoginAt;
|
||||||
|
|
||||||
|
private List<LoginHistory> loginHistories;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class LoginHistory {
|
||||||
|
|
||||||
|
@Schema(required = true)
|
||||||
|
private Instant loginAt;
|
||||||
|
|
||||||
|
@Schema(required = true)
|
||||||
|
private String sourceIp;
|
||||||
|
|
||||||
|
@Schema(required = true)
|
||||||
|
private String userAgent;
|
||||||
|
|
||||||
|
@Schema(required = true)
|
||||||
|
private Boolean successful;
|
||||||
|
|
||||||
|
private String reason;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package run.halo.app.security.authorization;
|
package run.halo.app.core.extension.service;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -20,7 +20,7 @@ import org.springframework.security.web.authentication.AnonymousAuthenticationFi
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class DefaultRoleBindingLister implements RoleBindingLister {
|
public class DefaultRoleBindingService implements RoleBindingService {
|
||||||
private static final String SCOPE_AUTHORITY_PREFIX = "SCOPE_";
|
private static final String SCOPE_AUTHORITY_PREFIX = "SCOPE_";
|
||||||
private static final String ROLE_AUTHORITY_PREFIX = "ROLE_";
|
private static final String ROLE_AUTHORITY_PREFIX = "ROLE_";
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
package run.halo.app.core.extension.service;
|
||||||
|
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
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.extension.ExtensionClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class DefaultRoleService implements RoleService {
|
||||||
|
|
||||||
|
private final ExtensionClient extensionClient;
|
||||||
|
|
||||||
|
public DefaultRoleService(ExtensionClient extensionClient) {
|
||||||
|
this.extensionClient = extensionClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public Role getRole(@NonNull String name) {
|
||||||
|
return extensionClient.fetch(Role.class, name).orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flux<RoleRef> listRoleRefs(Subject subject) {
|
||||||
|
return Flux.fromIterable(extensionClient.list(RoleBinding.class,
|
||||||
|
binding -> binding.getSubjects().contains(subject),
|
||||||
|
null))
|
||||||
|
.map(RoleBinding::getRoleRef);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package run.halo.app.security.authorization;
|
package run.halo.app.core.extension.service;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -9,7 +9,8 @@ import org.springframework.security.core.GrantedAuthority;
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface RoleBindingLister {
|
public interface RoleBindingService {
|
||||||
|
|
||||||
Set<String> listBoundRoleNames(Collection<? extends GrantedAuthority> authorities);
|
Set<String> listBoundRoleNames(Collection<? extends GrantedAuthority> authorities);
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package run.halo.app.core.extension.service;
|
||||||
|
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import run.halo.app.core.extension.Role;
|
||||||
|
import run.halo.app.core.extension.RoleBinding.RoleRef;
|
||||||
|
import run.halo.app.core.extension.RoleBinding.Subject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public interface RoleService {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
Role getRole(String name);
|
||||||
|
|
||||||
|
Flux<RoleRef> listRoleRefs(Subject subject);
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package run.halo.app.core.extension.service;
|
||||||
|
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.core.extension.User;
|
||||||
|
|
||||||
|
public interface UserService {
|
||||||
|
|
||||||
|
Mono<User> getUser(String username);
|
||||||
|
|
||||||
|
Mono<Void> updatePassword(String username, String newPassword);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package run.halo.app.core.extension.service;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.core.extension.User;
|
||||||
|
import run.halo.app.extension.ExtensionClient;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class UserServiceImpl implements UserService {
|
||||||
|
|
||||||
|
private final ExtensionClient client;
|
||||||
|
|
||||||
|
public UserServiceImpl(ExtensionClient client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<User> getUser(String username) {
|
||||||
|
return Mono.justOrEmpty(client.fetch(User.class, username));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> updatePassword(String username, String newPassword) {
|
||||||
|
return getUser(username)
|
||||||
|
.doOnNext(user -> {
|
||||||
|
user.getSpec().setPassword(newPassword);
|
||||||
|
client.update(user);
|
||||||
|
})
|
||||||
|
.then();
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,10 @@ public record GroupVersionKind(String group, String version, String kind) {
|
||||||
return new GroupVersion(group, version);
|
return new GroupVersion(group, version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GroupKind groupKind() {
|
||||||
|
return new GroupKind(group, kind);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean hasGroup() {
|
public boolean hasGroup() {
|
||||||
return StringUtils.hasText(group);
|
return StringUtils.hasText(group);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package run.halo.app.extension;
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
import com.networknt.schema.JsonSchemaFactory;
|
import com.networknt.schema.JsonSchemaFactory;
|
||||||
|
@ -34,6 +35,7 @@ public class JSONExtensionConverter implements ExtensionConverter {
|
||||||
OBJECT_MAPPER = Jackson2ObjectMapperBuilder.json()
|
OBJECT_MAPPER = Jackson2ObjectMapperBuilder.json()
|
||||||
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||||
.featuresToDisable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
|
.featuresToDisable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
|
||||||
|
.serializationInclusion(JsonInclude.Include.NON_NULL)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,11 +50,6 @@ public class JSONExtensionConverter implements ExtensionConverter {
|
||||||
var scheme = schemeManager.get(gvk);
|
var scheme = schemeManager.get(gvk);
|
||||||
var storeName = ExtensionUtil.buildStoreName(scheme, extension.getMetadata().getName());
|
var storeName = ExtensionUtil.buildStoreName(scheme, extension.getMetadata().getName());
|
||||||
try {
|
try {
|
||||||
if (logger.isDebugEnabled()) {
|
|
||||||
logger.debug("JSON schema({}): {}", scheme.type(),
|
|
||||||
scheme.jsonSchema().toPrettyString());
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = OBJECT_MAPPER.writeValueAsBytes(extension);
|
var data = OBJECT_MAPPER.writeValueAsBytes(extension);
|
||||||
|
|
||||||
var validator = jsonSchemaFactory.getSchema(scheme.jsonSchema());
|
var validator = jsonSchemaFactory.getSchema(scheme.jsonSchema());
|
||||||
|
|
|
@ -4,11 +4,12 @@ import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||||
import org.springframework.context.ApplicationListener;
|
import org.springframework.context.ApplicationListener;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.stereotype.Component;
|
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.User;
|
||||||
import run.halo.app.extension.SchemeManager;
|
import run.halo.app.extension.SchemeManager;
|
||||||
import run.halo.app.plugin.Plugin;
|
import run.halo.app.plugin.Plugin;
|
||||||
import run.halo.app.security.authentication.pat.PersonalAccessToken;
|
import run.halo.app.security.authentication.pat.PersonalAccessToken;
|
||||||
import run.halo.app.security.authorization.Role;
|
|
||||||
import run.halo.app.security.authorization.RoleBinding;
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class SchemeInitializer implements ApplicationListener<ApplicationStartedEvent> {
|
public class SchemeInitializer implements ApplicationListener<ApplicationStartedEvent> {
|
||||||
|
@ -22,8 +23,9 @@ public class SchemeInitializer implements ApplicationListener<ApplicationStarted
|
||||||
@Override
|
@Override
|
||||||
public void onApplicationEvent(@NonNull ApplicationStartedEvent event) {
|
public void onApplicationEvent(@NonNull ApplicationStartedEvent event) {
|
||||||
schemeManager.register(Role.class);
|
schemeManager.register(Role.class);
|
||||||
schemeManager.register(RoleBinding.class);
|
|
||||||
schemeManager.register(PersonalAccessToken.class);
|
schemeManager.register(PersonalAccessToken.class);
|
||||||
schemeManager.register(Plugin.class);
|
schemeManager.register(Plugin.class);
|
||||||
|
schemeManager.register(RoleBinding.class);
|
||||||
|
schemeManager.register(User.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
package run.halo.app.security;
|
||||||
|
|
||||||
|
import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService;
|
||||||
|
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;
|
||||||
|
|
||||||
|
public class DefaultUserDetailService
|
||||||
|
implements ReactiveUserDetailsService, ReactiveUserDetailsPasswordService {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
private final RoleService roleService;
|
||||||
|
|
||||||
|
public DefaultUserDetailService(UserService userService, RoleService roleService) {
|
||||||
|
this.userService = userService;
|
||||||
|
this.roleService = roleService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<UserDetails> updatePassword(UserDetails user, String newPassword) {
|
||||||
|
return Mono.just(user)
|
||||||
|
.map(userDetails -> withNewPassword(user, newPassword))
|
||||||
|
.flatMap(userDetails -> userService.updatePassword(
|
||||||
|
userDetails.getUsername(),
|
||||||
|
userDetails.getPassword())
|
||||||
|
.then(Mono.just(userDetails))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<UserDetails> findByUsername(String username) {
|
||||||
|
return userService.getUser(username).flatMap(user -> {
|
||||||
|
final var userGvk =
|
||||||
|
new run.halo.app.core.extension.User().groupVersionKind();
|
||||||
|
var subject = new Subject(userGvk.kind(), username, userGvk.group());
|
||||||
|
return roleService.listRoleRefs(subject)
|
||||||
|
.filter(this::isRoleRef)
|
||||||
|
.map(RoleRef::getName)
|
||||||
|
.collectList()
|
||||||
|
.map(roleNames -> User.builder()
|
||||||
|
.username(username)
|
||||||
|
.password(user.getSpec().getPassword())
|
||||||
|
.roles(roleNames.toArray(new String[0]))
|
||||||
|
.build());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
package run.halo.app.security;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
|
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||||
|
import org.springframework.context.ApplicationListener;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import run.halo.app.core.extension.Role;
|
||||||
|
import run.halo.app.core.extension.Role.PolicyRule;
|
||||||
|
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.core.extension.User.UserSpec;
|
||||||
|
import run.halo.app.extension.ExtensionClient;
|
||||||
|
import run.halo.app.extension.Metadata;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class SuperAdminInitializer implements ApplicationListener<ApplicationStartedEvent> {
|
||||||
|
|
||||||
|
private final ExtensionClient client;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
public SuperAdminInitializer(ExtensionClient client, PasswordEncoder passwordEncoder) {
|
||||||
|
this.client = client;
|
||||||
|
this.passwordEncoder = passwordEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onApplicationEvent(ApplicationStartedEvent event) {
|
||||||
|
client.fetch(User.class, "admin").ifPresentOrElse(user -> {
|
||||||
|
// do nothing if admin has been initialized
|
||||||
|
}, () -> {
|
||||||
|
var admin = createAdmin();
|
||||||
|
var superRole = createSuperRole();
|
||||||
|
var roleBinding = bindAdminAndSuperRole(admin, superRole);
|
||||||
|
client.create(admin);
|
||||||
|
client.create(superRole);
|
||||||
|
client.create(roleBinding);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
RoleBinding bindAdminAndSuperRole(User admin, Role superRole) {
|
||||||
|
var metadata = new Metadata();
|
||||||
|
metadata.setName("admin-super-role-binding");
|
||||||
|
var roleRef = new RoleRef();
|
||||||
|
roleRef.setName(superRole.getMetadata().getName());
|
||||||
|
roleRef.setApiGroup(superRole.groupVersionKind().group());
|
||||||
|
roleRef.setKind(superRole.getKind());
|
||||||
|
|
||||||
|
var subject = new Subject();
|
||||||
|
subject.setName(admin.getMetadata().getName());
|
||||||
|
subject.setApiGroup(admin.groupVersionKind().group());
|
||||||
|
subject.setKind(admin.groupVersionKind().kind());
|
||||||
|
|
||||||
|
var roleBinding = new RoleBinding();
|
||||||
|
roleBinding.setMetadata(metadata);
|
||||||
|
roleBinding.setRoleRef(roleRef);
|
||||||
|
roleBinding.setSubjects(List.of(subject));
|
||||||
|
|
||||||
|
return roleBinding;
|
||||||
|
}
|
||||||
|
|
||||||
|
Role createSuperRole() {
|
||||||
|
var metadata = new Metadata();
|
||||||
|
metadata.setName("super-role");
|
||||||
|
|
||||||
|
var superRule = new PolicyRule.Builder()
|
||||||
|
.apiGroups("*")
|
||||||
|
.resources("*")
|
||||||
|
.nonResourceURLs("*")
|
||||||
|
.verbs("*")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
var role = new Role();
|
||||||
|
role.setMetadata(metadata);
|
||||||
|
role.setRules(List.of(superRule));
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
User createAdmin() {
|
||||||
|
var metadata = new Metadata();
|
||||||
|
metadata.setName("admin");
|
||||||
|
|
||||||
|
var spec = new UserSpec();
|
||||||
|
spec.setDisplayName("Administrator");
|
||||||
|
spec.setDisabled(false);
|
||||||
|
spec.setRegisteredAt(Instant.now());
|
||||||
|
spec.setTwoFactorAuthEnabled(false);
|
||||||
|
spec.setEmail("admin@halo.run");
|
||||||
|
// generate password
|
||||||
|
var randomPassword = RandomStringUtils.randomAlphanumeric(16);
|
||||||
|
log.info("=== Generated random password: {} for initial user: {} ===",
|
||||||
|
randomPassword, metadata.getName());
|
||||||
|
spec.setPassword(passwordEncoder.encode(randomPassword));
|
||||||
|
|
||||||
|
var user = new User();
|
||||||
|
user.setMetadata(metadata);
|
||||||
|
user.setSpec(spec);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package run.halo.app.security.authorization;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import run.halo.app.core.extension.Role;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* authorizing visitor short-circuits once allowed, and collects any resolution errors encountered.
|
* authorizing visitor short-circuits once allowed, and collects any resolution errors encountered.
|
||||||
|
@ -25,7 +26,7 @@ class AuthorizingVisitor implements RuleAccumulator {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean visit(String source, PolicyRule rule, Throwable error) {
|
public boolean visit(String source, Role.PolicyRule rule, Throwable error) {
|
||||||
if (rule != null && requestEvaluation.ruleAllows(requestAttributes, rule)) {
|
if (rule != null && requestEvaluation.ruleAllows(requestAttributes, rule)) {
|
||||||
this.allowed = true;
|
this.allowed = true;
|
||||||
this.reason = String.format("RBAC: allowed by %s", source);
|
this.reason = String.format("RBAC: allowed by %s", source);
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
package run.halo.app.security.authorization;
|
|
||||||
|
|
||||||
import org.springframework.lang.NonNull;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import run.halo.app.extension.ExtensionClient;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author guqing
|
|
||||||
* @since 2.0.0
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
public class DefaultRoleGetter implements RoleGetter {
|
|
||||||
|
|
||||||
private final ExtensionClient extensionClient;
|
|
||||||
|
|
||||||
public DefaultRoleGetter(ExtensionClient extensionClient) {
|
|
||||||
this.extensionClient = extensionClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@NonNull
|
|
||||||
public Role getRole(@NonNull String name) {
|
|
||||||
return extensionClient.fetch(Role.class, name).orElseThrow();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,6 +6,10 @@ import java.util.Set;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
import run.halo.app.core.extension.Role;
|
||||||
|
import run.halo.app.core.extension.service.DefaultRoleBindingService;
|
||||||
|
import run.halo.app.core.extension.service.RoleBindingService;
|
||||||
|
import run.halo.app.core.extension.service.RoleService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author guqing
|
* @author guqing
|
||||||
|
@ -14,12 +18,12 @@ import org.springframework.util.Assert;
|
||||||
@Data
|
@Data
|
||||||
public class DefaultRuleResolver implements AuthorizationRuleResolver {
|
public class DefaultRuleResolver implements AuthorizationRuleResolver {
|
||||||
|
|
||||||
private RoleGetter roleGetter;
|
private RoleService roleService;
|
||||||
|
|
||||||
private RoleBindingLister roleBindingLister = new DefaultRoleBindingLister();
|
private RoleBindingService roleBindingService = new DefaultRoleBindingService();
|
||||||
|
|
||||||
public DefaultRuleResolver(RoleGetter roleGetter) {
|
public DefaultRuleResolver(RoleService roleService) {
|
||||||
this.roleGetter = roleGetter;
|
this.roleService = roleService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -39,12 +43,12 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void visitRulesFor(UserDetails user, RuleAccumulator visitor) {
|
public void visitRulesFor(UserDetails user, RuleAccumulator visitor) {
|
||||||
Set<String> roleNames = roleBindingLister.listBoundRoleNames(user.getAuthorities());
|
Set<String> roleNames = roleBindingService.listBoundRoleNames(user.getAuthorities());
|
||||||
|
|
||||||
List<PolicyRule> rules = Collections.emptyList();
|
List<Role.PolicyRule> rules = Collections.emptyList();
|
||||||
for (String roleName : roleNames) {
|
for (String roleName : roleNames) {
|
||||||
try {
|
try {
|
||||||
Role role = roleGetter.getRole(roleName);
|
Role role = roleService.getRole(roleName);
|
||||||
rules = role.getRules();
|
rules = role.getRules();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (visitor.visit(null, null, e)) {
|
if (visitor.visit(null, null, e)) {
|
||||||
|
@ -53,7 +57,7 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
String source = roleBindingDescriber(roleName, user.getUsername());
|
String source = roleBindingDescriber(roleName, user.getUsername());
|
||||||
for (PolicyRule rule : rules) {
|
for (Role.PolicyRule rule : rules) {
|
||||||
if (!visitor.visit(source, rule, null)) {
|
if (!visitor.visit(source, rule, null)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -65,8 +69,8 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
|
||||||
return String.format("Binding role [%s] to [%s]", roleName, subject);
|
return String.format("Binding role [%s] to [%s]", roleName, subject);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRoleBindingLister(RoleBindingLister roleBindingLister) {
|
public void setRoleBindingService(RoleBindingService roleBindingService) {
|
||||||
Assert.notNull(roleBindingLister, "The roleBindingLister must not be null.");
|
Assert.notNull(roleBindingService, "The roleBindingLister must not be null.");
|
||||||
this.roleBindingLister = roleBindingLister;
|
this.roleBindingService = roleBindingService;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,108 +0,0 @@
|
||||||
package run.halo.app.security.authorization;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PolicyRule holds information that describes a policy rule, but does not contain information
|
|
||||||
* about whom the rule applies to or which namespace the rule applies to.
|
|
||||||
*
|
|
||||||
* @author guqing
|
|
||||||
* @since 2.0.0
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class PolicyRule {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* APIGroups is the name of the APIGroup that contains the resources.
|
|
||||||
* If multiple API groups are specified, any action requested against one of the enumerated
|
|
||||||
* resources in any API group will be allowed.
|
|
||||||
*/
|
|
||||||
String[] apiGroups;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resources is a list of resources this rule applies to. '*' represents all resources in
|
|
||||||
* the specified apiGroups.
|
|
||||||
* '*/foo' represents the subresource 'foo' for all resources in the specified apiGroups.
|
|
||||||
*/
|
|
||||||
String[] resources;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ResourceNames is an optional white list of names that the rule applies to. An empty set
|
|
||||||
* means that everything is allowed.
|
|
||||||
*/
|
|
||||||
String[] resourceNames;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NonResourceURLs is a set of partial urls that a user should have access to.
|
|
||||||
* *s are allowed, but only as the full, final step in the path
|
|
||||||
* If an action is not a resource API request, then the URL is split on '/' and is checked
|
|
||||||
* against the NonResourceURLs to look for a match.
|
|
||||||
* Since non-resource URLs are not namespaced, this field is only applicable for
|
|
||||||
* ClusterRoles referenced from a ClusterRoleBinding.
|
|
||||||
* Rules can either apply to API resources (such as "pods" or "secrets") or non-resource
|
|
||||||
* URL paths (such as "/api"), but not both.
|
|
||||||
*/
|
|
||||||
String[] nonResourceURLs;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* about who the rule applies to or which namespace the rule applies to.
|
|
||||||
*/
|
|
||||||
String[] verbs;
|
|
||||||
|
|
||||||
public PolicyRule() {
|
|
||||||
this(null, null, null, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public PolicyRule(String[] apiGroups, String[] resources, String[] resourceNames,
|
|
||||||
String[] nonResourceURLs, String[] verbs) {
|
|
||||||
this.apiGroups = nullElseEmpty(apiGroups);
|
|
||||||
this.resources = nullElseEmpty(resources);
|
|
||||||
this.resourceNames = nullElseEmpty(resourceNames);
|
|
||||||
this.nonResourceURLs = nullElseEmpty(nonResourceURLs);
|
|
||||||
this.verbs = nullElseEmpty(verbs);
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] nullElseEmpty(String... items) {
|
|
||||||
if (items == null) {
|
|
||||||
return new String[] {};
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Builder {
|
|
||||||
String[] apiGroups;
|
|
||||||
String[] resources;
|
|
||||||
String[] resourceNames;
|
|
||||||
String[] nonResourceURLs;
|
|
||||||
String[] verbs;
|
|
||||||
|
|
||||||
public Builder apiGroups(String... apiGroups) {
|
|
||||||
this.apiGroups = apiGroups;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder resources(String... resources) {
|
|
||||||
this.resources = resources;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder resourceNames(String... resourceNames) {
|
|
||||||
this.resourceNames = resourceNames;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder nonResourceURLs(String... nonResourceURLs) {
|
|
||||||
this.nonResourceURLs = nonResourceURLs;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder verbs(String... verbs) {
|
|
||||||
this.verbs = verbs;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PolicyRule build() {
|
|
||||||
return new PolicyRule(apiGroups, resources, resourceNames, nonResourceURLs, verbs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,12 +3,13 @@ package run.halo.app.security.authorization;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import run.halo.app.core.extension.Role;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author guqing
|
* @author guqing
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
public class PolicyRuleList extends LinkedList<PolicyRule> {
|
public class PolicyRuleList extends LinkedList<Role.PolicyRule> {
|
||||||
private final List<Throwable> errors = new ArrayList<>(4);
|
private final List<Throwable> errors = new ArrayList<>(4);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -4,6 +4,7 @@ import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import org.apache.commons.lang3.ArrayUtils;
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import run.halo.app.core.extension.Role;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author guqing
|
* @author guqing
|
||||||
|
@ -17,8 +18,8 @@ public class RbacRequestEvaluation {
|
||||||
String NonResourceAll = "*";
|
String NonResourceAll = "*";
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean rulesAllow(Attributes requestAttributes, List<PolicyRule> rules) {
|
public boolean rulesAllow(Attributes requestAttributes, List<Role.PolicyRule> rules) {
|
||||||
for (PolicyRule rule : rules) {
|
for (Role.PolicyRule rule : rules) {
|
||||||
if (ruleAllows(requestAttributes, rule)) {
|
if (ruleAllows(requestAttributes, rule)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -26,7 +27,7 @@ public class RbacRequestEvaluation {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean ruleAllows(Attributes requestAttributes, PolicyRule rule) {
|
protected boolean ruleAllows(Attributes requestAttributes, Role.PolicyRule rule) {
|
||||||
if (requestAttributes.isResourceRequest()) {
|
if (requestAttributes.isResourceRequest()) {
|
||||||
String combinedResource = requestAttributes.getResource();
|
String combinedResource = requestAttributes.getResource();
|
||||||
if (StringUtils.isNotBlank(requestAttributes.getSubresource())) {
|
if (StringUtils.isNotBlank(requestAttributes.getSubresource())) {
|
||||||
|
@ -43,7 +44,7 @@ public class RbacRequestEvaluation {
|
||||||
&& nonResourceURLMatches(rule, requestAttributes.getPath());
|
&& nonResourceURLMatches(rule, requestAttributes.getPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean verbMatches(PolicyRule rule, String requestedVerb) {
|
protected boolean verbMatches(Role.PolicyRule rule, String requestedVerb) {
|
||||||
for (String ruleVerb : rule.getVerbs()) {
|
for (String ruleVerb : rule.getVerbs()) {
|
||||||
if (Objects.equals(ruleVerb, WildCard.VerbAll)) {
|
if (Objects.equals(ruleVerb, WildCard.VerbAll)) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -55,7 +56,7 @@ public class RbacRequestEvaluation {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean apiGroupMatches(PolicyRule rule, String requestedGroup) {
|
protected boolean apiGroupMatches(Role.PolicyRule rule, String requestedGroup) {
|
||||||
for (String ruleGroup : rule.getApiGroups()) {
|
for (String ruleGroup : rule.getApiGroups()) {
|
||||||
if (Objects.equals(ruleGroup, WildCard.APIGroupAll)) {
|
if (Objects.equals(ruleGroup, WildCard.APIGroupAll)) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -67,8 +68,8 @@ public class RbacRequestEvaluation {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean resourceMatches(PolicyRule rule, String combinedRequestedResource,
|
protected boolean resourceMatches(Role.PolicyRule rule, String combinedRequestedResource,
|
||||||
String requestedSubresource) {
|
String requestedSubresource) {
|
||||||
for (String ruleResource : rule.getResources()) {
|
for (String ruleResource : rule.getResources()) {
|
||||||
// if everything is allowed, we match
|
// if everything is allowed, we match
|
||||||
if (Objects.equals(ruleResource, WildCard.ResourceAll)) {
|
if (Objects.equals(ruleResource, WildCard.ResourceAll)) {
|
||||||
|
@ -94,7 +95,7 @@ public class RbacRequestEvaluation {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean resourceNameMatches(PolicyRule rule, String requestedName) {
|
protected boolean resourceNameMatches(Role.PolicyRule rule, String requestedName) {
|
||||||
if (ArrayUtils.isEmpty(rule.getResourceNames())) {
|
if (ArrayUtils.isEmpty(rule.getResourceNames())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -106,7 +107,7 @@ public class RbacRequestEvaluation {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean nonResourceURLMatches(PolicyRule rule, String requestedURL) {
|
protected boolean nonResourceURLMatches(Role.PolicyRule rule, String requestedURL) {
|
||||||
for (String ruleURL : rule.getNonResourceURLs()) {
|
for (String ruleURL : rule.getNonResourceURLs()) {
|
||||||
if (Objects.equals(ruleURL, WildCard.NonResourceAll)) {
|
if (Objects.equals(ruleURL, WildCard.NonResourceAll)) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -14,6 +14,7 @@ import org.springframework.security.web.server.authorization.AuthorizationContex
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.core.extension.service.RoleService;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class RequestInfoAuthorizationManager
|
public class RequestInfoAuthorizationManager
|
||||||
|
@ -23,8 +24,8 @@ public class RequestInfoAuthorizationManager
|
||||||
|
|
||||||
private final AuthorizationRuleResolver ruleResolver;
|
private final AuthorizationRuleResolver ruleResolver;
|
||||||
|
|
||||||
public RequestInfoAuthorizationManager(RoleGetter roleGetter) {
|
public RequestInfoAuthorizationManager(RoleService roleService) {
|
||||||
this.ruleResolver = new DefaultRuleResolver(roleGetter);
|
this.ruleResolver = new DefaultRuleResolver(roleService);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
package run.halo.app.security.authorization;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author guqing
|
|
||||||
* @since 2.0.0
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
@ToString(callSuper = true)
|
|
||||||
@GVK(group = "", version = "v1alpha1", kind = "Role", plural = "roles", singular = "role")
|
|
||||||
public class Role extends AbstractExtension {
|
|
||||||
|
|
||||||
@Schema(minLength = 1)
|
|
||||||
List<PolicyRule> rules;
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
package run.halo.app.security.authorization;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RoleBinding references a role, but does not contain it.
|
|
||||||
* It can reference a Role in the global.
|
|
||||||
* It adds who information via Subjects.
|
|
||||||
*
|
|
||||||
* @author guqing
|
|
||||||
* @since 2.0.0
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
@ToString(callSuper = true)
|
|
||||||
@GVK(group = "", version = "v1alpha1", kind = "RoleBinding", plural = "rolebindings",
|
|
||||||
singular = "rolebinding")
|
|
||||||
public class RoleBinding extends AbstractExtension {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subjects holds references to the objects the role applies to.
|
|
||||||
*/
|
|
||||||
List<Subject> subjects;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RoleRef can reference a Role in the current namespace or a ClusterRole in the global
|
|
||||||
* namespace.
|
|
||||||
* If the RoleRef cannot be resolved, the Authorizer must return an error.
|
|
||||||
*/
|
|
||||||
RoleRef roleRef;
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
package run.halo.app.security.authorization;
|
|
||||||
|
|
||||||
import org.springframework.lang.NonNull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author guqing
|
|
||||||
* @since 2.0.0
|
|
||||||
*/
|
|
||||||
public interface RoleGetter {
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
Role getRole(String name);
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
package run.halo.app.security.authorization;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RoleRef contains information that points to the role being used.
|
|
||||||
*
|
|
||||||
* @author guqing
|
|
||||||
* @since 2.0.0
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class RoleRef {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Kind is the type of resource being referenced.
|
|
||||||
*/
|
|
||||||
String kind;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Name is the name of resource being referenced.
|
|
||||||
*/
|
|
||||||
String name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* APIGroup is the group for the resource being referenced.
|
|
||||||
*/
|
|
||||||
String apiGroup;
|
|
||||||
}
|
|
|
@ -1,9 +1,11 @@
|
||||||
package run.halo.app.security.authorization;
|
package run.halo.app.security.authorization;
|
||||||
|
|
||||||
|
import run.halo.app.core.extension.Role;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author guqing
|
* @author guqing
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
public interface RuleAccumulator {
|
public interface RuleAccumulator {
|
||||||
boolean visit(String source, PolicyRule rule, Throwable err);
|
boolean visit(String source, Role.PolicyRule rule, Throwable err);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
package run.halo.app.security.authorization;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author guqing
|
|
||||||
* @since 2.0.0
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class Subject {
|
|
||||||
/**
|
|
||||||
* Kind of object being referenced. Values defined by this API group are "User", "Group",
|
|
||||||
* and "ServiceAccount".
|
|
||||||
* If the Authorizer does not recognized the kind value, the Authorizer should report
|
|
||||||
* an error.
|
|
||||||
*/
|
|
||||||
String kind;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Name of the object being referenced.
|
|
||||||
*/
|
|
||||||
String name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* APIGroup holds the API group of the referenced subject.
|
|
||||||
* Defaults to "" for ServiceAccount subjects.
|
|
||||||
* Defaults to "rbac.authorization.k8s.io" for User and Group subjects.
|
|
||||||
*/
|
|
||||||
String apiGroup;
|
|
||||||
}
|
|
|
@ -18,13 +18,12 @@ import org.springframework.http.MediaType;
|
||||||
import org.springframework.security.test.context.support.WithMockUser;
|
import org.springframework.security.test.context.support.WithMockUser;
|
||||||
import org.springframework.test.annotation.DirtiesContext;
|
import org.springframework.test.annotation.DirtiesContext;
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
|
import run.halo.app.core.extension.Role;
|
||||||
|
import run.halo.app.core.extension.service.RoleService;
|
||||||
import run.halo.app.extension.FakeExtension;
|
import run.halo.app.extension.FakeExtension;
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
import run.halo.app.extension.Scheme;
|
import run.halo.app.extension.Scheme;
|
||||||
import run.halo.app.extension.SchemeManager;
|
import run.halo.app.extension.SchemeManager;
|
||||||
import run.halo.app.security.authorization.PolicyRule;
|
|
||||||
import run.halo.app.security.authorization.Role;
|
|
||||||
import run.halo.app.security.authorization.RoleGetter;
|
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
@AutoConfigureWebTestClient
|
@AutoConfigureWebTestClient
|
||||||
|
@ -38,18 +37,18 @@ class ExtensionConfigurationTest {
|
||||||
SchemeManager schemeManager;
|
SchemeManager schemeManager;
|
||||||
|
|
||||||
@MockBean
|
@MockBean
|
||||||
RoleGetter roleGetter;
|
RoleService roleService;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
// disable authorization
|
// disable authorization
|
||||||
var rule = new PolicyRule();
|
var rule = new Role.PolicyRule();
|
||||||
rule.setApiGroups(new String[] {"*"});
|
rule.setApiGroups(new String[] {"*"});
|
||||||
rule.setResources(new String[] {"*"});
|
rule.setResources(new String[] {"*"});
|
||||||
rule.setVerbs(new String[] {"*"});
|
rule.setVerbs(new String[] {"*"});
|
||||||
var role = new Role();
|
var role = new Role();
|
||||||
role.setRules(List.of(rule));
|
role.setRules(List.of(rule));
|
||||||
when(roleGetter.getRole(anyString())).thenReturn(role);
|
when(roleService.getRole(anyString())).thenReturn(role);
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
package run.halo.app.core.extension.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.argThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
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 reactor.test.StepVerifier;
|
||||||
|
import run.halo.app.core.extension.User;
|
||||||
|
import run.halo.app.extension.ExtensionClient;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class UserServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
ExtensionClient client;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
UserServiceImpl userService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldGetEmptyUserIfUserNotFoundInExtension() {
|
||||||
|
when(client.fetch(User.class, "faker")).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
StepVerifier.create(userService.getUser("faker"))
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(client, times(1)).fetch(eq(User.class), eq("faker"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldGetUserIfUserFoundInExtension() {
|
||||||
|
User fakeUser = new User();
|
||||||
|
when(client.fetch(User.class, "faker")).thenReturn(Optional.of(fakeUser));
|
||||||
|
|
||||||
|
StepVerifier.create(userService.getUser("faker"))
|
||||||
|
.assertNext(user -> assertEquals(fakeUser, user))
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(client, times(1)).fetch(eq(User.class), eq("faker"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUpdatePasswordIfUserFoundInExtension() {
|
||||||
|
User fakeUser = new User();
|
||||||
|
fakeUser.setSpec(new User.UserSpec());
|
||||||
|
|
||||||
|
when(client.fetch(User.class, "faker")).thenReturn(Optional.of(fakeUser));
|
||||||
|
|
||||||
|
StepVerifier.create(userService.updatePassword("faker", "new-fake-password"))
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(client, times(1)).fetch(eq(User.class), eq("faker"));
|
||||||
|
verify(client, times(1)).update(argThat(extension -> {
|
||||||
|
var user = (User) extension;
|
||||||
|
return "new-fake-password".equals(user.getSpec().getPassword());
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotUpdatePasswordIfUserNotFoundInExtension() {
|
||||||
|
when(client.fetch(User.class, "faker")).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
StepVerifier.create(userService.updatePassword("faker", "new-fake-password"))
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(client, times(1)).fetch(eq(User.class), eq("faker"));
|
||||||
|
verify(client, times(0)).update(any());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static run.halo.app.extension.GroupVersionKind.fromAPIVersionAndKind;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class GroupVersionKindTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFromApiVersionAndKind() {
|
||||||
|
record TestCase(String apiVersion, String kind, GroupVersionKind expected,
|
||||||
|
Class<? extends Throwable> exception) {
|
||||||
|
}
|
||||||
|
|
||||||
|
List.of(
|
||||||
|
new TestCase("v1alpha1", "Fake", new GroupVersionKind("", "v1alpha1", "Fake"), null),
|
||||||
|
new TestCase("fake.halo.run/v1alpha1", "Fake",
|
||||||
|
new GroupVersionKind("fake.halo.run", "v1alpha1", "Fake"), null),
|
||||||
|
new TestCase("", "", null, IllegalArgumentException.class),
|
||||||
|
new TestCase("", "Fake", null, IllegalArgumentException.class),
|
||||||
|
new TestCase("v1alpha1", "", null, IllegalArgumentException.class),
|
||||||
|
new TestCase("fake.halo.run/v1alpha1/v1alpha2", "Fake", null,
|
||||||
|
IllegalArgumentException.class)
|
||||||
|
).forEach(testCase -> {
|
||||||
|
if (testCase.exception != null) {
|
||||||
|
assertThrows(testCase.exception, () -> {
|
||||||
|
fromAPIVersionAndKind(testCase.apiVersion, testCase.kind);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var got = fromAPIVersionAndKind(testCase.apiVersion, testCase.kind);
|
||||||
|
assertEquals(testCase.expected, got);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testHasGroup() {
|
||||||
|
record TestCase(GroupVersionKind gvk, boolean hasGroup) {
|
||||||
|
}
|
||||||
|
|
||||||
|
List.of(
|
||||||
|
new TestCase(new GroupVersionKind("", "v1alpha1", "Fake"), false),
|
||||||
|
new TestCase(new GroupVersionKind("fake.halo.run", "v1alpha1", "Fake"), true)
|
||||||
|
).forEach(testCase -> assertEquals(testCase.hasGroup, testCase.gvk.hasGroup()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGroupKind() {
|
||||||
|
record TestCase(GroupVersionKind gvk, GroupKind gk) {
|
||||||
|
}
|
||||||
|
|
||||||
|
List.of(
|
||||||
|
new TestCase(new GroupVersionKind("", "v1alpha1", "Fake"), new GroupKind("", "Fake")),
|
||||||
|
new TestCase(new GroupVersionKind("fake.halo.run", "v1alpha1", "Fake"),
|
||||||
|
new GroupKind("fake.halo.run", "Fake"))
|
||||||
|
).forEach(testCase -> {
|
||||||
|
assertEquals(testCase.gk, testCase.gvk.groupKind());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGroupVersion() {
|
||||||
|
record TestCase(GroupVersionKind gvk, GroupVersion gv) {
|
||||||
|
}
|
||||||
|
|
||||||
|
List.of(
|
||||||
|
new TestCase(new GroupVersionKind("", "v1alpha1", "Fake"),
|
||||||
|
new GroupVersion("", "v1alpha1")),
|
||||||
|
new TestCase(new GroupVersionKind("fake.halo.run", "v1alpha1", "Fake"),
|
||||||
|
new GroupVersion("fake.halo.run", "v1alpha1"))
|
||||||
|
).forEach(testCase -> {
|
||||||
|
assertEquals(testCase.gv, testCase.gvk.groupVersion());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ import run.halo.app.extension.exception.ExtensionConvertException;
|
||||||
import run.halo.app.extension.exception.SchemaViolationException;
|
import run.halo.app.extension.exception.SchemaViolationException;
|
||||||
import run.halo.app.extension.store.ExtensionStore;
|
import run.halo.app.extension.store.ExtensionStore;
|
||||||
|
|
||||||
class JSONExtensionConverterTest {
|
class JsonExtensionConverterTest {
|
||||||
|
|
||||||
JSONExtensionConverter converter;
|
JSONExtensionConverter converter;
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ class JSONExtensionConverterTest {
|
||||||
fake.setKind("Fake");
|
fake.setKind("Fake");
|
||||||
var error = assertThrows(SchemaViolationException.class, () -> converter.convertTo(fake));
|
var error = assertThrows(SchemaViolationException.class, () -> converter.convertTo(fake));
|
||||||
assertEquals(1, error.getErrors().size());
|
assertEquals(1, error.getErrors().size());
|
||||||
assertEquals("$.metadata.name: null found, string expected",
|
assertEquals("$.metadata.name: is missing but it is required",
|
||||||
error.getErrors().iterator().next().getMessage());
|
error.getErrors().iterator().next().getMessage());
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,10 +13,9 @@ import org.springframework.boot.test.context.SpringBootTest;
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
import org.springframework.security.test.context.support.WithMockUser;
|
import org.springframework.security.test.context.support.WithMockUser;
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
|
import run.halo.app.core.extension.Role;
|
||||||
|
import run.halo.app.core.extension.service.RoleService;
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
import run.halo.app.security.authorization.DefaultRoleGetter;
|
|
||||||
import run.halo.app.security.authorization.PolicyRule;
|
|
||||||
import run.halo.app.security.authorization.Role;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author guqing
|
* @author guqing
|
||||||
|
@ -29,8 +28,9 @@ class PluginLifeCycleManagerControllerTest {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
WebTestClient webClient;
|
WebTestClient webClient;
|
||||||
|
|
||||||
@MockBean
|
@MockBean
|
||||||
DefaultRoleGetter defaultRoleGetter;
|
RoleService roleService;
|
||||||
|
|
||||||
@MockBean
|
@MockBean
|
||||||
HaloPluginManager haloPluginManager;
|
HaloPluginManager haloPluginManager;
|
||||||
|
@ -46,13 +46,13 @@ class PluginLifeCycleManagerControllerTest {
|
||||||
Metadata metadata = new Metadata();
|
Metadata metadata = new Metadata();
|
||||||
metadata.setName("test-plugin-lifecycle-role");
|
metadata.setName("test-plugin-lifecycle-role");
|
||||||
role.setMetadata(metadata);
|
role.setMetadata(metadata);
|
||||||
PolicyRule policyRule = new PolicyRule.Builder()
|
Role.PolicyRule policyRule = new Role.PolicyRule.Builder()
|
||||||
.apiGroups("plugin.halo.run")
|
.apiGroups("plugin.halo.run")
|
||||||
.resources("plugins", "plugins/startup", "plugins/stop")
|
.resources("plugins", "plugins/startup", "plugins/stop")
|
||||||
.verbs("*")
|
.verbs("*")
|
||||||
.build();
|
.build();
|
||||||
role.setRules(List.of(policyRule));
|
role.setRules(List.of(policyRule));
|
||||||
when(defaultRoleGetter.getRole("USER")).thenReturn(role);
|
when(roleService.getRole("USER")).thenReturn(role);
|
||||||
when(haloPluginManager.startPlugin(any())).thenReturn(PluginState.STARTED);
|
when(haloPluginManager.startPlugin(any())).thenReturn(PluginState.STARTED);
|
||||||
when(haloPluginManager.stopPlugin(any())).thenReturn(PluginState.STOPPED);
|
when(haloPluginManager.stopPlugin(any())).thenReturn(PluginState.STOPPED);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,192 @@
|
||||||
|
package run.halo.app.security;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
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 org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.userdetails.User;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
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;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class DefaultUserDetailServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
UserService userService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
RoleService roleService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
DefaultUserDetailService userDetailService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUpdatePasswordSuccessfully() {
|
||||||
|
var fakeUser = createFakeUserDetails();
|
||||||
|
|
||||||
|
when(userService.updatePassword("faker", "new-fake-password")).thenReturn(
|
||||||
|
Mono.just("").then()
|
||||||
|
);
|
||||||
|
|
||||||
|
var userDetailsMono = userDetailService.updatePassword(fakeUser, "new-fake-password");
|
||||||
|
|
||||||
|
StepVerifier.create(userDetailsMono)
|
||||||
|
.expectSubscription()
|
||||||
|
.assertNext(userDetails -> assertEquals("new-fake-password", userDetails.getPassword()))
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(userService, times(1)).updatePassword(eq("faker"), eq("new-fake-password"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnErrorWhenFailedToUpdatePassword() {
|
||||||
|
var fakeUser = createFakeUserDetails();
|
||||||
|
|
||||||
|
var exception = mock(RuntimeException.class);
|
||||||
|
when(userService.updatePassword("faker", "new-fake-password")).thenReturn(
|
||||||
|
Mono.error(exception)
|
||||||
|
);
|
||||||
|
|
||||||
|
var userDetailsMono = userDetailService.updatePassword(fakeUser, "new-fake-password");
|
||||||
|
|
||||||
|
StepVerifier.create(userDetailsMono)
|
||||||
|
.expectSubscription()
|
||||||
|
.expectErrorMatches(throwable -> throwable == exception)
|
||||||
|
.verify();
|
||||||
|
verify(userService, times(1)).updatePassword(eq("faker"), eq("new-fake-password"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
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));
|
||||||
|
|
||||||
|
var userDetailsMono = userDetailService.findByUsername("faker");
|
||||||
|
|
||||||
|
StepVerifier.create(userDetailsMono)
|
||||||
|
.expectSubscription()
|
||||||
|
.assertNext(gotUser -> {
|
||||||
|
assertEquals(foundUser.getMetadata().getName(), gotUser.getUsername());
|
||||||
|
assertEquals(foundUser.getSpec().getPassword(), gotUser.getPassword());
|
||||||
|
assertEquals(List.of("ROLE_fake-role"),
|
||||||
|
gotUser.getAuthorities()
|
||||||
|
.stream()
|
||||||
|
.map(GrantedAuthority::getAuthority)
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFindUserDetailsByExistingUsernameButKindOfRoleRefIsNotRole() {
|
||||||
|
var foundUser = createFakeUser();
|
||||||
|
|
||||||
|
var roleGvk = new Role().groupVersionKind();
|
||||||
|
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(0, gotUser.getAuthorities().size());
|
||||||
|
})
|
||||||
|
.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());
|
||||||
|
|
||||||
|
var userDetailsMono = userDetailService.findByUsername("faker");
|
||||||
|
|
||||||
|
StepVerifier.create(userDetailsMono)
|
||||||
|
.expectSubscription()
|
||||||
|
.assertNext(gotUser -> {
|
||||||
|
assertEquals(foundUser.getMetadata().getName(), gotUser.getUsername());
|
||||||
|
assertEquals(foundUser.getSpec().getPassword(), gotUser.getPassword());
|
||||||
|
assertEquals(0, gotUser.getAuthorities().size());
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotFindUserDetailsByNonExistingUsername() {
|
||||||
|
when(userService.getUser("non-existing-user")).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
var userDetailsMono = userDetailService.findByUsername("non-existing-user");
|
||||||
|
|
||||||
|
StepVerifier.create(userDetailsMono)
|
||||||
|
.expectSubscription()
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
UserDetails createFakeUserDetails() {
|
||||||
|
return User.builder()
|
||||||
|
.username("faker")
|
||||||
|
.password("fake-password")
|
||||||
|
.roles("fake-role")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
run.halo.app.core.extension.User createFakeUser() {
|
||||||
|
var metadata = new Metadata();
|
||||||
|
metadata.setName("faker");
|
||||||
|
|
||||||
|
var userSpec = new run.halo.app.core.extension.User.UserSpec();
|
||||||
|
userSpec.setPassword("fake-password");
|
||||||
|
|
||||||
|
var user = new run.halo.app.core.extension.User();
|
||||||
|
user.setMetadata(metadata);
|
||||||
|
user.setSpec(userSpec);
|
||||||
|
return user;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package run.halo.app.security;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.argThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.mock.mockito.SpyBean;
|
||||||
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
|
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.extension.ExtensionClient;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@AutoConfigureWebTestClient
|
||||||
|
@AutoConfigureTestDatabase
|
||||||
|
class SuperAdminInitializerTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@SpyBean
|
||||||
|
ExtensionClient client;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
WebTestClient webClient;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void checkSuperAdminInitialization() {
|
||||||
|
verify(client, times(1)).fetch(eq(User.class), eq("admin"));
|
||||||
|
verify(client, times(1)).create(argThat(extension -> {
|
||||||
|
if (extension instanceof User user) {
|
||||||
|
return "admin".equals(user.getMetadata().getName());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}));
|
||||||
|
verify(client, times(1)).create(argThat(extension -> {
|
||||||
|
if (extension instanceof Role role) {
|
||||||
|
return "super-role".equals(role.getMetadata().getName());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}));
|
||||||
|
verify(client, times(1)).create(argThat(extension -> {
|
||||||
|
if (extension instanceof RoleBinding roleBinding) {
|
||||||
|
return "admin-super-role-binding".equals(roleBinding.getMetadata().getName());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -3,16 +3,23 @@ package run.halo.app.security.authentication.jwt;
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import com.nimbusds.jwt.JWTClaimNames;
|
import com.nimbusds.jwt.JWTClaimNames;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
||||||
|
import org.springframework.security.core.userdetails.User;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
|
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
@AutoConfigureWebTestClient
|
@AutoConfigureWebTestClient
|
||||||
|
@ -21,6 +28,21 @@ class LoginTest {
|
||||||
@Autowired
|
@Autowired
|
||||||
WebTestClient webClient;
|
WebTestClient webClient;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
ReactiveUserDetailsService userDetailsService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp(@Autowired PasswordEncoder passwordEncoder) {
|
||||||
|
when(userDetailsService.findByUsername("user")).thenReturn(Mono.just(
|
||||||
|
User.builder()
|
||||||
|
.passwordEncoder(passwordEncoder::encode)
|
||||||
|
.username("user")
|
||||||
|
.password("password")
|
||||||
|
.roles("USER")
|
||||||
|
.build()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void logintWithoutLoginRequest() {
|
void logintWithoutLoginRequest() {
|
||||||
webClient.post().uri("/api/auth/token").exchange().expectStatus().isUnauthorized();
|
webClient.post().uri("/api/auth/token").exchange().expectStatus().isUnauthorized();
|
||||||
|
@ -46,6 +68,7 @@ class LoginTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loginWithInvalidCredential() {
|
void loginWithInvalidCredential() {
|
||||||
|
when(userDetailsService.findByUsername("user")).thenReturn(Mono.empty());
|
||||||
var request = new LoginAuthenticationConverter.UsernamePasswordRequest();
|
var request = new LoginAuthenticationConverter.UsernamePasswordRequest();
|
||||||
request.setUsername("user");
|
request.setUsername("user");
|
||||||
request.setPassword("invalid_password");
|
request.setPassword("invalid_password");
|
||||||
|
@ -57,7 +80,16 @@ class LoginTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loginWithValidCredential(@Autowired ReactiveJwtDecoder jwtDecoder) {
|
void loginWithValidCredential(@Autowired ReactiveJwtDecoder jwtDecoder,
|
||||||
|
@Autowired PasswordEncoder passwordEncoder) {
|
||||||
|
when(userDetailsService.findByUsername("user")).thenReturn(Mono.just(
|
||||||
|
User.builder()
|
||||||
|
.passwordEncoder(passwordEncoder::encode)
|
||||||
|
.username("user")
|
||||||
|
.password("password")
|
||||||
|
.roles("USER")
|
||||||
|
.build()
|
||||||
|
));
|
||||||
var request = new LoginAuthenticationConverter.UsernamePasswordRequest();
|
var request = new LoginAuthenticationConverter.UsernamePasswordRequest();
|
||||||
request.setUsername("user");
|
request.setUsername("user");
|
||||||
request.setPassword("password");
|
request.setPassword("password");
|
||||||
|
|
|
@ -28,6 +28,9 @@ import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.core.extension.Role;
|
||||||
|
import run.halo.app.core.extension.Role.PolicyRule;
|
||||||
|
import run.halo.app.core.extension.service.RoleService;
|
||||||
import run.halo.app.security.LoginUtils;
|
import run.halo.app.security.LoginUtils;
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
|
@ -42,7 +45,7 @@ class AuthorizationTest {
|
||||||
ReactiveUserDetailsService userDetailsService;
|
ReactiveUserDetailsService userDetailsService;
|
||||||
|
|
||||||
@MockBean
|
@MockBean
|
||||||
RoleGetter roleGetter;
|
RoleService roleService;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void accessProtectedApiWithoutSufficientRole() {
|
void accessProtectedApiWithoutSufficientRole() {
|
||||||
|
@ -67,7 +70,7 @@ class AuthorizationTest {
|
||||||
new PolicyRule.Builder().apiGroups("fake.halo.run").verbs("list").resources("posts")
|
new PolicyRule.Builder().apiGroups("fake.halo.run").verbs("list").resources("posts")
|
||||||
.build()));
|
.build()));
|
||||||
|
|
||||||
when(roleGetter.getRole(eq("post.read"))).thenReturn(role);
|
when(roleService.getRole(eq("post.read"))).thenReturn(role);
|
||||||
|
|
||||||
var token = LoginUtils.login(webClient, "user", "password").block();
|
var token = LoginUtils.login(webClient, "user", "password").block();
|
||||||
webClient.get().uri("/apis/fake.halo.run/v1/posts")
|
webClient.get().uri("/apis/fake.halo.run/v1/posts")
|
||||||
|
@ -80,7 +83,7 @@ class AuthorizationTest {
|
||||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token).exchange()
|
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token).exchange()
|
||||||
.expectStatus().isForbidden();
|
.expectStatus().isForbidden();
|
||||||
|
|
||||||
verify(roleGetter, times(2)).getRole("post.read");
|
verify(roleService, times(2)).getRole("post.read");
|
||||||
}
|
}
|
||||||
|
|
||||||
@TestConfiguration
|
@TestConfiguration
|
||||||
|
|
|
@ -10,21 +10,22 @@ import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import run.halo.app.core.extension.service.DefaultRoleBindingService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link DefaultRoleBindingLister}.
|
* Tests for {@link DefaultRoleBindingService}.
|
||||||
*
|
*
|
||||||
* @author guqing
|
* @author guqing
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
// @ExtendWith(SpringExtension.class)
|
// @ExtendWith(SpringExtension.class)
|
||||||
public class DefaultRoleBindingListerTest {
|
public class DefaultRoleBindingServiceTest {
|
||||||
|
|
||||||
private DefaultRoleBindingLister roleBindingLister;
|
private DefaultRoleBindingService roleBindingLister;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
roleBindingLister = new DefaultRoleBindingLister();
|
roleBindingLister = new DefaultRoleBindingService();
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
|
@ -7,10 +7,11 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import run.halo.app.core.extension.Role;
|
||||||
import run.halo.app.infra.utils.JsonUtils;
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link PolicyRule}.
|
* Tests for {@link Role.PolicyRule}.
|
||||||
*
|
*
|
||||||
* @author guqing
|
* @author guqing
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
|
@ -25,20 +26,20 @@ class PolicyRuleTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void constructPolicyRule() throws JsonProcessingException {
|
public void constructPolicyRule() throws JsonProcessingException {
|
||||||
PolicyRule policyRule = new PolicyRule(null, null, null, null, null);
|
Role.PolicyRule policyRule = new Role.PolicyRule(null, null, null, null, null);
|
||||||
assertThat(policyRule).isNotNull();
|
assertThat(policyRule).isNotNull();
|
||||||
JsonNode policyRuleJson = objectMapper.valueToTree(policyRule);
|
JsonNode policyRuleJson = objectMapper.valueToTree(policyRule);
|
||||||
assertThat(policyRuleJson).isEqualTo(objectMapper.readTree("""
|
assertThat(policyRuleJson).isEqualTo(objectMapper.readTree("""
|
||||||
{"apiGroups":[],"resources":[],"resourceNames":[],"nonResourceURLs":[],"verbs":[]}
|
{"apiGroups":[],"resources":[],"resourceNames":[],"nonResourceURLs":[],"verbs":[]}
|
||||||
"""));
|
"""));
|
||||||
|
|
||||||
PolicyRule policyByBuilder = new PolicyRule.Builder().build();
|
Role.PolicyRule policyByBuilder = new Role.PolicyRule.Builder().build();
|
||||||
JsonNode policyByBuilderJson = objectMapper.valueToTree(policyByBuilder);
|
JsonNode policyByBuilderJson = objectMapper.valueToTree(policyByBuilder);
|
||||||
assertThat(policyByBuilderJson).isEqualTo(objectMapper.readTree("""
|
assertThat(policyByBuilderJson).isEqualTo(objectMapper.readTree("""
|
||||||
{"apiGroups":[],"resources":[],"resourceNames":[],"nonResourceURLs":[],"verbs":[]}
|
{"apiGroups":[],"resources":[],"resourceNames":[],"nonResourceURLs":[],"verbs":[]}
|
||||||
"""));
|
"""));
|
||||||
|
|
||||||
PolicyRule policyNonNull = new PolicyRule.Builder()
|
Role.PolicyRule policyNonNull = new Role.PolicyRule.Builder()
|
||||||
.apiGroups("group")
|
.apiGroups("group")
|
||||||
.resources("resource-1", "resource-2")
|
.resources("resource-1", "resource-2")
|
||||||
.resourceNames("resourceName")
|
.resourceNames("resourceName")
|
||||||
|
|
|
@ -3,6 +3,9 @@ package run.halo.app.security.authorization;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
import static org.springframework.mock.http.server.reactive.MockServerHttpRequest.method;
|
import static org.springframework.mock.http.server.reactive.MockServerHttpRequest.method;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
@ -15,6 +18,9 @@ import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.authority.AuthorityUtils;
|
import org.springframework.security.core.authority.AuthorityUtils;
|
||||||
import org.springframework.security.core.userdetails.User;
|
import org.springframework.security.core.userdetails.User;
|
||||||
|
import run.halo.app.core.extension.Role;
|
||||||
|
import run.halo.app.core.extension.Role.PolicyRule;
|
||||||
|
import run.halo.app.core.extension.service.RoleService;
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -100,30 +106,32 @@ public class RequestInfoResolverTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void defaultRuleResolverTest() {
|
public void defaultRuleResolverTest() {
|
||||||
DefaultRuleResolver ruleResolver = new DefaultRuleResolver(name -> {
|
var roleService = mock(RoleService.class);
|
||||||
// role getter
|
var ruleResolver = new DefaultRuleResolver(roleService);
|
||||||
Role role = new Role();
|
|
||||||
List<PolicyRule> rules = List.of(
|
Role role = new Role();
|
||||||
new PolicyRule.Builder().apiGroups("").resources("posts").verbs("list", "get")
|
List<PolicyRule> rules = List.of(
|
||||||
.build(),
|
new PolicyRule.Builder().apiGroups("").resources("posts").verbs("list", "get")
|
||||||
new PolicyRule.Builder().apiGroups("").resources("categories").verbs("*").build(),
|
.build(),
|
||||||
new PolicyRule.Builder().nonResourceURLs("/healthy").verbs("get", "post", "head")
|
new PolicyRule.Builder().apiGroups("").resources("categories").verbs("*").build(),
|
||||||
.build());
|
new PolicyRule.Builder().nonResourceURLs("/healthy").verbs("get", "post", "head")
|
||||||
role.setRules(rules);
|
.build());
|
||||||
Metadata metadata = new Metadata();
|
role.setRules(rules);
|
||||||
metadata.setName("ruleReadPost");
|
Metadata metadata = new Metadata();
|
||||||
role.setMetadata(metadata);
|
metadata.setName("ruleReadPost");
|
||||||
return role;
|
role.setMetadata(metadata);
|
||||||
});
|
|
||||||
|
when(roleService.getRole(anyString())).thenReturn(role);
|
||||||
|
|
||||||
// list bound role names
|
// list bound role names
|
||||||
ruleResolver.setRoleBindingLister(
|
ruleResolver.setRoleBindingService(
|
||||||
(Collection<? extends GrantedAuthority> authorities) -> Set.of("ruleReadPost"));
|
(Collection<? extends GrantedAuthority> authorities) -> Set.of("ruleReadPost"));
|
||||||
|
|
||||||
User user = new User("admin", "123456", AuthorityUtils.createAuthorityList("ruleReadPost"));
|
User user = new User("admin", "123456", AuthorityUtils.createAuthorityList("ruleReadPost"));
|
||||||
|
|
||||||
// resolve user rules
|
// resolve user rules
|
||||||
List<PolicyRule> rules = ruleResolver.rulesFor(user);
|
List<PolicyRule> resolvedRules = ruleResolver.rulesFor(user);
|
||||||
assertThat(rules).isNotNull();
|
assertThat(resolvedRules).isNotNull();
|
||||||
|
|
||||||
RbacRequestEvaluation rbacRequestEvaluation = new RbacRequestEvaluation();
|
RbacRequestEvaluation rbacRequestEvaluation = new RbacRequestEvaluation();
|
||||||
for (RequestResolveCase requestResolveCase : getRequestResolveCases()) {
|
for (RequestResolveCase requestResolveCase : getRequestResolveCases()) {
|
||||||
|
@ -133,7 +141,7 @@ public class RequestInfoResolverTest {
|
||||||
RequestInfo requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);
|
RequestInfo requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);
|
||||||
|
|
||||||
AttributesRecord attributes = new AttributesRecord(user, requestInfo);
|
AttributesRecord attributes = new AttributesRecord(user, requestInfo);
|
||||||
boolean allowed = rbacRequestEvaluation.rulesAllow(attributes, rules);
|
boolean allowed = rbacRequestEvaluation.rulesAllow(attributes, resolvedRules);
|
||||||
assertThat(allowed).isEqualTo(requestResolveCase.expected);
|
assertThat(allowed).isEqualTo(requestResolveCase.expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue