Skip to content

Commit 73a4f34

Browse files
committed
feat: refactor user lookup and resource handling with new UserFinder and ResourceFinder classes
streamline code, merged "shared-with" and "shared-by" resource endpoints into the generic endpoint. The latter is technically a breaking change, but since currently nobody uses the library, I will count this as a minor update
1 parent fd33073 commit 73a4f34

21 files changed

+501
-412
lines changed

README.md

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,15 @@ Returns a list of authorization resource definitions for the client requesting t
6969
that can be used to filter resources based on their ids.
7070

7171
Supported query parameters:
72-
* **ids** - A list of resource ids to search for, in the format `id1,id2`. If provided, the search and attributes parameters CAN NOT be used.
72+
* **ids** - A list of resource ids to search for, in the format `id1,id2`. If provided the filters "name", "owner", "type" and "uri" can not be used. (Exception owner + sharedOnly, this is allowed)
73+
* **name** - Allows for filtering the resources by their name, by default a partial search is done, set 'exactName' for exact matching
74+
* **uri** - Allows filtering the resources by the uri
75+
* **owner** - The uuid of a user will only return resources owned by the user
76+
* **sharedWith** - Allows filtering the resources to all resources shared with the user with the given uuid
77+
* **sharedOnly** - If true, only resources shared with the user will be returned. Enabling this flag MUST be accompanied by the "owner" filter, that is the user id which shared the resources.
78+
* **idsOnly** - If set, only the ids of the resources are returned
79+
* **exactName** - If set only resources that match the name-filter exactly will be returned
80+
* **type** - Allows for filtering for types of resources
7381
* **first** - The first result to return (0-based)
7482
* **max** - The maximum number of results to return (default 100)
7583

