Skip to content

Commit 0b213f5

Browse files
committed
#1059 add user-access validator service to validate the user grants
1 parent cd0ee42 commit 0b213f5

File tree

7 files changed

+170
-44
lines changed

7 files changed

+170
-44
lines changed

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,11 @@ if [[ -n $ACCESS_TOKEN ]]; then
6060
curl -X POST --insecure -v "https://localhost:8443/calm/namespaces/finos/user-access" \
6161
-H "Content-Type: application/json" \
6262
-H "Authorization: Bearer $ACCESS_TOKEN" \
63-
-d '{ "namespace": "finos", "resource": "*", "role": "write", "username": "demo" }'
63+
-d '{ "namespace": "finos", "resourceType": "namespaces", "permission": "read", "username": "demo" }'
6464

6565
echo -e "\nPress enter to get list of user-access details"
6666
read
6767
curl --insecure -v "https://localhost:8443/calm/namespaces/finos/user-access" \
6868
-H "Content-Type: application/json" \
6969
-H "Authorization: Bearer $ACCESS_TOKEN"
7070
fi
71-
72-
#Reference: https://github.com/keycloak/keycloak-community/blob/main/design/oauth2-device-authorization-grant.md

calm-hub/mongo/init-mongo.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2668,4 +2668,28 @@ db.architectures.insertMany([
26682668
}
26692669
}]
26702670
}
2671+
]);
2672+
2673+
db.userAccess.insertMany([
2674+
{
2675+
"id": NumberInt(1),
2676+
"username": "demo_admin",
2677+
"permission": "write",
2678+
"namespace": "finos",
2679+
"resourceType": "all"
2680+
},
2681+
{
2682+
"id": NumberInt(2),
2683+
"username": "demo_admin",
2684+
"permission": "write",
2685+
"namespace": "workshop",
2686+
"resourceType": "patterns"
2687+
},
2688+
{
2689+
"id": NumberInt(3),
2690+
"username": "demo_admin",
2691+
"permission": "write",
2692+
"namespace": "traderx",
2693+
"resourceType": "all"
2694+
}
26712695
]);

calm-hub/src/main/java/org/finos/calm/domain/UserAccess.java

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,28 @@
99
import java.util.Objects;
1010

