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;
+ }
+ }
+}