@@ -100,30 +108,6 @@ Expected body:
100108
}
101109
```
102110

103-
#### GET Resources shared with User
104-
`/realms/{realm}/hawk/resources/shared-with/{userId}`
105-
106-
Returns a list of resource ids that have been shared with the specified user. (Shared means, the resources
107-
have been specifically allowed to the user by a UMA ticket or using the "Allow Resource to User" endpoint).
108-
109-
Supported query parameters:
110-
* **first** - The first result to return (0-based)
111-
* **max** - The maximum number of results to return (default 100)
112-
113-
Required roles: `hawk-view-resource-permissions` or `hawk-manage-resource-permissions` or both.
114-
115-
#### GET Resources shared by User
116-
`/realms/{realm}/hawk/resources/shared-by/{userId}`
117-
118-
Returns a list of resource ids that have been shared by the specified user. (Shared means, the resources
119-
have been specifically allowed to the user by a UMA ticket or using the "Allow Resource to User" endpoint).
120-
121-
Supported query parameters:
122-
* **first** - The first result to return (0-based)
123-
* **max** - The maximum number of results to return (default 100)
124-
125-
Required roles: `hawk-view-resource-permissions` or `hawk-manage-resource-permissions` or both.
126-
127111
#### GET Roles
128112
`/realms/{realm}/hawk/roles`
129113

src/main/java/com/hawk/keycloak/ApiRoot.java

Lines changed: 26 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import org.keycloak.protocol.oidc.TokenManager;
1818
import org.keycloak.representations.account.UserRepresentation;
1919
import org.keycloak.representations.idm.RoleRepresentation;
20-
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
2120
import org.keycloak.representations.userprofile.config.UPConfig;
2221
import org.keycloak.services.resources.admin.AdminAuth;
2322

@@ -39,7 +38,7 @@ public ApiRoot(RequestHandlerFactory requestHandlerFactory, KeycloakSession sess
3938
@Path("users")
4039
@Produces(MediaType.APPLICATION_JSON)
4140
@NoCache
42-
public Stream<Map<String, Object>> getUsers(
41+
public Response getUsers(
4342
@Parameter(description = "A String contained in username, first or last name, or email. Default search behavior is prefix-based (e.g., foo or foo*). Use *foo* for infix search and \"foo\" for exact search.") @QueryParam("search") String search,
4443
@Parameter(description = "A query to search for custom attributes, in the format 'key1:value2 key2:value2'") @QueryParam("attributes") String attributes,
4544
@Parameter(description = "List of comma separated user ids, in the format 'id1,id2'") @QueryParam("ids") String ids,
@@ -65,34 +64,48 @@ public Stream<Map<String, Object>> getUsers(
6564
@Path("users/count")
6665
@Produces(MediaType.TEXT_PLAIN)
6766
@NoCache
68-
public String getUsersCount(
67+
public Response getUsersCount(
6968
@Parameter(description = "A String contained in username, first or last name, or email. Default search behavior is prefix-based (e.g., foo or foo*). Use *foo* for infix search and \"foo\" for exact search.") @QueryParam("search") String search,
7069
@Parameter(description = "A query to search for custom attributes, in the format 'key1:value2 key2:value2'") @QueryParam("attributes") String attributes,
7170
@Parameter(description = "If true, only users with an active session (in any client of the realm) will be counted") @QueryParam("onlineOnly") Boolean onlineOnly
7271
) {
73-
return String.valueOf(
74-
requestHandlerFactory
75-
.usersRequestHandler(authenticate())
76-
.getUserCount(
77-
search,
78-
attributes,
79-
onlineOnly
80-
)
81-
);
72+
return requestHandlerFactory
73+
.usersRequestHandler(authenticate())
74+
.getUserCount(
75+
search,
76+
attributes,
77+
onlineOnly
78+
);
8279
}
8380

8481
@GET
8582
@Path("resources")
8683
@Produces(MediaType.APPLICATION_JSON)
87-
public Stream<ResourceRepresentation> getResources(
84+
public Response getResources(
8885
@Parameter(description = "List of comma separated resource ids, in the format 'id1,id2'") @QueryParam("ids") String ids,
86+
@Parameter(description = "The uuid of a user will return only resources shared with that user") @QueryParam("sharedWith") String sharedWith,
87+
@Parameter(description = "Allows for filtering the resources by their name, by default a partial search is done, set 'exactName' for exact matching") @QueryParam("name") String name,
88+
@Parameter(description = "Allows filtering the resources by the uri") @QueryParam("uri") String uri,
89+
@Parameter(description = "The uuid of a user will only return resources owned by the user") @QueryParam("owner") String owner,
90+
@Parameter(description = "If enabled AND the owner is set, only resources shared by the owner will be returned") @QueryParam("sharedOnly") Boolean sharedOnly,
91+
@Parameter(description = "If set, only the ids of the resources are returned") @QueryParam("idsOnly") Boolean idsOnly,
92+
@Parameter(description = "If set only resources that match the name-filter exactly will be returned") @QueryParam("exactName") Boolean exactName,
93+
@Parameter(description = "Allows for filtering for types of resources") @QueryParam("type") String type,
8994
@Parameter(description = "Pagination offset") @QueryParam("first") Integer firstResult,
9095
@Parameter(description = "Maximum results size (defaults to 100)") @QueryParam("max") Integer maxResults
9196
) {
9297
return requestHandlerFactory
9398
.resourceRequestHandler(authenticate())
9499
.handleGetResourcesRequest(
95100
commaListToCollection(ids),
101+
sharedWith,
102+
name,
103+
uri,
104+
owner,
105+
type,
106+
exactName,
107+
idsOnly,
108+
sharedOnly,
96109
firstResult,
97110
maxResults
98111
);
@@ -127,32 +140,6 @@ public Response setResourceUserPermissions(
127140
);
128141
}
129142

130-
@GET
131-
@Path("resources/shared-with/{user}")
132-
@Produces(org.keycloak.utils.MediaType.APPLICATION_JSON)
133-
public Response getResourcesSharedWithUser(
134-
@Parameter(description = "The id of the user to find the resources shared with") @PathParam("user") String userId,
135-
@Parameter(description = "Pagination offset") @QueryParam("first") Integer firstResult,
136-
@Parameter(description = "Maximum results size (defaults to 100)") @QueryParam("max") Integer maxResults
137-
) {
138-
return requestHandlerFactory
139-
.sharedResourceRequestHandler(authenticate())
140-
.handleSharedWithUserRequest(userId, firstResult, maxResults);
141-
}
142-
143-
@GET
144-
@Path("resources/shared-by/{user}")
145-
@Produces(org.keycloak.utils.MediaType.APPLICATION_JSON)
146-
public Response getResourcesSharedByUser(
147-
@Parameter(description = "The id of the user to find the resources shared by") @PathParam("user") String userId,
148-
@Parameter(description = "Pagination offset") @QueryParam("first") Integer firstResult,
149-
@Parameter(description = "Maximum results size (defaults to 100)") @QueryParam("max") Integer maxResults
150-
) {
151-
return requestHandlerFactory
152-
.sharedResourceRequestHandler(authenticate())
153-
.handleSharedByUserRequest(userId, firstResult, maxResults);
154-
}
155-
156143
@GET
157144
@Path("roles")
158145
@Produces(MediaType.APPLICATION_JSON)

src/main/java/com/hawk/keycloak/RequestHandlerFactory.java

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,20 @@
55
import com.hawk.keycloak.profiles.ProfileDataRequestHandler;
66
import com.hawk.keycloak.profiles.ProfileStructureRequestHandler;
77
import com.hawk.keycloak.resources.ResourceRequestHandler;
8-
import com.hawk.keycloak.resources.SharedResourceRequestHandler;
8+
import com.hawk.keycloak.resources.lookup.ResourceFinder;
99
import com.hawk.keycloak.resources.lookup.ResourceUserFinder;
1010
import com.hawk.keycloak.resources.lookup.SharedResourceFinder;
1111
import com.hawk.keycloak.resources.service.ResourcePermissionSetter;
1212
import com.hawk.keycloak.roles.RolesRequestHandler;
13+
import com.hawk.keycloak.users.lookup.UserFinder;
1314
import com.hawk.keycloak.users.lookup.UserInfoGenerator;
1415
import com.hawk.keycloak.users.UsersRequestHandler;
1516
import com.hawk.keycloak.util.ConnectionInfoRequestHandler;
1617
import lombok.RequiredArgsConstructor;
1718
import org.keycloak.authorization.AuthorizationProvider;
1819
import org.keycloak.authorization.model.ResourceServer;
1920
import org.keycloak.authorization.store.PermissionTicketStore;
21+
import org.keycloak.authorization.store.ResourceStore;
2022
import org.keycloak.events.EventBuilder;
2123
import org.keycloak.models.KeycloakSession;
2224
import org.keycloak.services.resources.admin.AdminEventBuilder;
@@ -30,7 +32,21 @@ public CacheBusterRequestHandler cacheBusterRequestHandler(HawkPermissionEvaluat
3032
}
3133

3234
public UsersRequestHandler usersRequestHandler(HawkPermissionEvaluator auth) {
33-
return new UsersRequestHandler(session, auth, userInfoGenerator());
35+
return new UsersRequestHandler(
36+
new UserFinder(
37+
session.users(),
38+
session.sessions(),
39+
session.getContext().getRealm()
40+
),
41+
session,
42+
auth,
43+
new UserInfoGenerator(
44+
session,
45+
session.getContext().getClient(),
46+
session.getContext().getUri(),
47+
session.getContext().getConnection()
48+
)
49+
);
3450
}
3551

3652
public ConnectionInfoRequestHandler connectionInfoRequestHandler(HawkPermissionEvaluator auth) {
@@ -55,41 +71,33 @@ public ResourceRequestHandler resourceRequestHandler(HawkPermissionEvaluator aut
5571
session.getContext().getClient()
5672
);
5773
PermissionTicketStore ticketStore = authorizationProvider.getStoreFactory().getPermissionTicketStore();
74+
ResourceStore resourceStore = authorizationProvider.getStoreFactory().getResourceStore();
5875
return new ResourceRequestHandler(
5976
new ResourceUserFinder(ticketStore),
6077
auth,
61-
session,
62-
authorizationProvider.getStoreFactory().getResourceStore(),
78+
resourceStore,
6379
resourceServer,
6480
new ResourcePermissionSetter(
6581
ticketStore,
6682
resourceServer,
6783
authorizationProvider.getStoreFactory().getScopeStore(),
68-
session,
69-
adminEventBuilder(auth)
84+
adminEventBuilder(auth),
85+
session.getContext().getUri()
7086
),
71-
authorizationProvider
72-
);
73-
}
74-
75-
public SharedResourceRequestHandler sharedResourceRequestHandler(HawkPermissionEvaluator auth) {
76-
AuthorizationProvider provider = session.getProvider(AuthorizationProvider.class);
77-
return new SharedResourceRequestHandler(
78-
new SharedResourceFinder(
79-
provider.getStoreFactory().getPermissionTicketStore()
87+
authorizationProvider,
88+
new ResourceFinder(
89+
session,
90+
resourceStore,
91+
authorizationProvider.getStoreFactory().getResourceServerStore().findByClient(
92+
session.getContext().getClient()
93+
),
94+
new SharedResourceFinder(ticketStore)
8095
),
81-
auth,
82-
session,
83-
provider.getStoreFactory().getResourceServerStore().findByClient(
84-
session.getContext().getClient()
85-
)
96+
session.getContext().getRealm(),
97+
session.users()
8698
);
8799
}
88100

89-
private UserInfoGenerator userInfoGenerator() {
90-
return new UserInfoGenerator(session);
91-
}
92-
93101
private AdminEventBuilder adminEventBuilder(HawkPermissionEvaluator auth) {
94102
return new AdminEventBuilder(
95103
session.getContext().getRealm(),
Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.hawk.keycloak.resources;
22

33
import com.hawk.keycloak.auth.HawkPermissionEvaluator;
4+
import com.hawk.keycloak.resources.lookup.ResourceFinder;
45
import com.hawk.keycloak.resources.lookup.ResourceUserFinder;
56
import com.hawk.keycloak.resources.model.UserResourcePermission;
67
import com.hawk.keycloak.resources.service.ResourcePermissionSetter;
@@ -11,33 +12,31 @@
1112
import org.keycloak.authorization.model.Resource;
1213
import org.keycloak.authorization.model.ResourceServer;
1314
import org.keycloak.authorization.store.ResourceStore;
14-
import org.keycloak.models.Constants;
15-
import org.keycloak.models.KeycloakSession;
16-
import org.keycloak.models.UserModel;
15+
import org.keycloak.models.*;
1716
import org.keycloak.models.utils.ModelToRepresentation;
18-
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
1917

2018
import java.util.Collection;
2119
import java.util.List;
22-
import java.util.Map;
2320
import java.util.stream.Stream;
2421

2522
@RequiredArgsConstructor
2623
public class ResourceRequestHandler {
2724
private final ResourceUserFinder resourceUserFinder;
2825
private final HawkPermissionEvaluator auth;
29-
private final KeycloakSession session;
3026
private final ResourceStore resourceStore;
3127
private final ResourceServer resourceServer;
3228
private final ResourcePermissionSetter permissionSetter;
3329
private final AuthorizationProvider authorization;
30+
private final ResourceFinder resourceFinder;
31+
private final RealmModel realm;
32+
private final UserProvider userProvider;
3433

3534
public Collection<UserResourcePermission> handleUsersOfResourceRequest(String resourceId) {
3635
auth.requireViewResourcePermissions();
3736

3837
Resource resource = resourceStore.findById(resourceServer, resourceId);
3938

40-
if(resource == null){
39+
if (resource == null) {
4140
throw new NotFoundException("Resource not found");
4241
}
4342

@@ -47,13 +46,13 @@ public Collection<UserResourcePermission> handleUsersOfResourceRequest(String re
4746
public Response handleSetUserPermissionsRequest(String userId, String resourceId, List<String> scopes) {
4847
auth.requireManageResourcePermissions();
4948

50-
UserModel user = session.users().getUserById(session.getContext().getRealm(), userId);
51-
if(user == null){
49+
UserModel user = userProvider.getUserById(realm, userId);
50+
if (user == null) {
5251
throw new NotFoundException("User not found");
5352
}
5453

5554
Resource resource = resourceStore.findById(resourceServer, resourceId);
56-
if(resource == null){
55+
if (resource == null) {
5756
throw new NotFoundException("Resource not found");
5857
}
5958

@@ -62,30 +61,26 @@ public Response handleSetUserPermissionsRequest(String userId, String resourceId
6261
return Response.noContent().build();
6362
}
6463

65-
public Stream<ResourceRepresentation> handleGetResourcesRequest(
64+
public Response handleGetResourcesRequest(
6665
List<String> ids,
66+
String sharedWith,
67+
String name,
68+
String uri,
69+
String owner,
70+
String type,
71+
Boolean exactName,
72+
Boolean idsOnly,
73+
Boolean sharedOnly,
6774
Integer firstResult,
6875
Integer maxResult
6976
) {
7077
auth.requireViewResourcePermissions();
78+
Stream<Resource> resources = resourceFinder.findResources(ids, sharedWith, name, uri, owner, type, exactName, sharedOnly, firstResult, maxResult);
7179

72-
Stream<Resource> resources;
73-
74-
if(ids == null || ids.isEmpty()){
75-
resources = resourceStore.find(
76-
this.resourceServer,
77-
Map.of(),
78-
firstResult != null ? firstResult : -1,
79-
maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS
80-
).stream();
81-
} else {
82-
resources = ids.stream().map(id -> resourceStore.findById(resourceServer, id));
80+
if(idsOnly != null && idsOnly) {
81+
return Response.ok(resources.map(Resource::getId)).build();
8382
}
8483

85-
return resources
86-
.distinct()
87-
.skip(firstResult != null ? firstResult : 0)
88-
.limit(maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS)
89-
.map(r -> ModelToRepresentation.toRepresentation(r, resourceServer, authorization, true));
84+
return Response.ok(resources.map(r -> ModelToRepresentation.toRepresentation(r, resourceServer, authorization, true))).build();
9085
}
9186
}

0 commit comments

Comments
 (0)