From 8df3f5bc3f20d2e9bcadc148388809e822fc65e2 Mon Sep 17 00:00:00 2001 From: Asaf Cohen Date: Tue, 10 Oct 2023 01:30:06 +0300 Subject: [PATCH 1/3] bulk check --- src/main/java/io/permit/sdk/Permit.java | 11 +- .../io/permit/sdk/enforcement/CheckQuery.java | 19 ++++ .../io/permit/sdk/enforcement/Enforcer.java | 103 +++++++++++++++++- .../permit/sdk/enforcement/IEnforcerApi.java | 58 ++++++++++ .../java/io/permit/sdk/e2e/RbacE2ETest.java | 22 ++++ 5 files changed, 207 insertions(+), 6 deletions(-) create mode 100644 src/main/java/io/permit/sdk/enforcement/CheckQuery.java diff --git a/src/main/java/io/permit/sdk/Permit.java b/src/main/java/io/permit/sdk/Permit.java index 295755a..8912e68 100644 --- a/src/main/java/io/permit/sdk/Permit.java +++ b/src/main/java/io/permit/sdk/Permit.java @@ -4,16 +4,14 @@ import com.google.gson.GsonBuilder; import io.permit.sdk.api.ApiClient; import io.permit.sdk.api.ElementsApi; -import io.permit.sdk.enforcement.Enforcer; -import io.permit.sdk.enforcement.IEnforcerApi; -import io.permit.sdk.enforcement.Resource; -import io.permit.sdk.enforcement.User; +import io.permit.sdk.enforcement.*; import io.permit.sdk.util.Context; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.util.List; /** * The {@code Permit} class represents the main entry point for interacting with the Permit.io SDK. @@ -133,6 +131,11 @@ public boolean checkUrl(User user, String httpMethod, String url, String tenant, return this.enforcer.checkUrl(user, httpMethod, url, tenant, context); } + @Override + public boolean[] bulkCheck(List checks) throws IOException { + return this.enforcer.bulkCheck(checks); + } + @Override public boolean checkUrl(User user, String httpMethod, String url, String tenant) throws IOException { return this.enforcer.checkUrl(user, httpMethod, url, tenant); diff --git a/src/main/java/io/permit/sdk/enforcement/CheckQuery.java b/src/main/java/io/permit/sdk/enforcement/CheckQuery.java new file mode 100644 index 0000000..afe6067 --- /dev/null +++ b/src/main/java/io/permit/sdk/enforcement/CheckQuery.java @@ -0,0 +1,19 @@ +package io.permit.sdk.enforcement; + +import io.permit.sdk.util.Context; + +import java.util.HashMap; + +/** + * The {@code CheckQuery} class represents a single permit.check() request (query) + * It is used by the bulk APIs to call many checks at once. + */ +public final class CheckQuery extends EnforcerInput { + public CheckQuery(User user, String action, Resource resource, Context context) { + super(user, action, resource, context); + } + + public CheckQuery(User user, String action, Resource resource) { + this(user, action, resource, new Context()); + } +} diff --git a/src/main/java/io/permit/sdk/enforcement/Enforcer.java b/src/main/java/io/permit/sdk/enforcement/Enforcer.java index d975daf..1cad0fe 100644 --- a/src/main/java/io/permit/sdk/enforcement/Enforcer.java +++ b/src/main/java/io/permit/sdk/enforcement/Enforcer.java @@ -1,13 +1,19 @@ package io.permit.sdk.enforcement; +import com.google.common.primitives.Booleans; import com.google.gson.Gson; import io.permit.sdk.PermitConfig; import io.permit.sdk.api.HttpLoggingInterceptor; +import io.permit.sdk.openapi.models.RoleRead; import io.permit.sdk.util.Context; import io.permit.sdk.util.ContextStore; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; import okhttp3.MediaType; import okhttp3.OkHttpClient; @@ -26,7 +32,7 @@ class EnforcerInput { public final User user; public final String action; public final Resource resource; - public final HashMap context; + public final Context context; /** * Constructs a new instance of the {@code EnforcerInput} class with the specified data. @@ -36,7 +42,7 @@ class EnforcerInput { * @param resource The resource on which the action is performed. * @param context The context for the authorization check. */ - EnforcerInput(User user, String action, Resource resource, HashMap context) { + EnforcerInput(User user, String action, Resource resource, Context context) { this.user = user; this.action = action; this.resource = resource; @@ -76,6 +82,22 @@ class OpaResult { } } +/** + * The {@code OpaBulkResult} class represents the result of a Permit bulk enforcement check returned by the policy agent. + */ +class OpaBulkResult { + public final List allow; + + /** + * Constructs a new instance of the {@code OpaResult} class with the specified result. + * + * @param allow {@code true} if the action is allowed, {@code false} otherwise. + */ + OpaBulkResult(List allow) { + this.allow = allow; + } +} + /** * The {@code Enforcer} class is responsible for performing permission checks against the PDP. * It implements the {@link IEnforcerApi} interface. @@ -257,8 +279,85 @@ public boolean checkUrl(User user, String httpMethod, String url, String tenant, } } + @Override + public boolean[] bulkCheck(List checks) throws IOException { + List inputs = new ArrayList<>(); + + for (CheckQuery check: checks) { + Resource normalizedResource = check.resource.normalize(this.config); + inputs.add(new EnforcerInput(check.user, check.action, normalizedResource, check.context)); + } + + // request body + Gson gson = new Gson(); + String requestBody = gson.toJson(inputs); + RequestBody body = RequestBody.create(requestBody, MediaType.parse("application/json")); + + // create the request + String url = String.format("%s/allowed/bulk", this.config.getPdpAddress()); + Request request = new Request.Builder() + .url(url) + .post(body) + .addHeader("Content-Type", "application/json") + .addHeader("Authorization", String.format("Bearer %s", this.config.getToken())) + .addHeader("X-Permit-SDK-Version", String.format("java:%s", this.config.version)) + .build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + String errorMessage = String.format( + "Error in %s: got unexpected status code %d", + bulkCheckRepr(inputs), + response.code() + ); + logger.error(errorMessage); + throw new IOException(errorMessage); + } + ResponseBody responseBody = response.body(); + if (responseBody == null) { + String errorMessage = String.format( + "Error in %s: got empty response", + bulkCheckRepr(inputs) + ); + logger.error(errorMessage); + throw new IOException(errorMessage); + } + + String responseString = responseBody.string(); + OpaBulkResult result = gson.fromJson(responseString, OpaBulkResult.class); + if (this.config.isDebugMode()) { + for (int i = 0; i < result.allow.size(); i++) { + logger.info(String.format( + "permit.bulkCheck[%d/%d](%s, %s, %s) = %s", + i + 1, + result.allow.size(), + inputs.get(i).user, + inputs.get(i).action, + inputs.get(i).resource, + result.allow.get(i).allow + )); + } + + } + return Booleans.toArray(result.allow.stream().map(r -> r.allow).collect(Collectors.toList())); + } + } + @Override public boolean checkUrl(User user, String httpMethod, String url, String tenant) throws IOException { return this.checkUrl(user, httpMethod, url, tenant, new Context()); } + + private String bulkCheckRepr(List inputs) { + return String.format( + "permit.bulkCheck(%s)", + inputs.stream().map(i -> String.format( + "%s, %s, %s, %s", + i.user, + i.action, + i.resource, + i.context + )).collect(Collectors.toList()) + ); + } } \ No newline at end of file diff --git a/src/main/java/io/permit/sdk/enforcement/IEnforcerApi.java b/src/main/java/io/permit/sdk/enforcement/IEnforcerApi.java index 1db5ed6..b2e14e2 100644 --- a/src/main/java/io/permit/sdk/enforcement/IEnforcerApi.java +++ b/src/main/java/io/permit/sdk/enforcement/IEnforcerApi.java @@ -3,10 +3,68 @@ import io.permit.sdk.util.Context; import java.io.IOException; +import java.util.List; public interface IEnforcerApi { + /** + * Checks if a `user` is authorized to perform an `action` on a `resource` within the specified context. + * + * @param user The user object representing the user. + * @param action The action to be performed on the resource. + * @param resource The resource object representing the resource. + * @param context The context object representing the context in which the action is performed. + * @return `true` if the user is authorized, `false` otherwise. + * @throws IOException if an error occurs while sending the authorization request to the PDP. + */ boolean check(User user, String action, Resource resource, Context context) throws IOException; + + /** + * Checks if a `user` is authorized to perform an `action` on a `resource` without additional context + * + * @param user The user object representing the user. + * @param action The action to be performed on the resource. + * @param resource The resource object representing the resource. + * @return `true` if the user is authorized, `false` otherwise. + * @throws IOException if an error occurs while sending the authorization request to the PDP. + */ boolean check(User user, String action, Resource resource) throws IOException; + + /** + * Performs a permission check on a (resource, action) pair that are represented by an HTTP endpoint. + * The resource and actions are extracted from the HTTP url and method. + * A tenant must be provided to determine the scope of the permission check. + * + * @param user The user object representing the user. + * @param httpMethod the HTTP method the user is calling, typically determines the action. + * @param url the url the user is calling, typically determines the resource. + * @param tenant the tenant determines the scope of the permission check. + * @return `true` if the user is authorized, `false` otherwise. + * @throws IOException if an error occurs while sending the authorization request to the PDP. + */ boolean checkUrl(User user, String httpMethod, String url, String tenant) throws IOException; + + /** + * Performs a permission check on a (resource, action) pair that are represented by an HTTP endpoint. + * The resource and actions are extracted from the HTTP url and method. + * A tenant must be provided to determine the scope of the permission check. + * Receives additional context + * + * @param user The user object representing the user. + * @param httpMethod the HTTP method the user is calling, typically determines the action. + * @param url the url the user is calling, typically determines the resource. + * @param tenant the tenant determines the scope of the permission check. + * @param context The context object representing the context in which the action is performed. + * @return `true` if the user is authorized, `false` otherwise. + * @throws IOException if an error occurs while sending the authorization request to the PDP. + */ boolean checkUrl(User user, String httpMethod, String url, String tenant, Context context) throws IOException; + + /** + * Runs multiple permission checks in a single HTTP Request (Bulk Check). + * + * @param checks The check requests, each containing user, action, resource and context. + * @return array containing `true` if the user is authorized, `false` otherwise for each check request. + * @throws IOException if an error occurs while sending the authorization request to the PDP. + */ + boolean[] bulkCheck(List checks) throws IOException; } diff --git a/src/test/java/io/permit/sdk/e2e/RbacE2ETest.java b/src/test/java/io/permit/sdk/e2e/RbacE2ETest.java index 7f24389..c6e402f 100644 --- a/src/test/java/io/permit/sdk/e2e/RbacE2ETest.java +++ b/src/test/java/io/permit/sdk/e2e/RbacE2ETest.java @@ -1,10 +1,12 @@ package io.permit.sdk.e2e; +import com.google.common.primitives.Booleans; import io.permit.sdk.PermitE2ETestBase; import io.permit.sdk.api.PermitApiError; import io.permit.sdk.api.PermitContextError; import io.permit.sdk.Permit; import io.permit.sdk.api.models.CreateOrUpdateResult; +import io.permit.sdk.enforcement.CheckQuery; import io.permit.sdk.enforcement.Resource; import io.permit.sdk.enforcement.User; import io.permit.sdk.openapi.models.*; @@ -16,6 +18,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import static org.junit.jupiter.api.Assertions.*; @@ -181,6 +184,25 @@ void testPermissionCheckRBAC() { new Resource.Builder("document").withTenant(tenant.key).build() )); + logger.info("testing bulk permission check"); + boolean[] checks = permit.bulkCheck(Arrays.asList( + // positive permission check + new CheckQuery( + userInput, + "read", + new Resource.Builder("document").withTenant(tenant.key).build() + ), + // negative permission check + new CheckQuery( + User.fromString("auth0|elon"), + "create", + new Resource.Builder("document").withTenant(tenant.key).build() + ) + )); + assertEquals(checks.length, 2); + assertTrue(checks[0]); + assertFalse(checks[1]); + // change the user role permit.api.users.assignRole(user.key, admin.key, tenant.key); permit.api.users.unassignRole(user.key, viewer.key, tenant.key); From 395a49dd11fbb19e803b5cf79b78000b810ebfba Mon Sep 17 00:00:00 2001 From: Asaf Cohen Date: Sun, 15 Oct 2023 12:47:07 +0300 Subject: [PATCH 2/3] added new permission check: check in all tenants --- src/main/java/io/permit/sdk/Permit.java | 14 ++- .../io/permit/sdk/enforcement/Enforcer.java | 98 ++++++++++++++++++- .../permit/sdk/enforcement/IEnforcerApi.java | 26 +++++ .../permit/sdk/enforcement/TenantDetails.java | 17 ++++ .../java/io/permit/sdk/e2e/RbacE2ETest.java | 42 ++++++++ 5 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 src/main/java/io/permit/sdk/enforcement/TenantDetails.java diff --git a/src/main/java/io/permit/sdk/Permit.java b/src/main/java/io/permit/sdk/Permit.java index 8912e68..c0f5704 100644 --- a/src/main/java/io/permit/sdk/Permit.java +++ b/src/main/java/io/permit/sdk/Permit.java @@ -131,13 +131,23 @@ public boolean checkUrl(User user, String httpMethod, String url, String tenant, return this.enforcer.checkUrl(user, httpMethod, url, tenant, context); } + @Override + public boolean checkUrl(User user, String httpMethod, String url, String tenant) throws IOException { + return this.enforcer.checkUrl(user, httpMethod, url, tenant); + } + @Override public boolean[] bulkCheck(List checks) throws IOException { return this.enforcer.bulkCheck(checks); } @Override - public boolean checkUrl(User user, String httpMethod, String url, String tenant) throws IOException { - return this.enforcer.checkUrl(user, httpMethod, url, tenant); + public List checkInAllTenants(User user, String action, Resource resource, Context context) throws IOException { + return this.enforcer.checkInAllTenants(user, action, resource, context); + } + + @Override + public List checkInAllTenants(User user, String action, Resource resource) throws IOException { + return this.enforcer.checkInAllTenants(user, action, resource); } } diff --git a/src/main/java/io/permit/sdk/enforcement/Enforcer.java b/src/main/java/io/permit/sdk/enforcement/Enforcer.java index 1cad0fe..6c42c1b 100644 --- a/src/main/java/io/permit/sdk/enforcement/Enforcer.java +++ b/src/main/java/io/permit/sdk/enforcement/Enforcer.java @@ -4,7 +4,6 @@ import com.google.gson.Gson; import io.permit.sdk.PermitConfig; import io.permit.sdk.api.HttpLoggingInterceptor; -import io.permit.sdk.openapi.models.RoleRead; import io.permit.sdk.util.Context; import io.permit.sdk.util.ContextStore; @@ -82,6 +81,32 @@ class OpaResult { } } + +/** + * The {@code TenantResult} class represents a single tenant returned by the checkInAllTenants query. + */ +class TenantResult { + public final Boolean allow; + + public final TenantDetails tenant; + + public TenantResult(Boolean allow, TenantDetails tenant) { + this.allow = allow; + this.tenant = tenant; + } +} + +/** + * The {@code AllTenantsResult} class represents the result of the checkInAllTenants query. + */ +class AllTenantsResult { + public final TenantResult[] allowed_tenants; + + public AllTenantsResult(TenantResult[] allowed_tenants) { + this.allowed_tenants = allowed_tenants; + } +} + /** * The {@code OpaBulkResult} class represents the result of a Permit bulk enforcement check returned by the policy agent. */ @@ -343,6 +368,77 @@ public boolean[] bulkCheck(List checks) throws IOException { } } + @Override + public List checkInAllTenants(User user, String action, Resource resource, Context context) throws IOException { + Resource normalizedResource = resource.normalize(this.config); + Context queryContext = this.contextStore.getDerivedContext(context); + + EnforcerInput input = new EnforcerInput( + user, + action, + normalizedResource, + queryContext + ); + + // request body + Gson gson = new Gson(); + String requestBody = gson.toJson(input); + RequestBody body = RequestBody.create(requestBody, MediaType.parse("application/json")); + + // create the request + String url = String.format("%s/allowed/all-tenants", this.config.getPdpAddress()); + Request request = new Request.Builder() + .url(url) + .post(body) + .addHeader("Content-Type", "application/json") + .addHeader("Authorization", String.format("Bearer %s", this.config.getToken())) + .addHeader("X-Permit-SDK-Version", String.format("java:%s", this.config.version)) + .build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + String errorMessage = String.format( + "Error in permit.checkInAllTenants(%s, %s, %s): got unexpected status code %d", + user.toString(), + action, + resource, + response.code() + ); + logger.error(errorMessage); + throw new IOException(errorMessage); + } + ResponseBody responseBody = response.body(); + if (responseBody == null) { + String errorMessage = String.format( + "Error in permit.checkInAllTenants(%s, %s, %s): got empty response", + user, + action, + resource + ); + logger.error(errorMessage); + throw new IOException(errorMessage); + } + String responseString = responseBody.string(); + AllTenantsResult result = gson.fromJson(responseString, AllTenantsResult.class); + List tenants = Arrays.stream(result.allowed_tenants).map(r -> r.tenant).collect(Collectors.toList()); + if (this.config.isDebugMode()) { + logger.info(String.format( + "permit.checkInAllTenants(%s, %s, %s) => allowed in: [%s]", + user, + action, + resource, + tenants.stream().map(t -> t.key).collect(Collectors.joining(", ")) + )); + } + return tenants; + } + } + + @Override + public List checkInAllTenants(User user, String action, Resource resource) throws IOException { + return checkInAllTenants(user, action, resource, new Context()); + } + @Override public boolean checkUrl(User user, String httpMethod, String url, String tenant) throws IOException { return this.checkUrl(user, httpMethod, url, tenant, new Context()); diff --git a/src/main/java/io/permit/sdk/enforcement/IEnforcerApi.java b/src/main/java/io/permit/sdk/enforcement/IEnforcerApi.java index b2e14e2..8dbd6f5 100644 --- a/src/main/java/io/permit/sdk/enforcement/IEnforcerApi.java +++ b/src/main/java/io/permit/sdk/enforcement/IEnforcerApi.java @@ -67,4 +67,30 @@ public interface IEnforcerApi { * @throws IOException if an error occurs while sending the authorization request to the PDP. */ boolean[] bulkCheck(List checks) throws IOException; + + /** + * Checks if a `user` is authorized to perform an `action` on a `resource` (with `context`) across all tenants. + * Returns only tenants in which the action is allowed for this user, including the tenant attributes. + * + * @param user The user object representing the user. + * @param action The action to be performed on the resource. + * @param resource The resource object representing the resource. + * @param context The context object representing the context in which the action is performed. + * @return List of TenantDetails objects, representing the tenants in which the action is allowed. + * @throws IOException if an error occurs while sending the authorization request to the PDP. + */ + List checkInAllTenants(User user, String action, Resource resource, Context context) throws IOException; + + /** + * Checks if a `user` is authorized to perform an `action` on a `resource` across all tenants, + * without additional context. Returns only tenants in which the action is allowed for this user, + * including the tenant attributes. + * + * @param user The user object representing the user. + * @param action The action to be performed on the resource. + * @param resource The resource object representing the resource. + * @return List of TenantDetails objects, representing the tenants in which the action is allowed. + * @throws IOException if an error occurs while sending the authorization request to the PDP. + */ + List checkInAllTenants(User user, String action, Resource resource) throws IOException; } diff --git a/src/main/java/io/permit/sdk/enforcement/TenantDetails.java b/src/main/java/io/permit/sdk/enforcement/TenantDetails.java new file mode 100644 index 0000000..80ef3e0 --- /dev/null +++ b/src/main/java/io/permit/sdk/enforcement/TenantDetails.java @@ -0,0 +1,17 @@ +package io.permit.sdk.enforcement; + +import io.permit.sdk.util.Context; + +import java.util.HashMap; + +/** + * The {@code TenantDetails} class represents a single tenant information fetched from the PDP (key and attributes). + */ +public final class TenantDetails { + public final String key; + public final HashMap attributes; + public TenantDetails(String key, HashMap attributes) { + this.key = key; + this.attributes = attributes; + } +} \ No newline at end of file diff --git a/src/test/java/io/permit/sdk/e2e/RbacE2ETest.java b/src/test/java/io/permit/sdk/e2e/RbacE2ETest.java index c6e402f..a34c085 100644 --- a/src/test/java/io/permit/sdk/e2e/RbacE2ETest.java +++ b/src/test/java/io/permit/sdk/e2e/RbacE2ETest.java @@ -8,6 +8,7 @@ import io.permit.sdk.api.models.CreateOrUpdateResult; import io.permit.sdk.enforcement.CheckQuery; import io.permit.sdk.enforcement.Resource; +import io.permit.sdk.enforcement.TenantDetails; import io.permit.sdk.enforcement.User; import io.permit.sdk.openapi.models.*; import org.junit.jupiter.api.Test; @@ -124,11 +125,24 @@ void testPermissionCheckRBAC() { assertEquals(tenant.description, "The car company"); assertNull(tenant.attributes); + // create another tenant + HashMap tenantAttributes = new HashMap<>(); + tenantAttributes.put("tier", "pro"); + tenantAttributes.put("unit", "one"); + + TenantRead tenant2 = permit.api.tenants.create( + new TenantCreate("twitter", "Twitter Inc").withAttributes(tenantAttributes) + ); + assertEquals(tenant2.key, "twitter"); + assertEquals(((String)tenant2.attributes.get("tier")), "pro"); + assertEquals(((String)tenant2.attributes.get("unit")), "one"); + // create a user HashMap userAttributes = new HashMap<>(); userAttributes.put("age", Integer.valueOf(50)); userAttributes.put("fav_color", "red"); + User userInput = (new User.Builder("auth0|elon")) .withEmail("elonmusk@tesla.com") .withFirstName("Elon") @@ -155,6 +169,12 @@ void testPermissionCheckRBAC() { assertEquals(ra.role, viewer.key); assertEquals(ra.tenant, tenant.key); + // assign a second role in another tenant + RoleAssignmentRead ra2 = permit.api.users.assignRole("auth0|elon", "admin", "twitter"); + assertEquals(ra2.userId, user.id); + assertEquals(ra2.roleId, admin.id); + assertEquals(ra2.tenantId, tenant2.id); + logger.info("sleeping 20 seconds before permit.check() to make sure all writes propagated from cloud to PDP"); Thread.sleep(20000); @@ -203,6 +223,27 @@ void testPermissionCheckRBAC() { assertTrue(checks[0]); assertFalse(checks[1]); + logger.info("testing 'check in all tenants' on read:document"); + List allowedTenants = permit.checkInAllTenants( + userInput, + "read", + new Resource.Builder("document").build() + ); + assertEquals(allowedTenants.size(), 2); + assertTrue(allowedTenants.get(0).key.equals(tenant.key) || allowedTenants.get(0).key.equals(tenant2.key)); + assertTrue(allowedTenants.get(1).key.equals(tenant.key) || allowedTenants.get(1).key.equals(tenant2.key)); + assertNotEquals(allowedTenants.get(0).key, allowedTenants.get(1).key); + + logger.info("testing 'check in all tenants' on create:document"); + List allowedTenants2 = permit.checkInAllTenants( + userInput, + "create", + new Resource.Builder("document").build() + ); + assertEquals(allowedTenants2.size(), 1); + assertEquals(allowedTenants2.get(0).key, tenant2.key); + assertEquals(((String)allowedTenants2.get(0).attributes.get("unit")), "one"); + // change the user role permit.api.users.assignRole(user.key, admin.key, tenant.key); permit.api.users.unassignRole(user.key, viewer.key, tenant.key); @@ -235,6 +276,7 @@ void testPermissionCheckRBAC() { permit.api.roles.delete("admin"); permit.api.roles.delete("viewer"); permit.api.tenants.delete("tesla"); + permit.api.tenants.delete("twitter"); permit.api.users.delete("auth0|elon"); assertEquals(permit.api.resources.list().length, 0); assertEquals(permit.api.roles.list().length, 0); From 9fee5b57883782c5732f46b719308643cdc5f504 Mon Sep 17 00:00:00 2001 From: Asaf Cohen Date: Sun, 15 Oct 2023 20:48:57 +0300 Subject: [PATCH 3/3] added new permission check: get user permissions --- src/main/java/io/permit/sdk/Permit.java | 5 ++ .../io/permit/sdk/enforcement/Enforcer.java | 59 +++++++++++++++++++ .../enforcement/GetUserPermissionsQuery.java | 47 +++++++++++++++ .../permit/sdk/enforcement/IEnforcerApi.java | 9 +++ .../sdk/enforcement/ObjectPermissions.java | 19 ++++++ .../sdk/enforcement/ResourceDetails.java | 16 +++++ .../permit/sdk/enforcement/TenantDetails.java | 2 +- .../sdk/enforcement/UserPermissions.java | 8 +++ .../java/io/permit/sdk/e2e/RbacE2ETest.java | 22 +++++-- 9 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 src/main/java/io/permit/sdk/enforcement/GetUserPermissionsQuery.java create mode 100644 src/main/java/io/permit/sdk/enforcement/ObjectPermissions.java create mode 100644 src/main/java/io/permit/sdk/enforcement/ResourceDetails.java create mode 100644 src/main/java/io/permit/sdk/enforcement/UserPermissions.java diff --git a/src/main/java/io/permit/sdk/Permit.java b/src/main/java/io/permit/sdk/Permit.java index c0f5704..886bc49 100644 --- a/src/main/java/io/permit/sdk/Permit.java +++ b/src/main/java/io/permit/sdk/Permit.java @@ -150,4 +150,9 @@ public List checkInAllTenants(User user, String action, Resource public List checkInAllTenants(User user, String action, Resource resource) throws IOException { return this.enforcer.checkInAllTenants(user, action, resource); } + + @Override + public UserPermissions getUserPermissions(GetUserPermissionsQuery input) throws IOException { + return this.enforcer.getUserPermissions(input); + } } diff --git a/src/main/java/io/permit/sdk/enforcement/Enforcer.java b/src/main/java/io/permit/sdk/enforcement/Enforcer.java index 6c42c1b..16d6e3b 100644 --- a/src/main/java/io/permit/sdk/enforcement/Enforcer.java +++ b/src/main/java/io/permit/sdk/enforcement/Enforcer.java @@ -439,6 +439,65 @@ public List checkInAllTenants(User user, String action, Resource return checkInAllTenants(user, action, resource, new Context()); } + @Override + public UserPermissions getUserPermissions(GetUserPermissionsQuery input) throws IOException { + // request body + Gson gson = new Gson(); + String requestBody = gson.toJson(input); + RequestBody body = RequestBody.create(requestBody, MediaType.parse("application/json")); + + // create the request + String url = String.format("%s/user-permissions", this.config.getPdpAddress()); + Request request = new Request.Builder() + .url(url) + .post(body) + .addHeader("Content-Type", "application/json") + .addHeader("Authorization", String.format("Bearer %s", this.config.getToken())) + .addHeader("X-Permit-SDK-Version", String.format("java:%s", this.config.version)) + .build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + String errorMessage = String.format( + "Error in permit.getUserPermissions(%s, %s, %s, %s): got unexpected status code %d", + input.user.toString(), + input.tenants.toString(), + input.resource_types.toString(), + input.resources.toString(), + response.code() + ); + logger.error(errorMessage); + throw new IOException(errorMessage); + } + ResponseBody responseBody = response.body(); + if (responseBody == null) { + String errorMessage = String.format( + "Error in permit.getUserPermissions(%s, %s, %s, %s): got empty response", + input.user.toString(), + input.tenants.toString(), + input.resource_types.toString(), + input.resources.toString() + ); + logger.error(errorMessage); + throw new IOException(errorMessage); + } + String responseString = responseBody.string(); + UserPermissions result = gson.fromJson(responseString, UserPermissions.class); + if (this.config.isDebugMode()) { + logger.info(String.format( + "permit.getUserPermissions(%s, %s, %s, %s) => returned %d permissions on %d objects", + input.user.toString(), + input.tenants != null ? input.tenants.toString() : "null", + input.resource_types != null ? input.resource_types.toString() : "null", + input.resources != null ? input.resources.toString() : "null", + result.values().stream().map(obj -> obj.permissions.size()).reduce(0, Integer::sum), + result.keySet().size() + )); + } + return result; + } + } + @Override public boolean checkUrl(User user, String httpMethod, String url, String tenant) throws IOException { return this.checkUrl(user, httpMethod, url, tenant, new Context()); diff --git a/src/main/java/io/permit/sdk/enforcement/GetUserPermissionsQuery.java b/src/main/java/io/permit/sdk/enforcement/GetUserPermissionsQuery.java new file mode 100644 index 0000000..fa38fe3 --- /dev/null +++ b/src/main/java/io/permit/sdk/enforcement/GetUserPermissionsQuery.java @@ -0,0 +1,47 @@ +package io.permit.sdk.enforcement; + +import io.permit.sdk.util.Context; + +import java.util.ArrayList; +import java.util.List; + +public final class GetUserPermissionsQuery { + public final User user; + public final List tenants; + public final List resource_types; + public final List resources; + public final Context context; + + /** + * input to get user permissions api + * + * @param user the user we'd like to get a list of permissions for. + * @param tenants filter only permissions granted on specific tenants. + * @param resource_types filter permissions based on resource type. + * @param resources filter permissions based on resource instance key. + * @param context The context for the authorization check. + */ + public GetUserPermissionsQuery(User user, List tenants, List resource_types, List resources, Context context) { + this.user = user; + this.tenants = tenants; + this.resource_types = resource_types; + this.resources = resources; + this.context = context; + } + + public GetUserPermissionsQuery(User user, List tenants, List resource_types, List resources) { + this(user, tenants, resource_types, resources, new Context()); + } + + public GetUserPermissionsQuery(User user, List tenants, List resource_types) { + this(user, tenants, resource_types, null); + } + + public GetUserPermissionsQuery(User user, List tenants) { + this(user, tenants, null); + } + + public GetUserPermissionsQuery(User user) { + this(user, null); + } +} diff --git a/src/main/java/io/permit/sdk/enforcement/IEnforcerApi.java b/src/main/java/io/permit/sdk/enforcement/IEnforcerApi.java index 8dbd6f5..a4f9711 100644 --- a/src/main/java/io/permit/sdk/enforcement/IEnforcerApi.java +++ b/src/main/java/io/permit/sdk/enforcement/IEnforcerApi.java @@ -93,4 +93,13 @@ public interface IEnforcerApi { * @throws IOException if an error occurs while sending the authorization request to the PDP. */ List checkInAllTenants(User user, String action, Resource resource) throws IOException; + + /** + * list all the permissions granted to a user (by default in all tenants and for all objects). + * + * @param input input to get user permissions api + * @return A UserPermissions object, that contains all the permissions granted to the user. + * @throws IOException if an error occurs while sending the authorization request to the PDP. + */ + UserPermissions getUserPermissions(GetUserPermissionsQuery input) throws IOException; } diff --git a/src/main/java/io/permit/sdk/enforcement/ObjectPermissions.java b/src/main/java/io/permit/sdk/enforcement/ObjectPermissions.java new file mode 100644 index 0000000..e546b19 --- /dev/null +++ b/src/main/java/io/permit/sdk/enforcement/ObjectPermissions.java @@ -0,0 +1,19 @@ +package io.permit.sdk.enforcement; + +import java.util.List; + +/** + * The {@code ObjectPermissions} class represents a single object (tenant or resource instance) that the queried user can access. + */ +public class ObjectPermissions { + + public final TenantDetails tenant; + public final ResourceDetails resource; + public final List permissions; + + public ObjectPermissions(TenantDetails tenant, ResourceDetails resource, List permissions) { + this.tenant = tenant; + this.resource = resource; + this.permissions = permissions; + } +} diff --git a/src/main/java/io/permit/sdk/enforcement/ResourceDetails.java b/src/main/java/io/permit/sdk/enforcement/ResourceDetails.java new file mode 100644 index 0000000..3215fa0 --- /dev/null +++ b/src/main/java/io/permit/sdk/enforcement/ResourceDetails.java @@ -0,0 +1,16 @@ +package io.permit.sdk.enforcement; + +import io.permit.sdk.util.Context; + +import java.util.HashMap; + +/** + * The {@code ResourceDetails} class represents a single resource instance information fetched from the PDP (key and attributes). + */ +public final class ResourceDetails extends TenantDetails { + public final String type; + public ResourceDetails(String type, String key, HashMap attributes) { + super(key, attributes); + this.type = type; + } +} \ No newline at end of file diff --git a/src/main/java/io/permit/sdk/enforcement/TenantDetails.java b/src/main/java/io/permit/sdk/enforcement/TenantDetails.java index 80ef3e0..4166cde 100644 --- a/src/main/java/io/permit/sdk/enforcement/TenantDetails.java +++ b/src/main/java/io/permit/sdk/enforcement/TenantDetails.java @@ -7,7 +7,7 @@ /** * The {@code TenantDetails} class represents a single tenant information fetched from the PDP (key and attributes). */ -public final class TenantDetails { +public class TenantDetails { public final String key; public final HashMap attributes; public TenantDetails(String key, HashMap attributes) { diff --git a/src/main/java/io/permit/sdk/enforcement/UserPermissions.java b/src/main/java/io/permit/sdk/enforcement/UserPermissions.java new file mode 100644 index 0000000..7027f3c --- /dev/null +++ b/src/main/java/io/permit/sdk/enforcement/UserPermissions.java @@ -0,0 +1,8 @@ +package io.permit.sdk.enforcement; + +import java.util.HashMap; + +/** + * The {@code UserPermissions} class represents all the objects a user can access. + */ +final public class UserPermissions extends HashMap {} diff --git a/src/test/java/io/permit/sdk/e2e/RbacE2ETest.java b/src/test/java/io/permit/sdk/e2e/RbacE2ETest.java index a34c085..205d8b3 100644 --- a/src/test/java/io/permit/sdk/e2e/RbacE2ETest.java +++ b/src/test/java/io/permit/sdk/e2e/RbacE2ETest.java @@ -6,10 +6,7 @@ import io.permit.sdk.api.PermitContextError; import io.permit.sdk.Permit; import io.permit.sdk.api.models.CreateOrUpdateResult; -import io.permit.sdk.enforcement.CheckQuery; -import io.permit.sdk.enforcement.Resource; -import io.permit.sdk.enforcement.TenantDetails; -import io.permit.sdk.enforcement.User; +import io.permit.sdk.enforcement.*; import io.permit.sdk.openapi.models.*; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -20,6 +17,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.*; @@ -32,6 +30,8 @@ public class RbacE2ETest extends PermitE2ETestBase { private final Logger logger = LoggerFactory.getLogger(RbacE2ETest.class); + private static final String TENANT_RESOURCE_KEY = "__tenant"; + @Test void testPermissionCheckRBAC() { // init the client @@ -244,6 +244,20 @@ void testPermissionCheckRBAC() { assertEquals(allowedTenants2.get(0).key, tenant2.key); assertEquals(((String)allowedTenants2.get(0).attributes.get("unit")), "one"); + logger.info("testing 'get user permissions' on user 'elon'"); + UserPermissions permissions = permit.getUserPermissions( + new GetUserPermissionsQuery( + User.fromString("auth0|elon") + ) + ); + assertEquals(permissions.keySet().size(), 2); // elon has access to 2 tenants + String tenantObjectKey = String.format("%s:%s", TENANT_RESOURCE_KEY, tenant.key); + String tenant2ObjectKey = String.format("%s:%s", TENANT_RESOURCE_KEY, tenant2.key); + assertTrue(permissions.containsKey(tenantObjectKey)); + assertTrue(permissions.containsKey(tenant2ObjectKey)); + assertTrue(permissions.get(tenantObjectKey).permissions.contains("document:read")); + assertTrue(permissions.get(tenant2ObjectKey).permissions.contains("document:create")); + // change the user role permit.api.users.assignRole(user.key, admin.key, tenant.key); permit.api.users.unassignRole(user.key, viewer.key, tenant.key);