1111
/**
12-
* Represents a CalmHub user role on resources associated to a namespace.
12+
* Represents a CalmHub user access on resources associated to a namespace.
1313
*/
1414
public class UserAccess {
1515

16+
public enum Permission {
17+
read,
18+
write
19+
}
20+
21+
public enum ResourceType {
22+
patterns,
23+
flows,
24+
adrs,
25+
architectures,
26+
namespaces,
27+
all
28+
}
29+
1630
private String username;
17-
private String role;
31+
private Permission permission;
1832
private String namespace;
19-
private String resource;
33+
private ResourceType resourceType;
2034
private int id;
2135

2236
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@@ -27,19 +41,19 @@ public class UserAccess {
2741
@JsonSerialize(using = LocalDateTimeSerializer.class)
2842
private LocalDateTime updateDateTime;
2943

30-
public UserAccess(String username, String role, String namespace, String resource, int id) {
44+
public UserAccess(String username, Permission permission, String namespace, ResourceType resourceType, int id) {
3145
this.username = username;
32-
this.role = role;
46+
this.permission = permission;
3347
this.namespace = namespace;
34-
this.resource = resource;
48+
this.resourceType = resourceType;
3549
this.id = id;
3650
}
3751

38-
public UserAccess(String username, String role, String namespace, String resource) {
52+
public UserAccess(String username, Permission permission, String namespace, ResourceType resourceType) {
3953
this.username = username;
40-
this.role = role;
54+
this.permission = permission;
4155
this.namespace = namespace;
42-
this.resource = resource;
56+
this.resourceType = resourceType;
4357
}
4458

4559
public UserAccess(){
@@ -49,18 +63,18 @@ public UserAccess(){
4963
public static class UserAccessBuilder {
5064

5165
private String username;
52-
private String role;
66+
private Permission permission;
5367
private String namespace;
54-
private String resource;
68+
private ResourceType resourceType;
5569
private int id;
5670

5771
public UserAccessBuilder setUsername(String username) {
5872
this.username = username;
5973
return this;
6074
}
6175

62-
public UserAccessBuilder setRole(String role) {
63-
this.role = role;
76+
public UserAccessBuilder setPermission(Permission permission) {
77+
this.permission = permission;
6478
return this;
6579
}
6680

@@ -69,8 +83,8 @@ public UserAccessBuilder setNamespace(String namespace) {
6983
return this;
7084
}
7185

72-
public UserAccessBuilder setResource(String resource) {
73-
this.resource = resource;
86+
public UserAccessBuilder setResourceType(ResourceType resourceType) {
87+
this.resourceType = resourceType;
7488
return this;
7589
}
7690

@@ -80,24 +94,24 @@ public UserAccessBuilder setId(int id) {
8094
}
8195

8296
public UserAccess build(){
83-
return new UserAccess(username, role, namespace, resource, id);
97+
return new UserAccess(username, permission, namespace, resourceType, id);
8498
}
8599
}
86100

87101
public String getUsername() {
88102
return username;
89103
}
90104

91-
public String getRole() {
92-
return role;
105+
public Permission getPermission() {
106+
return permission;
93107
}
94108

95109
public String getNamespace() {
96110
return namespace;
97111
}
98112

99-
public String getResource() {
100-
return resource;
113+
public ResourceType getResourceType() {
114+
return resourceType;
101115
}
102116

103117
public int getId() {
@@ -129,23 +143,23 @@ public boolean equals(Object o) {
129143

130144
if (id != that.id) return false;
131145
if (!Objects.equals(username, that.username)) return false;
132-
if (!Objects.equals(role, that.role)) return false;
146+
if (!Objects.equals(permission, that.permission)) return false;
133147
if (!Objects.equals(namespace, that.namespace)) return false;
134-
return Objects.equals(resource, that.resource);
148+
return Objects.equals(resourceType, that.resourceType);
135149
}
136150

137151
@Override
138152
public int hashCode() {
139-
return Objects.hash(username, role, namespace, resource, id);
153+
return Objects.hash(username, permission, namespace, resourceType, id);
140154
}
141155

142156
@Override
143157
public String toString() {
144158
return "UserAccess{" +
145159
"username='" + username + '\'' +
146-
", role='" + role + '\'' +
160+
", permission='" + permission + '\'' +
147161
", namespace='" + namespace + '\'' +
148-
", resource='" + resource + '\'' +
162+
", resourceType='" + resourceType + '\'' +
149163
", id=" + id +
150164
'}';
151165
}
@@ -154,16 +168,16 @@ public void setUsername(String username) {
154168
this.username = username;
155169
}
156170

157-
public void setRole(String role) {
158-
this.role = role;
171+
public void setPermission(Permission permission) {
172+
this.permission = permission;
159173
}
160174

161175
public void setNamespace(String namespace) {
162176
this.namespace = namespace;
163177
}
164178

165-
public void setResource(String resource) {
166-
this.resource = resource;
179+
public void setResourceType(ResourceType resourceType) {
180+
this.resourceType = resourceType;
167181
}
168182

169183
public void setId(int id) {

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,14 @@ public class AccessControlFilter implements ContainerRequestFilter {
4545

4646
private final JsonWebToken jwt;
4747
private final ResourceInfo resourceInfo;
48+
private final UserAccessValidator userAccessValidator;
4849
private final Logger logger = LoggerFactory.getLogger(AccessControlFilter.class);
4950

50-
public AccessControlFilter(JsonWebToken jwt, ResourceInfo resourceInfo) {
51+
public AccessControlFilter(JsonWebToken jwt, ResourceInfo resourceInfo,
52+
UserAccessValidator userAccessValidator) {
5153
this.jwt = jwt;
5254
this.resourceInfo = resourceInfo;
55+
this.userAccessValidator = userAccessValidator;
5356
}
5457

5558
@Override
@@ -68,6 +71,16 @@ public void filter(ContainerRequestContext requestContext) {
6871
.entity("Forbidden: JWT does not have required scopes.")
6972
.build());
7073
}
74+
75+
String requestMethod = requestContext.getMethod();
76+
String username = jwt.getClaim("preferred_username");
77+
String path = requestContext.getUriInfo().getPath();
78+
String namespace = requestContext.getUriInfo().getPathParameters().getFirst("namespace");
79+
80+
UserRequestAttributes userRequestAttributes = new UserRequestAttributes(requestMethod,
81+
username, path, namespace);
82+
logger.info("User request attributes: {}", userRequestAttributes);
83+
userAccessValidator.validate(userRequestAttributes);
7184
}
7285

7386
/**
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package org.finos.calm.security;
2+
3+
import io.netty.util.internal.StringUtil;
4+
import io.quarkus.arc.profile.IfBuildProfile;
5+
import jakarta.enterprise.context.ApplicationScoped;
6+
import jakarta.ws.rs.ForbiddenException;
7+
import org.finos.calm.domain.UserAccess;
8+
import org.finos.calm.domain.exception.NamespaceNotFoundException;
9+
import org.finos.calm.domain.exception.UserAccessNotFoundException;
10+
import org.finos.calm.store.UserAccessStore;
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
13+
14+
import java.util.List;
15+
16+
@ApplicationScoped
17+
@IfBuildProfile("secure")
18+
public class UserAccessValidator {
19+
20+
private static final Logger log = LoggerFactory.getLogger(UserAccessValidator.class);
21+
private final UserAccessStore userAccessStore;
22+
23+
public UserAccessValidator(UserAccessStore userAccessStore) {
24+
this.userAccessStore = userAccessStore;
25+
}
26+
27+
public void validate(UserRequestAttributes userRequestAttributes) {
28+
29+
String action = mapHttpMethodToPermission(userRequestAttributes.requestMethod());
30+
String requestPath = userRequestAttributes.path();
31+
try {
32+
List<UserAccess> userAccesses = userAccessStore.getUserAccessForUsername(userRequestAttributes.username());
33+
34+
boolean authorized = userAccesses.stream().anyMatch(userAccess -> {
35+
boolean resourceMatches = (UserAccess.ResourceType.all == userAccess.getResourceType())
36+
|| requestPath.contains(userAccess.getResourceType().name());
37+
boolean permissionSufficient = permissionAllows(userAccess.getPermission(), action);
38+
boolean namespaceMatches = !StringUtil.isNullOrEmpty(userRequestAttributes.namespace()) && userRequestAttributes.namespace().equals(userAccess.getNamespace());
39+
return resourceMatches && permissionSufficient && namespaceMatches;
40+
});
41+
42+
if (!authorized && !isDefaultAccessibleResource(userRequestAttributes)) {
43+
log.warn("Access denied for user [{}] to path [{}] with action [{}]", userRequestAttributes.username(),
44+
userRequestAttributes.path(),
45+
action);
46+
throw new ForbiddenException("Access denied.");
47+
}
48+
49+
} catch (NamespaceNotFoundException | UserAccessNotFoundException e) {
50+
log.error("Access check failed for user [{}]", userRequestAttributes.username(), e.getMessage());
51+
throw new ForbiddenException("Access denied.");
52+
}
53+
}
54+
55+
private boolean isDefaultAccessibleResource(UserRequestAttributes userRequestAttributes) {
56+
return "/calm/namespaces".equals(userRequestAttributes.path()) &&
57+
"get".equalsIgnoreCase(userRequestAttributes.requestMethod().toLowerCase());
58+
}
59+
60+
private String mapHttpMethodToPermission(String method) {
61+
return switch (method) {
62+
case "POST", "PUT", "PATCH", "DELETE" -> "write";
63+
default -> "read";
64+
};
65+
}
66+
67+
private boolean permissionAllows(UserAccess.Permission userPermission, String requestedAction) {
68+
return switch (userPermission) {
69+
case write -> requestedAction.equals("write") || requestedAction.equals("read");
70+
case read -> requestedAction.equals("read");
71+
};
72+
}
73+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package org.finos.calm.security;
2+
3+
public record UserRequestAttributes(String requestMethod, String username, String path, String namespace) {
4+
5+
}

calm-hub/src/main/java/org/finos/calm/store/mongo/MongoUserAccessStore.java

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,29 +36,29 @@ public MongoUserAccessStore(MongoClient mongoClient, MongoNamespaceStore namespa
3636
public UserAccess createUserAccessForNamespace(UserAccess userAccess)
3737
throws NamespaceNotFoundException, JsonParseException {
3838

39-
log.info("User-access details: {}",userAccess);
39+
log.info("User-access details: {}", userAccess);
4040
if (!namespaceStore.namespaceExists(userAccess.getNamespace())) {
4141
throw new NamespaceNotFoundException();
4242
}
4343

4444
int id = counterStore.getNextUserAccessSequenceValue();
4545
Document userAccessDoc = new Document("username", userAccess.getUsername())
46-
.append("role", userAccess.getRole())
46+
.append("permission", userAccess.getPermission().name())
4747
.append("namespace", userAccess.getNamespace())
48-
.append("resource", userAccess.getResource())
48+
.append("resourceType", userAccess.getResourceType().name())
4949
.append("createdAt", userAccess.getCreationDateTime())
5050
.append("lastUpdated", userAccess.getUpdateDateTime())
5151
.append("id", id);
5252

5353
userAccessCollection.insertOne(userAccessDoc);
5454
log.info("UserAccess has been created for namespace: {}, resource: {}, role: {}, username: {}",
55-
userAccess.getNamespace(), userAccess.getResource(), userAccess.getRole(), userAccess.getUsername());
55+
userAccess.getNamespace(), userAccess.getResourceType(), userAccess.getPermission(), userAccess.getUsername());
5656

5757
UserAccess persistedUserAccess = new UserAccess.UserAccessBuilder()
5858
.setId(id)
59-
.setResource(userAccess.getResource())
59+
.setResourceType(userAccess.getResourceType())
6060
.setNamespace(userAccess.getNamespace())
61-
.setRole(userAccess.getRole())
61+
.setPermission(userAccess.getPermission())
6262
.setUsername(userAccess.getUsername())
6363
.build();
6464
return persistedUserAccess;
@@ -77,12 +77,11 @@ public List<UserAccess> getUserAccessForUsername(String username)
7777

7878
UserAccess userAccess = new UserAccess.UserAccessBuilder()
7979
.setUsername(doc.getString("username"))
80-
.setRole(doc.getString("role"))
80+
.setPermission(UserAccess.Permission.valueOf(doc.getString("permission")))
8181
.setNamespace(namespace)
82-
.setResource(doc.getString("resource"))
82+
.setResourceType(UserAccess.ResourceType.valueOf(doc.getString("resourceType")))
8383
.setId(doc.getInteger("id"))
8484
.build();
85-
8685
userAccessList.add(userAccess);
8786
}
8887

@@ -103,9 +102,9 @@ public List<UserAccess> getUserAccessForNamespace(String namespace)
103102
for (Document doc : userAccessCollection.find(Filters.eq("namespace", namespace))) {
104103
UserAccess userAccess = new UserAccess.UserAccessBuilder()
105104
.setUsername(doc.getString("username"))
106-
.setRole(doc.getString("role"))
105+
.setPermission(UserAccess.Permission.valueOf(doc.getString("permission")))
107106
.setNamespace(namespace)
108-
.setResource(doc.getString("resource"))
107+
.setResourceType(UserAccess.ResourceType.valueOf(doc.getString("resourceType")))
109108
.setId(doc.getInteger("id"))
110109
.build();
111110
userAccessList.add(userAccess);

0 commit comments

Comments
 (0)