Skip to content

Commit d429eb9

Browse files
committed
#1059 add readonly access to demo user for RBAC validation
1 parent 6e19fa7 commit d429eb9

File tree

3 files changed

+67
-10
lines changed

3 files changed

+67
-10
lines changed

calm-hub/keycloak-dev/device-code-flow.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ poll_token() {
5454
#Start token polling
5555
poll_token
5656

57-
echo -e "\nPress enter to create a sample user-access details associated to finos"
57+
echo -e "\nPress enter to create a sample user-access details associated to finos namespace."
5858
read
5959
if [[ -n $ACCESS_TOKEN ]]; then
6060
curl -X POST --insecure -v "https://localhost:8443/calm/namespaces/finos/user-access" \

calm-hub/mongo/init-mongo.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@ if (db.counters.countDocuments({ _id: "flowStoreCounter" }) === 1) {
4242
print("flowStoreCounter already exists, no initialization needed");
4343
}
4444

45-
if (db.counters.countDocuments({ _id: "userAccessStoreCounter" }) === 1) {
45+
if (db.counters.countDocuments({ _id: "userAccessStoreCounter" }) === 0) {
4646
db.counters.insertOne({
4747
_id: "userAccessStoreCounter",
48-
sequence_value: 4
48+
sequence_value: 6
4949
});
5050
print("Initialized userAccessStoreCounter with sequence_value 4");
5151
} else {
@@ -2691,5 +2691,26 @@ db.userAccess.insertMany([
26912691
"permission": "write",
26922692
"namespace": "traderx",
26932693
"resourceType": "all"
2694+
},
2695+
{
2696+
"userAccessId": NumberInt(4),
2697+
"username": "demo",
2698+
"permission": "read",
2699+
"namespace": "finos",
2700+
"resourceType": "all"
2701+
},
2702+
{
2703+
"userAccessId": NumberInt(5),
2704+
"username": "demo",
2705+
"permission": "read",
2706+
"namespace": "traderx",
2707+
"resourceType": "all"
2708+
},
2709+
{
2710+
"userAccessId": NumberInt(6),
2711+
"username": "demo",
2712+
"permission": "read",
2713+
"namespace": "workshop",
2714+
"resourceType": "all"
26942715
}
26952716
]);

calm-hub/src/main/java/org/finos/calm/security/UserAccessValidator.java

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,36 @@
1212

1313
import java.util.List;
1414

15+
/**
16+
* Validates whether a user is authorized to access a particular resource based on
17+
* their assigned permissions and namespaces.
18+
*
19+
* This validator is active only when the 'secure' profile is enabled.
20+
*/
1521
@ApplicationScoped
1622
@IfBuildProfile("secure")
1723
public class UserAccessValidator {
1824

1925
private static final Logger log = LoggerFactory.getLogger(UserAccessValidator.class);
2026
private final UserAccessStore userAccessStore;
2127

28+
/**
29+
* Constructs a new UserAccessValidator with the provided UserAccessStore.
30+
*
31+
* @param userAccessStore the store used to retrieve user access permissions
32+
*/
2233
public UserAccessValidator(UserAccessStore userAccessStore) {
2334
this.userAccessStore = userAccessStore;
2435
}
2536

37+
/**
38+
* Validates whether a user has sufficient access rights to perform an action on a given resource.
39+
* If validation fails, a {@link ForbiddenException} is thrown.
40+
*
41+
* @param userRequestAttributes the request attributes including username, HTTP method, namespace, and path
42+
* @throws ForbiddenException if the user is not authorized
43+
*/
2644
public void validate(UserRequestAttributes userRequestAttributes) {
27-
2845
String action = mapHttpMethodToPermission(userRequestAttributes.requestMethod());
2946
String requestPath = userRequestAttributes.path();
3047

@@ -35,6 +52,7 @@ public void validate(UserRequestAttributes userRequestAttributes) {
3552

3653
try {
3754
List<UserAccess> userAccesses = userAccessStore.getUserAccessForUsername(userRequestAttributes.username());
55+
3856
boolean authorized = userAccesses.stream().anyMatch(userAccess -> {
3957
boolean resourceMatches = (UserAccess.ResourceType.all == userAccess.getResourceType())
4058
|| requestPath.contains(userAccess.getResourceType().name());
@@ -45,35 +63,53 @@ public void validate(UserRequestAttributes userRequestAttributes) {
4563
});
4664

4765
if (!authorized) {
48-
log.warn("Access denied for user [{}] to path [{}] with action [{}]", userRequestAttributes.username(),
49-
userRequestAttributes.path(),
50-
action);
66+
log.warn("Access denied for user [{}] to path [{}] with action [{}]",
67+
userRequestAttributes.username(), userRequestAttributes.path(), action);
5168
throw new ForbiddenException("Access denied.");
5269
}
5370

5471
} catch (UserAccessNotFoundException ex) {
55-
log.error("No access permissions assigned to the user: [{}]", userRequestAttributes.username(), ex.getMessage());
72+
log.error("No access permissions assigned to the user: [{}]", userRequestAttributes.username(), ex);
5673
throw new ForbiddenException("Access denied.");
5774
}
5875
}
5976

60-
//TODO: How to protect GET - /calm/namespaces endpoint by maintaining namespace specific user grants.
77+
/**
78+
* Checks whether the request targets the default-accessible endpoint.
79+
*
80+
* @param userRequestAttributes the attributes of the incoming user request
81+
* @return true if the endpoint is accessible by default, false otherwise
82+
*/
6183
private boolean isDefaultAccessibleResource(UserRequestAttributes userRequestAttributes) {
84+
//TODO: How to protect GET - /calm/namespaces endpoint, by maintaining namespace specific user grants.
6285
return "/calm/namespaces".equals(userRequestAttributes.path()) &&
6386
"get".equalsIgnoreCase(userRequestAttributes.requestMethod().toLowerCase());
6487
}
6588

89+
/**
90+
* Maps HTTP methods to access permissions.
91+
*
92+
* @param method the HTTP method
93+
* @return "write" for modifying methods, "read" otherwise
94+
*/
6695
private String mapHttpMethodToPermission(String method) {
6796
return switch (method) {
6897
case "POST", "PUT", "PATCH", "DELETE" -> "write";
6998
default -> "read";
7099
};
71100
}
72101

102+
/**
103+
* Checks whether the user's permission level allows the requested action.
104+
*
105+
* @param userPermission the user's assigned permission
106+
* @param requestedAction the action the user is attempting to perform
107+
* @return true if the permission is sufficient, false otherwise
108+
*/
73109
private boolean permissionAllows(UserAccess.Permission userPermission, String requestedAction) {
74110
return switch (userPermission) {
75111
case write -> requestedAction.equals("write") || requestedAction.equals("read");
76112
case read -> requestedAction.equals("read");
77113
};
78114
}
79-
}
115+
}

0 commit comments

Comments
 (0)