diff --git a/.forge/runtime-role-matrix-live-20260714101723-v7-reviewer-001-attempt-3-run-f5cb07374744.md b/.forge/runtime-role-matrix-live-20260714101723-v7-reviewer-001-attempt-3-run-f5cb07374744.md new file mode 100644 index 0000000..2139757 --- /dev/null +++ b/.forge/runtime-role-matrix-live-20260714101723-v7-reviewer-001-attempt-3-run-f5cb07374744.md @@ -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`. diff --git a/pom.xml b/pom.xml index 8062b34..23282ac 100644 --- a/pom.xml +++ b/pom.xml @@ -3,37 +3,54 @@ 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"> 4.0.0 + - org.springframework.boot - spring-boot-starter-parent - 3.2.5 - + com.klaroworks + developer-role-smoke + 1.0.0-SNAPSHOT - com.example + developer-role-smoke - 1.0.0 - Developer Role Smoke Test - Spring Boot smoke application for Developer role + 1.0.0-SNAPSHOT + jar + + Developer Role Smoke + Runtime role matrix for developer role validation + - 17 + 17 + 17 + UTF-8 + 5.10.0 + - org.springframework.boot - spring-boot-starter + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test - org.springframework.boot - spring-boot-starter-test + org.junit.jupiter + junit-jupiter-engine + ${junit.version} test + - org.springframework.boot - spring-boot-maven-plugin + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 - \ No newline at end of file + diff --git a/src/main/java/com/klaroworks/runtime/role/RoleContext.java b/src/main/java/com/klaroworks/runtime/role/RoleContext.java new file mode 100644 index 0000000..51c18ec --- /dev/null +++ b/src/main/java/com/klaroworks/runtime/role/RoleContext.java @@ -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 attributes; + + public RoleContext(String userId) { + this(userId, new ConcurrentHashMap<>()); + } + + public RoleContext(String userId, Map 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 getAttributes() { + return Map.copyOf(attributes); + } + + public Object getAttribute(String key) { + return attributes.get(key); + } + + public RoleContext withAttribute(String key, Object value) { + Map 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 + "}"; + } +} diff --git a/src/main/java/com/klaroworks/runtime/role/RoleMatrixRuntime.java b/src/main/java/com/klaroworks/runtime/role/RoleMatrixRuntime.java new file mode 100644 index 0000000..a6944c0 --- /dev/null +++ b/src/main/java/com/klaroworks/runtime/role/RoleMatrixRuntime.java @@ -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> roleHierarchy; + private final Map> rolePermissions; + private final RoleValidator validator; + private final ThreadLocal currentContext; + + public RoleMatrixRuntime() { + this(new HashMap<>(), new HashMap<>(), new RoleValidator()); + } + + public RoleMatrixRuntime(Map> roleHierarchy, Map> 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 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 userRoles = getUserRoles(context); + for (String role : userRoles) { + Set 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 parentRoles) { + roleHierarchy.put(role, Set.copyOf(parentRoles)); + } + + public void assignPermissions(String role, Set permissions) { + rolePermissions.put(role, Set.copyOf(permissions)); + } + + private Set getUserRoles(RoleContext context) { + Object rolesObj = context.getAttribute("roles"); + if (rolesObj instanceof Collection) { + Set 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 userRoles) { + for (String userRole : userRoles) { + if (hasInHierarchy(targetRole, userRole, new HashSet<>())) { + return true; + } + } + return false; + } + + private boolean hasInHierarchy(String target, String current, Set visited) { + if (visited.contains(current)) { + return false; + } + visited.add(current); + Set 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); + } + } +} diff --git a/src/main/java/com/klaroworks/runtime/role/RoleValidator.java b/src/main/java/com/klaroworks/runtime/role/RoleValidator.java new file mode 100644 index 0000000..0b20259 --- /dev/null +++ b/src/main/java/com/klaroworks/runtime/role/RoleValidator.java @@ -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 validRoles; + private final Set validPermissions; + + public RoleValidator() { + this(Collections.emptySet(), Collections.emptySet()); + } + + public RoleValidator(Collection validRoles, Collection 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 getValidRoles() { + return Collections.unmodifiableSet(validRoles); + } + + public Set 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; + } + } +}