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.web.server.SecurityWebFiltersOrder;
|
||||
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.User;
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
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.csrf.CookieServerCsrfTokenRepository;
|
||||
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.security.DefaultUserDetailService;
|
||||
import run.halo.app.security.authentication.jwt.LoginAuthenticationFilter;
|
||||
import run.halo.app.security.authentication.jwt.LoginAuthenticationManager;
|
||||
import run.halo.app.security.authorization.RequestInfoAuthorizationManager;
|
||||
import run.halo.app.security.authorization.RoleGetter;
|
||||
|
||||
/**
|
||||
* Security configuration for WebFlux.
|
||||
|
@ -52,15 +52,18 @@ public class WebServerSecurityConfig {
|
|||
SecurityWebFilterChain apiFilterChain(ServerHttpSecurity http,
|
||||
ServerCodecConfigurer codec,
|
||||
ServerResponse.Context context,
|
||||
RoleGetter roleGetter) {
|
||||
UserService userService,
|
||||
RoleService roleService) {
|
||||
http.csrf().disable()
|
||||
.securityMatcher(pathMatchers("/api/**", "/apis/**"))
|
||||
.authorizeExchange(exchanges ->
|
||||
exchanges.anyExchange().access(new RequestInfoAuthorizationManager(roleGetter)))
|
||||
exchanges.anyExchange().access(new RequestInfoAuthorizationManager(roleService)))
|
||||
// for reuse the JWT authentication
|
||||
.oauth2ResourceServer().jwt();
|
||||
|
||||
var loginManager = new LoginAuthenticationManager(userDetailsService(), passwordEncoder());
|
||||
var loginManager = new LoginAuthenticationManager(
|
||||
userDetailsService(userService, roleService),
|
||||
passwordEncoder());
|
||||
var loginFilter = new LoginAuthenticationFilter(loginManager,
|
||||
codec,
|
||||
jwtEncoder(),
|
||||
|
@ -92,15 +95,9 @@ public class WebServerSecurityConfig {
|
|||
}
|
||||
|
||||
@Bean
|
||||
ReactiveUserDetailsService userDetailsService() {
|
||||
//TODO Implement details service when User Extension is ready.
|
||||
return new MapReactiveUserDetailsService(
|
||||
// for test
|
||||
User.withDefaultPasswordEncoder().username("user").password("password").roles("USER")
|
||||
.build(),
|
||||
// for test
|
||||
User.withDefaultPasswordEncoder().username("admin").password("password").roles("ADMIN")
|
||||
.build());
|
||||
ReactiveUserDetailsService userDetailsService(UserService userService,
|
||||
RoleService roleService) {
|
||||
return new DefaultUserDetailService(userService, roleService);
|
||||
}
|
||||
|
||||
@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.Set;
|
||||
|
@ -20,7 +20,7 @@ import org.springframework.security.web.authentication.AnonymousAuthenticationFi
|
|||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class DefaultRoleBindingLister implements RoleBindingLister {
|
||||
public class DefaultRoleBindingService implements RoleBindingService {
|
||||
private static final String SCOPE_AUTHORITY_PREFIX = "SCOPE_";
|
||||
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.Set;
|
||||
|
@ -9,7 +9,8 @@ import org.springframework.security.core.GrantedAuthority;
|
|||
* @since 2.0.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface RoleBindingLister {
|
||||
public interface RoleBindingService {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public GroupKind groupKind() {
|
||||
return new GroupKind(group, kind);
|
||||
}
|
||||
|
||||
public boolean hasGroup() {
|
||||
return StringUtils.hasText(group);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package run.halo.app.extension;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.networknt.schema.JsonSchemaFactory;
|
||||
|
@ -34,6 +35,7 @@ public class JSONExtensionConverter implements ExtensionConverter {
|
|||
OBJECT_MAPPER = Jackson2ObjectMapperBuilder.json()
|
||||
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||
.featuresToDisable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
|
||||
.serializationInclusion(JsonInclude.Include.NON_NULL)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
@ -48,11 +50,6 @@ public class JSONExtensionConverter implements ExtensionConverter {
|
|||
var scheme = schemeManager.get(gvk);
|
||||
var storeName = ExtensionUtil.buildStoreName(scheme, extension.getMetadata().getName());
|
||||
try {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("JSON schema({}): {}", scheme.type(),
|
||||
scheme.jsonSchema().toPrettyString());
|
||||
}
|
||||
|
||||
var data = OBJECT_MAPPER.writeValueAsBytes(extension);
|
||||
|
||||
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.lang.NonNull;
|
||||
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.plugin.Plugin;
|
||||
import run.halo.app.security.authentication.pat.PersonalAccessToken;
|
||||
import run.halo.app.security.authorization.Role;
|
||||
import run.halo.app.security.authorization.RoleBinding;
|
||||
|
||||
@Component
|
||||
public class SchemeInitializer implements ApplicationListener<ApplicationStartedEvent> {
|
||||
|
@ -22,8 +23,9 @@ public class SchemeInitializer implements ApplicationListener<ApplicationStarted
|
|||
@Override
|
||||
public void onApplicationEvent(@NonNull ApplicationStartedEvent event) {
|
||||
schemeManager.register(Role.class);
|
||||
schemeManager.register(RoleBinding.class);
|
||||
schemeManager.register(PersonalAccessToken.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.List;
|
||||
import run.halo.app.core.extension.Role;
|
||||
|
||||
/**
|
||||
* authorizing visitor short-circuits once allowed, and collects any resolution errors encountered.
|
||||
|
@ -25,7 +26,7 @@ class AuthorizingVisitor implements RuleAccumulator {
|
|||
}
|
||||
|
||||
@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)) {
|
||||
this.allowed = true;
|
||||
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 org.springframework.security.core.userdetails.UserDetails;
|
||||
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
|
||||
|
@ -14,12 +18,12 @@ import org.springframework.util.Assert;
|
|||
@Data
|
||||
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) {
|
||||
this.roleGetter = roleGetter;
|
||||
public DefaultRuleResolver(RoleService roleService) {
|
||||
this.roleService = roleService;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -39,12 +43,12 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
|
|||
|
||||
@Override
|
||||
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) {
|
||||
try {
|
||||
Role role = roleGetter.getRole(roleName);
|
||||
Role role = roleService.getRole(roleName);
|
||||
rules = role.getRules();
|
||||
} catch (Exception e) {
|
||||
if (visitor.visit(null, null, e)) {
|
||||
|
@ -53,7 +57,7 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
|
|||
}
|
||||
|
||||
String source = roleBindingDescriber(roleName, user.getUsername());
|
||||
for (PolicyRule rule : rules) {
|
||||
for (Role.PolicyRule rule : rules) {
|
||||
if (!visitor.visit(source, rule, null)) {
|
||||
return;
|
||||
}
|
||||
|
@ -65,8 +69,8 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
|
|||
return String.format("Binding role [%s] to [%s]", roleName, subject);
|
||||
}
|
||||
|
||||
public void setRoleBindingLister(RoleBindingLister roleBindingLister) {
|
||||
Assert.notNull(roleBindingLister, "The roleBindingLister must not be null.");
|
||||
this.roleBindingLister = roleBindingLister;
|
||||
public void setRoleBindingService(RoleBindingService roleBindingService) {
|
||||
Assert.notNull(roleBindingService, "The roleBindingLister must not be null.");
|
||||
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.LinkedList;
|
||||
import java.util.List;
|
||||
import run.halo.app.core.extension.Role;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
* @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);
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,6 +4,7 @@ import java.util.List;
|
|||
import java.util.Objects;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import run.halo.app.core.extension.Role;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
|
@ -17,8 +18,8 @@ public class RbacRequestEvaluation {
|
|||
String NonResourceAll = "*";
|
||||
}
|
||||
|
||||
public boolean rulesAllow(Attributes requestAttributes, List<PolicyRule> rules) {
|
||||
for (PolicyRule rule : rules) {
|
||||
public boolean rulesAllow(Attributes requestAttributes, List<Role.PolicyRule> rules) {
|
||||
for (Role.PolicyRule rule : rules) {
|
||||
if (ruleAllows(requestAttributes, rule)) {
|
||||
return true;
|
||||
}
|
||||
|
@ -26,7 +27,7 @@ public class RbacRequestEvaluation {
|
|||
return false;
|
||||
}
|
||||
|
||||
protected boolean ruleAllows(Attributes requestAttributes, PolicyRule rule) {
|
||||
protected boolean ruleAllows(Attributes requestAttributes, Role.PolicyRule rule) {
|
||||
if (requestAttributes.isResourceRequest()) {
|
||||
String combinedResource = requestAttributes.getResource();
|
||||
if (StringUtils.isNotBlank(requestAttributes.getSubresource())) {
|
||||
|
@ -43,7 +44,7 @@ public class RbacRequestEvaluation {
|
|||
&& nonResourceURLMatches(rule, requestAttributes.getPath());
|
||||
}
|
||||
|
||||
protected boolean verbMatches(PolicyRule rule, String requestedVerb) {
|
||||
protected boolean verbMatches(Role.PolicyRule rule, String requestedVerb) {
|
||||
for (String ruleVerb : rule.getVerbs()) {
|
||||
if (Objects.equals(ruleVerb, WildCard.VerbAll)) {
|
||||
return true;
|
||||
|
@ -55,7 +56,7 @@ public class RbacRequestEvaluation {
|
|||
return false;
|
||||
}
|
||||
|
||||
protected boolean apiGroupMatches(PolicyRule rule, String requestedGroup) {
|
||||
protected boolean apiGroupMatches(Role.PolicyRule rule, String requestedGroup) {
|
||||
for (String ruleGroup : rule.getApiGroups()) {
|
||||
if (Objects.equals(ruleGroup, WildCard.APIGroupAll)) {
|
||||
return true;
|
||||
|
@ -67,8 +68,8 @@ public class RbacRequestEvaluation {
|
|||
return false;
|
||||
}
|
||||
|
||||
protected boolean resourceMatches(PolicyRule rule, String combinedRequestedResource,
|
||||
String requestedSubresource) {
|
||||
protected boolean resourceMatches(Role.PolicyRule rule, String combinedRequestedResource,
|
||||
String requestedSubresource) {
|
||||
for (String ruleResource : rule.getResources()) {
|
||||
// if everything is allowed, we match
|
||||
if (Objects.equals(ruleResource, WildCard.ResourceAll)) {
|
||||
|
@ -94,7 +95,7 @@ public class RbacRequestEvaluation {
|
|||
return false;
|
||||
}
|
||||
|
||||
protected boolean resourceNameMatches(PolicyRule rule, String requestedName) {
|
||||
protected boolean resourceNameMatches(Role.PolicyRule rule, String requestedName) {
|
||||
if (ArrayUtils.isEmpty(rule.getResourceNames())) {
|
||||
return true;
|
||||
}
|
||||
|
@ -106,7 +107,7 @@ public class RbacRequestEvaluation {
|
|||
return false;
|
||||
}
|
||||
|
||||
protected boolean nonResourceURLMatches(PolicyRule rule, String requestedURL) {
|
||||
protected boolean nonResourceURLMatches(Role.PolicyRule rule, String requestedURL) {
|
||||
for (String ruleURL : rule.getNonResourceURLs()) {
|
||||
if (Objects.equals(ruleURL, WildCard.NonResourceAll)) {
|
||||
return true;
|
||||
|
|
|
@ -14,6 +14,7 @@ import org.springframework.security.web.server.authorization.AuthorizationContex
|
|||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.service.RoleService;
|
||||
|
||||
@Slf4j
|
||||
public class RequestInfoAuthorizationManager
|
||||
|
@ -23,8 +24,8 @@ public class RequestInfoAuthorizationManager
|
|||
|
||||
private final AuthorizationRuleResolver ruleResolver;
|
||||
|
||||
public RequestInfoAuthorizationManager(RoleGetter roleGetter) {
|
||||
this.ruleResolver = new DefaultRuleResolver(roleGetter);
|
||||
public RequestInfoAuthorizationManager(RoleService roleService) {
|
||||
this.ruleResolver = new DefaultRuleResolver(roleService);
|
||||
}
|
||||
|
||||
@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;
|
||||
|
||||
import run.halo.app.core.extension.Role;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
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.test.annotation.DirtiesContext;
|
||||
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.Metadata;
|
||||
import run.halo.app.extension.Scheme;
|
||||
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
|
||||
@AutoConfigureWebTestClient
|
||||
|
@ -38,18 +37,18 @@ class ExtensionConfigurationTest {
|
|||
SchemeManager schemeManager;
|
||||
|
||||
@MockBean
|
||||
RoleGetter roleGetter;
|
||||
RoleService roleService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// disable authorization
|
||||
var rule = new PolicyRule();
|
||||
var rule = new Role.PolicyRule();
|
||||
rule.setApiGroups(new String[] {"*"});
|
||||
rule.setResources(new String[] {"*"});
|
||||
rule.setVerbs(new String[] {"*"});
|
||||
var role = new Role();
|
||||
role.setRules(List.of(rule));
|
||||
when(roleGetter.getRole(anyString())).thenReturn(role);
|
||||
when(roleService.getRole(anyString())).thenReturn(role);
|
||||
}
|
||||
|
||||
@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.store.ExtensionStore;
|
||||
|
||||
class JSONExtensionConverterTest {
|
||||
class JsonExtensionConverterTest {
|
||||
|
||||
JSONExtensionConverter converter;
|
||||
|
||||
|
@ -71,7 +71,7 @@ class JSONExtensionConverterTest {
|
|||
fake.setKind("Fake");
|
||||
var error = assertThrows(SchemaViolationException.class, () -> converter.convertTo(fake));
|
||||
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());
|
||||
}
|
||||
|
|
@ -13,10 +13,9 @@ import org.springframework.boot.test.context.SpringBootTest;
|
|||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
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.security.authorization.DefaultRoleGetter;
|
||||
import run.halo.app.security.authorization.PolicyRule;
|
||||
import run.halo.app.security.authorization.Role;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
|
@ -29,8 +28,9 @@ class PluginLifeCycleManagerControllerTest {
|
|||
|
||||
@Autowired
|
||||
WebTestClient webClient;
|
||||
|
||||
@MockBean
|
||||
DefaultRoleGetter defaultRoleGetter;
|
||||
RoleService roleService;
|
||||
|
||||
@MockBean
|
||||
HaloPluginManager haloPluginManager;
|
||||
|
@ -46,13 +46,13 @@ class PluginLifeCycleManagerControllerTest {
|
|||
Metadata metadata = new Metadata();
|
||||
metadata.setName("test-plugin-lifecycle-role");
|
||||
role.setMetadata(metadata);
|
||||
PolicyRule policyRule = new PolicyRule.Builder()
|
||||
Role.PolicyRule policyRule = new Role.PolicyRule.Builder()
|
||||
.apiGroups("plugin.halo.run")
|
||||
.resources("plugins", "plugins/startup", "plugins/stop")
|
||||
.verbs("*")
|
||||
.build();
|
||||
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.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.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.nimbusds.jwt.JWTClaimNames;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
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.test.web.reactive.server.WebTestClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureWebTestClient
|
||||
|
@ -21,6 +28,21 @@ class LoginTest {
|
|||
@Autowired
|
||||
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
|
||||
void logintWithoutLoginRequest() {
|
||||
webClient.post().uri("/api/auth/token").exchange().expectStatus().isUnauthorized();
|
||||
|
@ -46,6 +68,7 @@ class LoginTest {
|
|||
|
||||
@Test
|
||||
void loginWithInvalidCredential() {
|
||||
when(userDetailsService.findByUsername("user")).thenReturn(Mono.empty());
|
||||
var request = new LoginAuthenticationConverter.UsernamePasswordRequest();
|
||||
request.setUsername("user");
|
||||
request.setPassword("invalid_password");
|
||||
|
@ -57,7 +80,16 @@ class LoginTest {
|
|||
}
|
||||
|
||||
@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();
|
||||
request.setUsername("user");
|
||||
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.ServerResponse;
|
||||
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;
|
||||
|
||||
@SpringBootTest
|
||||
|
@ -42,7 +45,7 @@ class AuthorizationTest {
|
|||
ReactiveUserDetailsService userDetailsService;
|
||||
|
||||
@MockBean
|
||||
RoleGetter roleGetter;
|
||||
RoleService roleService;
|
||||
|
||||
@Test
|
||||
void accessProtectedApiWithoutSufficientRole() {
|
||||
|
@ -67,7 +70,7 @@ class AuthorizationTest {
|
|||
new PolicyRule.Builder().apiGroups("fake.halo.run").verbs("list").resources("posts")
|
||||
.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();
|
||||
webClient.get().uri("/apis/fake.halo.run/v1/posts")
|
||||
|
@ -80,7 +83,7 @@ class AuthorizationTest {
|
|||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token).exchange()
|
||||
.expectStatus().isForbidden();
|
||||
|
||||
verify(roleGetter, times(2)).getRole("post.read");
|
||||
verify(roleService, times(2)).getRole("post.read");
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
|
|
|
@ -10,21 +10,22 @@ import org.junit.jupiter.api.BeforeEach;
|
|||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
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
|
||||
* @since 2.0.0
|
||||
*/
|
||||
// @ExtendWith(SpringExtension.class)
|
||||
public class DefaultRoleBindingListerTest {
|
||||
public class DefaultRoleBindingServiceTest {
|
||||
|
||||
private DefaultRoleBindingLister roleBindingLister;
|
||||
private DefaultRoleBindingService roleBindingLister;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
roleBindingLister = new DefaultRoleBindingLister();
|
||||
roleBindingLister = new DefaultRoleBindingService();
|
||||
}
|
||||
|
||||
@AfterEach
|
|
@ -7,10 +7,11 @@ import com.fasterxml.jackson.databind.JsonNode;
|
|||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import run.halo.app.core.extension.Role;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* Tests for {@link PolicyRule}.
|
||||
* Tests for {@link Role.PolicyRule}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
|
@ -25,20 +26,20 @@ class PolicyRuleTest {
|
|||
|
||||
@Test
|
||||
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();
|
||||
JsonNode policyRuleJson = objectMapper.valueToTree(policyRule);
|
||||
assertThat(policyRuleJson).isEqualTo(objectMapper.readTree("""
|
||||
{"apiGroups":[],"resources":[],"resourceNames":[],"nonResourceURLs":[],"verbs":[]}
|
||||
"""));
|
||||
|
||||
PolicyRule policyByBuilder = new PolicyRule.Builder().build();
|
||||
Role.PolicyRule policyByBuilder = new Role.PolicyRule.Builder().build();
|
||||
JsonNode policyByBuilderJson = objectMapper.valueToTree(policyByBuilder);
|
||||
assertThat(policyByBuilderJson).isEqualTo(objectMapper.readTree("""
|
||||
{"apiGroups":[],"resources":[],"resourceNames":[],"nonResourceURLs":[],"verbs":[]}
|
||||
"""));
|
||||
|
||||
PolicyRule policyNonNull = new PolicyRule.Builder()
|
||||
Role.PolicyRule policyNonNull = new Role.PolicyRule.Builder()
|
||||
.apiGroups("group")
|
||||
.resources("resource-1", "resource-2")
|
||||
.resourceNames("resourceName")
|
||||
|
|
|
@ -3,6 +3,9 @@ package run.halo.app.security.authorization;
|
|||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
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 java.util.Collection;
|
||||
|
@ -15,6 +18,9 @@ import org.springframework.http.HttpMethod;
|
|||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
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;
|
||||
|
||||
/**
|
||||
|
@ -100,30 +106,32 @@ public class RequestInfoResolverTest {
|
|||
|
||||
@Test
|
||||
public void defaultRuleResolverTest() {
|
||||
DefaultRuleResolver ruleResolver = new DefaultRuleResolver(name -> {
|
||||
// role getter
|
||||
Role role = new Role();
|
||||
List<PolicyRule> rules = List.of(
|
||||
new PolicyRule.Builder().apiGroups("").resources("posts").verbs("list", "get")
|
||||
.build(),
|
||||
new PolicyRule.Builder().apiGroups("").resources("categories").verbs("*").build(),
|
||||
new PolicyRule.Builder().nonResourceURLs("/healthy").verbs("get", "post", "head")
|
||||
.build());
|
||||
role.setRules(rules);
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName("ruleReadPost");
|
||||
role.setMetadata(metadata);
|
||||
return role;
|
||||
});
|
||||
var roleService = mock(RoleService.class);
|
||||
var ruleResolver = new DefaultRuleResolver(roleService);
|
||||
|
||||
Role role = new Role();
|
||||
List<PolicyRule> rules = List.of(
|
||||
new PolicyRule.Builder().apiGroups("").resources("posts").verbs("list", "get")
|
||||
.build(),
|
||||
new PolicyRule.Builder().apiGroups("").resources("categories").verbs("*").build(),
|
||||
new PolicyRule.Builder().nonResourceURLs("/healthy").verbs("get", "post", "head")
|
||||
.build());
|
||||
role.setRules(rules);
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName("ruleReadPost");
|
||||
role.setMetadata(metadata);
|
||||
|
||||
when(roleService.getRole(anyString())).thenReturn(role);
|
||||
|
||||
// list bound role names
|
||||
ruleResolver.setRoleBindingLister(
|
||||
ruleResolver.setRoleBindingService(
|
||||
(Collection<? extends GrantedAuthority> authorities) -> Set.of("ruleReadPost"));
|
||||
|
||||
User user = new User("admin", "123456", AuthorityUtils.createAuthorityList("ruleReadPost"));
|
||||
|
||||
// resolve user rules
|
||||
List<PolicyRule> rules = ruleResolver.rulesFor(user);
|
||||
assertThat(rules).isNotNull();
|
||||
List<PolicyRule> resolvedRules = ruleResolver.rulesFor(user);
|
||||
assertThat(resolvedRules).isNotNull();
|
||||
|
||||
RbacRequestEvaluation rbacRequestEvaluation = new RbacRequestEvaluation();
|
||||
for (RequestResolveCase requestResolveCase : getRequestResolveCases()) {
|
||||
|
@ -133,7 +141,7 @@ public class RequestInfoResolverTest {
|
|||
RequestInfo requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);
|
||||
|
||||
AttributesRecord attributes = new AttributesRecord(user, requestInfo);
|
||||
boolean allowed = rbacRequestEvaluation.rulesAllow(attributes, rules);
|
||||
boolean allowed = rbacRequestEvaluation.rulesAllow(attributes, resolvedRules);
|
||||
assertThat(allowed).isEqualTo(requestResolveCase.expected);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue