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);