Skip to content

Commit c388a80

Browse files
author
Asaf Cohen
authored
Merge pull request #17 from permitio/asaf/cto-121-java-sdk-more-checks
Advanced permission checks: bulk check, check in all tenants, get user permissions
2 parents fe3ce2c + 9fee5b5 commit c388a80

10 files changed

Lines changed: 577 additions & 8 deletions

File tree

src/main/java/io/permit/sdk/Permit.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,14 @@
44
import com.google.gson.GsonBuilder;
55
import io.permit.sdk.api.ApiClient;
66
import io.permit.sdk.api.ElementsApi;
7-
import io.permit.sdk.enforcement.Enforcer;
8-
import io.permit.sdk.enforcement.IEnforcerApi;
9-
import io.permit.sdk.enforcement.Resource;
10-
import io.permit.sdk.enforcement.User;
7+
import io.permit.sdk.enforcement.*;
118
import io.permit.sdk.util.Context;
129

1310
import org.slf4j.Logger;
1411
import org.slf4j.LoggerFactory;
1512

1613
import java.io.IOException;
14+
import java.util.List;
1715

1816
/**
1917
* The {@code Permit} class represents the main entry point for interacting with the Permit.io SDK.
@@ -137,4 +135,24 @@ public boolean checkUrl(User user, String httpMethod, String url, String tenant,
137135
public boolean checkUrl(User user, String httpMethod, String url, String tenant) throws IOException {
138136
return this.enforcer.checkUrl(user, httpMethod, url, tenant);
139137
}
138+
139+
@Override
140+
public boolean[] bulkCheck(List<CheckQuery> checks) throws IOException {
141+
return this.enforcer.bulkCheck(checks);
142+
}
143+
144+
@Override
145+
public List<TenantDetails> checkInAllTenants(User user, String action, Resource resource, Context context) throws IOException {
146+
return this.enforcer.checkInAllTenants(user, action, resource, context);
147+
}
148+
149+
@Override
150+
public List<TenantDetails> checkInAllTenants(User user, String action, Resource resource) throws IOException {
151+
return this.enforcer.checkInAllTenants(user, action, resource);
152+
}
153+
154+
@Override
155+
public UserPermissions getUserPermissions(GetUserPermissionsQuery input) throws IOException {
156+
return this.enforcer.getUserPermissions(input);
157+
}
140158
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.permit.sdk.enforcement;
2+
3+
import io.permit.sdk.util.Context;
4+
5+
import java.util.HashMap;
6+
7+
/**
8+
* The {@code CheckQuery} class represents a single permit.check() request (query)
9+
* It is used by the bulk APIs to call many checks at once.
10+
*/
11+
public final class CheckQuery extends EnforcerInput {
12+
public CheckQuery(User user, String action, Resource resource, Context context) {
13+
super(user, action, resource, context);
14+
}
15+
16+
public CheckQuery(User user, String action, Resource resource) {
17+
this(user, action, resource, new Context());
18+
}
19+
}

src/main/java/io/permit/sdk/enforcement/Enforcer.java

Lines changed: 256 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
package io.permit.sdk.enforcement;
22

3+
import com.google.common.primitives.Booleans;
34
import com.google.gson.Gson;
45
import io.permit.sdk.PermitConfig;
56
import io.permit.sdk.api.HttpLoggingInterceptor;
67
import io.permit.sdk.util.Context;
78
import io.permit.sdk.util.ContextStore;
89

910
import java.io.IOException;
11+
import java.util.ArrayList;
12+
import java.util.Arrays;
1013
import java.util.HashMap;
14+
import java.util.List;
15+
import java.util.stream.Collectors;
1116

