Compare commits

..

5 commits

5 changed files with 321 additions and 16 deletions

View file

@ -0,0 +1,3 @@
# runtime-role-matrix-live-20260714101723-v7-reviewer-001-attempt-3-run-f5cb07374744
Forge 이슈 작업 브랜치 `forge/runtime-role-matrix-live-20260714101723-v7-reviewer-001-attempt-3-run-f5cb07374744`.

49
pom.xml
View file

@ -3,36 +3,53 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<groupId>com.klaroworks</groupId>
<artifactId>developer-role-smoke</artifactId>
<version>1.0.0</version>
<name>Developer Role Smoke Test</name>
<description>Spring Boot smoke application for Developer role</description>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>developer-role-smoke</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Developer Role Smoke</name>
<description>Runtime role matrix for developer role validation</description>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.10.0</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
</plugin>
</plugins>
</build>

View file

@ -0,0 +1,59 @@
package com.klaroworks.runtime.role;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* Context holder for role evaluation containing user identity and runtime attributes.
*/
public final class RoleContext {
private final String userId;
private final Map<String, Object> attributes;
public RoleContext(String userId) {
this(userId, new ConcurrentHashMap<>());
}
public RoleContext(String userId, Map<String, Object> attributes) {
this.userId = Objects.requireNonNull(userId, "userId must not be null");
this.attributes = new ConcurrentHashMap<>(Objects.requireNonNull(attributes, "attributes must not be null"));
}
public String getUserId() {
return userId;
}
public Map<String, Object> getAttributes() {
return Map.copyOf(attributes);
}
public Object getAttribute(String key) {
return attributes.get(key);
}
public RoleContext withAttribute(String key, Object value) {
Map<String, Object> newAttrs = new ConcurrentHashMap<>(attributes);
newAttrs.put(key, value);
return new RoleContext(userId, newAttrs);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RoleContext that = (RoleContext) o;
return Objects.equals(userId, that.userId) && Objects.equals(attributes, that.attributes);
}
@Override
public int hashCode() {
return Objects.hash(userId, attributes);
}
@Override
public String toString() {
return "RoleContext{userId='" + userId + "', attributes=" + attributes + "}";
}
}

View file

@ -0,0 +1,132 @@
package com.klaroworks.runtime.role;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Runtime engine for evaluating role-based access and permissions.
*/
public final class RoleMatrixRuntime {
private final Map<String, Set<String>> roleHierarchy;
private final Map<String, Set<String>> rolePermissions;
private final RoleValidator validator;
private final ThreadLocal<RoleContext> currentContext;
public RoleMatrixRuntime() {
this(new HashMap<>(), new HashMap<>(), new RoleValidator());
}
public RoleMatrixRuntime(Map<String, Set<String>> roleHierarchy, Map<String, Set<String>> rolePermissions, RoleValidator validator) {
this.roleHierarchy = new ConcurrentHashMap<>(roleHierarchy);
this.rolePermissions = new ConcurrentHashMap<>(rolePermissions);
this.validator = validator != null ? validator : new RoleValidator();
this.currentContext = ThreadLocal.withInitial(() -> null);
}
public boolean hasRole(RoleContext context, String role) {
if (context == null || role == null) {
return false;
}
Set<String> userRoles = getUserRoles(context);
return userRoles.contains(role) || hasRoleInHierarchy(role, userRoles);
}
public boolean hasPermission(RoleContext context, String permission) {
if (context == null || permission == null) {
return false;
}
Set<String> userRoles = getUserRoles(context);
for (String role : userRoles) {
Set<String> perms = rolePermissions.get(role);
if (perms != null && perms.contains(permission)) {
return true;
}
}
return false;
}
public boolean evaluateRole(RoleContext context, String roleExpression) {
if (context == null || roleExpression == null) {
return false;
}
String[] parts = roleExpression.split("\\|");
for (String part : parts) {
String trimmed = part.trim();
if (trimmed.startsWith("!") && hasRole(context, trimmed.substring(1))) {
return false;
} else if (hasRole(context, trimmed)) {
return true;
}
}
return false;
}
public void setContext(RoleContext context) {
currentContext.set(context);
}
public RoleContext getContext() {
return currentContext.get();
}
public void clearContext() {
currentContext.remove();
}
public void defineRole(String role, Set<String> parentRoles) {
roleHierarchy.put(role, Set.copyOf(parentRoles));
}
public void assignPermissions(String role, Set<String> permissions) {
rolePermissions.put(role, Set.copyOf(permissions));
}
private Set<String> getUserRoles(RoleContext context) {
Object rolesObj = context.getAttribute("roles");
if (rolesObj instanceof Collection) {
Set<String> roles = new HashSet<>();
for (Object r : (Collection<?>) rolesObj) {
if (r != null) {
roles.add(r.toString());
}
}
return roles;
}
return Collections.emptySet();
}
private boolean hasRoleInHierarchy(String targetRole, Set<String> userRoles) {
for (String userRole : userRoles) {
if (hasInHierarchy(targetRole, userRole, new HashSet<>())) {
return true;
}
}
return false;
}
private boolean hasInHierarchy(String target, String current, Set<String> visited) {
if (visited.contains(current)) {
return false;
}
visited.add(current);
Set<String> parents = roleHierarchy.get(current);
if (parents != null) {
if (parents.contains(target)) {
return true;
}
for (String parent : parents) {
if (hasInHierarchy(target, parent, visited)) {
return true;
}
}
}
return false;
}
public static final class Nested {
public static boolean evaluate(RoleMatrixRuntime runtime, RoleContext context, String expression) {
return runtime.evaluateRole(context, expression);
}
}
}