1217
import okhttp3.MediaType;
1318
import okhttp3.OkHttpClient;
@@ -26,7 +31,7 @@ class EnforcerInput {
2631
public final User user;
2732
public final String action;
2833
public final Resource resource;
29-
public final HashMap<String, Object> context;
34+
public final Context context;
3035

3136
/**
3237
* Constructs a new instance of the {@code EnforcerInput} class with the specified data.
@@ -36,7 +41,7 @@ class EnforcerInput {
3641
* @param resource The resource on which the action is performed.
3742
* @param context The context for the authorization check.
3843
*/
39-
EnforcerInput(User user, String action, Resource resource, HashMap<String, Object> context) {
44+
EnforcerInput(User user, String action, Resource resource, Context context) {
4045
this.user = user;
4146
this.action = action;
4247
this.resource = resource;
@@ -76,6 +81,48 @@ class OpaResult {
7681
}
7782
}
7883

84+
85+
/**
86+
* The {@code TenantResult} class represents a single tenant returned by the checkInAllTenants query.
87+
*/
88+
class TenantResult {
89+
public final Boolean allow;
90+
91+
public final TenantDetails tenant;
92+
93+
public TenantResult(Boolean allow, TenantDetails tenant) {
94+
this.allow = allow;
95+
this.tenant = tenant;
96+
}
97+
}
98+
99+
/**
100+
* The {@code AllTenantsResult} class represents the result of the checkInAllTenants query.
101+
*/
102+
class AllTenantsResult {
103+
public final TenantResult[] allowed_tenants;
104+
105+
public AllTenantsResult(TenantResult[] allowed_tenants) {
106+
this.allowed_tenants = allowed_tenants;
107+
}
108+
}
109+
110+
/**
111+
* The {@code OpaBulkResult} class represents the result of a Permit bulk enforcement check returned by the policy agent.
112+
*/
113+
class OpaBulkResult {
114+
public final List<OpaResult> allow;
115+
116+
/**
117+
* Constructs a new instance of the {@code OpaResult} class with the specified result.
118+
*
119+
* @param allow {@code true} if the action is allowed, {@code false} otherwise.
120+
*/
121+
OpaBulkResult(List<OpaResult> allow) {
122+
this.allow = allow;
123+
}
124+
}
125+
79126
/**
80127
* The {@code Enforcer} class is responsible for performing permission checks against the PDP.
81128
* It implements the {@link IEnforcerApi} interface.
@@ -257,8 +304,215 @@ public boolean checkUrl(User user, String httpMethod, String url, String tenant,
257304
}
258305
}
259306

307+
@Override
308+
public boolean[] bulkCheck(List<CheckQuery> checks) throws IOException {
309+
List<EnforcerInput> inputs = new ArrayList<>();
310+
311+
for (CheckQuery check: checks) {
312+
Resource normalizedResource = check.resource.normalize(this.config);
313+
inputs.add(new EnforcerInput(check.user, check.action, normalizedResource, check.context));
314+
}
315+
316+
// request body
317+
Gson gson = new Gson();
318+
String requestBody = gson.toJson(inputs);
319+
RequestBody body = RequestBody.create(requestBody, MediaType.parse("application/json"));
320+
321+
// create the request
322+
String url = String.format("%s/allowed/bulk", this.config.getPdpAddress());
323+
Request request = new Request.Builder()
324+
.url(url)
325+
.post(body)
326+
.addHeader("Content-Type", "application/json")
327+
.addHeader("Authorization", String.format("Bearer %s", this.config.getToken()))
328+
.addHeader("X-Permit-SDK-Version", String.format("java:%s", this.config.version))
329+
.build();
330+
331+
try (Response response = client.newCall(request).execute()) {
332+
if (!response.isSuccessful()) {
333+
String errorMessage = String.format(
334+
"Error in %s: got unexpected status code %d",
335+
bulkCheckRepr(inputs),
336+
response.code()
337+
);
338+
logger.error(errorMessage);
339+
throw new IOException(errorMessage);
340+
}
341+
ResponseBody responseBody = response.body();
342+
if (responseBody == null) {
343+
String errorMessage = String.format(
344+
"Error in %s: got empty response",
345+
bulkCheckRepr(inputs)
346+
);
347+
logger.error(errorMessage);
348+
throw new IOException(errorMessage);
349+
}
350+
351+
String responseString = responseBody.string();
352+
OpaBulkResult result = gson.fromJson(responseString, OpaBulkResult.class);
353+
if (this.config.isDebugMode()) {
354+
for (int i = 0; i < result.allow.size(); i++) {
355+
logger.info(String.format(
356+
"permit.bulkCheck[%d/%d](%s, %s, %s) = %s",
357+
i + 1,
358+
result.allow.size(),
359+
inputs.get(i).user,
360+
inputs.get(i).action,
361+
inputs.get(i).resource,
362+
result.allow.get(i).allow
363+
));
364+
}
365+
366+
}
367+
return Booleans.toArray(result.allow.stream().map(r -> r.allow).collect(Collectors.toList()));
368+
}
369+
}
370+
371+
@Override
372+
public List<TenantDetails> checkInAllTenants(User user, String action, Resource resource, Context context) throws IOException {
373+
Resource normalizedResource = resource.normalize(this.config);
374+
Context queryContext = this.contextStore.getDerivedContext(context);
375+
376+
EnforcerInput input = new EnforcerInput(
377+
user,
378+
action,
379+
normalizedResource,
380+
queryContext
381+
);
382+
383+
// request body
384+
Gson gson = new Gson();
385+
String requestBody = gson.toJson(input);
386+
RequestBody body = RequestBody.create(requestBody, MediaType.parse("application/json"));
387+
388+
// create the request
389+
String url = String.format("%s/allowed/all-tenants", this.config.getPdpAddress());
390+
Request request = new Request.Builder()
391+
.url(url)
392+
.post(body)
393+
.addHeader("Content-Type", "application/json")
394+
.addHeader("Authorization", String.format("Bearer %s", this.config.getToken()))
395+
.addHeader("X-Permit-SDK-Version", String.format("java:%s", this.config.version))
396+
.build();
397+
398+
try (Response response = client.newCall(request).execute()) {
399+
if (!response.isSuccessful()) {
400+
String errorMessage = String.format(
401+
"Error in permit.checkInAllTenants(%s, %s, %s): got unexpected status code %d",
402+
user.toString(),
403+
action,
404+
resource,
405+
response.code()
406+
);
407+
logger.error(errorMessage);
408+
throw new IOException(errorMessage);
409+
}
410+
ResponseBody responseBody = response.body();
411+
if (responseBody == null) {
412+
String errorMessage = String.format(
413+
"Error in permit.checkInAllTenants(%s, %s, %s): got empty response",
414+
user,
415+
action,
416+
resource
417+
);
418+
logger.error(errorMessage);
419+
throw new IOException(errorMessage);
420+
}
421+
String responseString = responseBody.string();
422+
AllTenantsResult result = gson.fromJson(responseString, AllTenantsResult.class);
423+
List<TenantDetails> tenants = Arrays.stream(result.allowed_tenants).map(r -> r.tenant).collect(Collectors.toList());
424+
if (this.config.isDebugMode()) {
425+
logger.info(String.format(
426+
"permit.checkInAllTenants(%s, %s, %s) => allowed in: [%s]",
427+
user,
428+
action,
429+
resource,
430+
tenants.stream().map(t -> t.key).collect(Collectors.joining(", "))
431+
));
432+
}
433+
return tenants;
434+
}
435+
}
436+
437+
@Override
438+
public List<TenantDetails> checkInAllTenants(User user, String action, Resource resource) throws IOException {
439+
return checkInAllTenants(user, action, resource, new Context());
440+
}
441+
442+
@Override
443+
public UserPermissions getUserPermissions(GetUserPermissionsQuery input) throws IOException {
444+
// request body
445+
Gson gson = new Gson();
446+
String requestBody = gson.toJson(input);
447+
RequestBody body = RequestBody.create(requestBody, MediaType.parse("application/json"));
448+
449+
// create the request
450+
String url = String.format("%s/user-permissions", this.config.getPdpAddress());
451+
Request request = new Request.Builder()
452+
.url(url)
453+
.post(body)
454+
.addHeader("Content-Type", "application/json")
455+
.addHeader("Authorization", String.format("Bearer %s", this.config.getToken()))
456+
.addHeader("X-Permit-SDK-Version", String.format("java:%s", this.config.version))
457+
.build();
458+
459+
try (Response response = client.newCall(request).execute()) {
460+
if (!response.isSuccessful()) {
461+
String errorMessage = String.format(
462+
"Error in permit.getUserPermissions(%s, %s, %s, %s): got unexpected status code %d",
463+
input.user.toString(),
464+
input.tenants.toString(),
465+
input.resource_types.toString(),
466+
input.resources.toString(),
467+
response.code()
468+
);
469+
logger.error(errorMessage);
470+
throw new IOException(errorMessage);
471+
}
472+
ResponseBody responseBody = response.body();
473+
if (responseBody == null) {
474+
String errorMessage = String.format(
475+
"Error in permit.getUserPermissions(%s, %s, %s, %s): got empty response",
476+
input.user.toString(),
477+
input.tenants.toString(),
478+
input.resource_types.toString(),
479+
input.resources.toString()
480+
);
481+
logger.error(errorMessage);
482+
throw new IOException(errorMessage);
483+
}
484+
String responseString = responseBody.string();
485+
UserPermissions result = gson.fromJson(responseString, UserPermissions.class);
486+
if (this.config.isDebugMode()) {
487+
logger.info(String.format(
488+
"permit.getUserPermissions(%s, %s, %s, %s) => returned %d permissions on %d objects",
489+
input.user.toString(),
490+
input.tenants != null ? input.tenants.toString() : "null",
491+
input.resource_types != null ? input.resource_types.toString() : "null",
492+
input.resources != null ? input.resources.toString() : "null",
493+
result.values().stream().map(obj -> obj.permissions.size()).reduce(0, Integer::sum),
494+
result.keySet().size()
495+
));
496+
}
497+
return result;
498+
}
499+
}
500+
260501
@Override
261502
public boolean checkUrl(User user, String httpMethod, String url, String tenant) throws IOException {
262503
return this.checkUrl(user, httpMethod, url, tenant, new Context());
263504
}
505+
506+
private String bulkCheckRepr(List<EnforcerInput> inputs) {
507+
return String.format(
508+
"permit.bulkCheck(%s)",
509+
inputs.stream().map(i -> String.format(
510+
"%s, %s, %s, %s",
511+
i.user,
512+
i.action,
513+
i.resource,
514+
i.context
515+
)).collect(Collectors.toList())
516+
);
517+
}
264518
}

0 commit comments

Comments
 (0)