View file

@ -0,0 +1,94 @@
package com.klaroworks.runtime.role;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* Validates role context and permission requests against configured rules.
*/
public final class RoleValidator {
private final Set<String> validRoles;
private final Set<String> validPermissions;
public RoleValidator() {
this(Collections.emptySet(), Collections.emptySet());
}
public RoleValidator(Collection<String> validRoles, Collection<String> validPermissions) {
this.validRoles = Set.copyOf(validRoles);
this.validPermissions = Set.copyOf(validPermissions);
}
public boolean isValidRole(String role) {
if (role == null || role.isBlank()) {
return false;
}
return validRoles.isEmpty() || validRoles.contains(role);
}
public boolean isValidPermission(String permission) {
if (permission == null || permission.isBlank()) {
return false;
}
return validPermissions.isEmpty() || validPermissions.contains(permission);
}
public ValidationResult validateContext(RoleContext context) {
if (context == null) {
return new ValidationResult(false, "Context must not be null");
}
if (context.getUserId() == null || context.getUserId().isBlank()) {
return new ValidationResult(false, "User ID must not be blank");
}
return new ValidationResult(true, null);
}
public ValidationResult validateRole(String role) {
if (role == null || role.isBlank()) {
return new ValidationResult(false, "Role must not be blank");
}
if (!validRoles.isEmpty() && !validRoles.contains(role)) {
return new ValidationResult(false, "Unknown role: " + role);
}
return new ValidationResult(true, null);
}
public ValidationResult validatePermission(String permission) {
if (permission == null || permission.isBlank()) {
return new ValidationResult(false, "Permission must not be blank");
}
if (!validPermissions.isEmpty() && !validPermissions.contains(permission)) {
return new ValidationResult(false, "Unknown permission: " + permission);
}
return new ValidationResult(true, null);
}
public Set<String> getValidRoles() {
return Collections.unmodifiableSet(validRoles);
}
public Set<String> getValidPermissions() {
return Collections.unmodifiableSet(validPermissions);
}
public static final class ValidationResult {
private final boolean valid;
private final String errorMessage;
public ValidationResult(boolean valid, String errorMessage) {
this.valid = valid;
this.errorMessage = errorMessage;
}
public boolean isValid() {
return valid;
}
public String getErrorMessage() {
return errorMessage;
}
}
}