Skip to content

Commit

Permalink
Merge pull request #12 from permitio/asaf/per-6027-rewrite-java-conte…
Browse files Browse the repository at this point in the history
…xt-api

Fix context api to differentiate between access level and object scoping context
  • Loading branch information
Asaf Cohen authored Jun 20, 2023
2 parents d0e3bad + d137e2a commit 7f7a36f
Show file tree
Hide file tree
Showing 22 changed files with 410 additions and 113 deletions.
18 changes: 18 additions & 0 deletions src/main/java/io/permit/sdk/ApiContextLevel.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.permit.sdk;

public enum ApiContextLevel {
WAIT_FOR_INIT(0),
ORGANIZATION(1),
PROJECT(2),
ENVIRONMENT(3);

private final int value;

ApiContextLevel(int value) {
this.value = value;
}

public int getValue() {
return value;
}
}
25 changes: 19 additions & 6 deletions src/main/java/io/permit/sdk/ApiKeyLevel.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
package io.permit.sdk;

/**
* The {@code ApiKeyLevel} enum represents the different levels (or scopes) of API keys in the Permit SDK.
* These levels determine the scope of permissions granted by the API key: is the API granting permission
* The {@code ApiKeyLevel} enum represents the granted access level of an API key used by the Permit SDK.
* The access level determine the scope of permissions granted by the API key: is the API granting permission
* to the entire workspace (i.e: organization), to a specific project within the workspace, or to a specific
* project and environment within the workspace.
*
* NOTE: Access levels are enforced on the backend using the API key (you cannot override this value).
* this enum is intended as a read-only representation to help the user understand his access level from code.
*/
public enum ApiKeyLevel {
WAIT_FOR_INIT,
ORGANIZATION_LEVEL_API_KEY,
PROJECT_LEVEL_API_KEY,
ENVIRONMENT_LEVEL_API_KEY,
WAIT_FOR_INIT(0),
ORGANIZATION_LEVEL_API_KEY(1),
PROJECT_LEVEL_API_KEY(2),
ENVIRONMENT_LEVEL_API_KEY(3);

private final int value;

ApiKeyLevel(int value) {
this.value = value;
}

public int getValue() {
return value;
}
}
4 changes: 4 additions & 0 deletions src/main/java/io/permit/sdk/PermitConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class PermitConfig {
private final String defaultTenant;
private final Boolean useDefaultTenantIfEmpty;
private PermitContext context;
public final String version;
private final static String defaultVersion = "1.3.1-rc";

private PermitConfig(Builder builder) {
this.token = builder.token;
Expand All @@ -41,6 +43,8 @@ private PermitConfig(Builder builder) {
this.defaultTenant = builder.defaultTenant;
this.useDefaultTenantIfEmpty = builder.useDefaultTenantIfEmpty;
this.context = builder.context;
String runtimeVersion = Permit.class.getPackage().getImplementationVersion();
this.version = (runtimeVersion == null) ? defaultVersion : runtimeVersion;
}

/**
Expand Down
185 changes: 168 additions & 17 deletions src/main/java/io/permit/sdk/PermitContext.java
Original file line number Diff line number Diff line change
@@ -1,40 +1,116 @@
package io.permit.sdk;

import io.permit.sdk.api.PermitContextChangeError;

/**
* The {@code PermitContext} class represents the context for Permit API calls.
*
* A context can be: the entire workspace, a specific project, or a specific environment.
* Most API methods require an environment scope (e.g: in order to create a role you
* need to know in which environment to place it), but some methods can work in higher
* scopes (for example: creating a project can be done with a workspace-level scope).
*
* Since the Permit API hierarchy is deeply nested, it is less convenient to specify
* the full object hierarchy in every request.
*
* For example, in order to list roles, the user needs to specify the (id or key) of the:
* - the org
* - the project
* - then environment
* in which the roles are located under.
*
* Instead, the SDK can "remember" the current context and "auto-complete" the details
* from that context.
*
* We then get this kind of experience:
* <pre>{@code
* permit.api.roles.list();
* }</pre>
*
* We can only run this function if the current context already knows the org, project,
* and environment that we want to run under, and that is why in this example the api
* method assumes we are running under an {@link ApiContextLevel#ENVIRONMENT} context.
*/
public class PermitContext {
private final ApiKeyLevel apiKeyLevel;
private final String org;
private final String project;
private final String environment;
private ApiKeyLevel apiKeyLevel;
private String permittedOrganization;
private String permittedProject;
private String permittedEnviroment;

private ApiContextLevel contextLevel;
private String org;
private String project;
private String environment;

/**
* Constructs a new instance of the {@code PermitContext} class with the specified builder.
*
* @param builder The builder used to construct the PermitContext.
*/
public PermitContext(Builder builder) {
this.apiKeyLevel = builder.apiKeyLevel;
this.saveApiKeyAccessibleScope(builder.org, builder.project, builder.environment);
this.contextLevel = builder.contextLevel;
this.org = builder.org;
this.project = builder.project;
this.environment = builder.environment;
}

public PermitContext() {
// access level
this.apiKeyLevel = ApiKeyLevel.WAIT_FOR_INIT;
this.permittedOrganization = null;
this.permittedProject = null;
this.permittedEnviroment = null;

// known context
this.contextLevel = ApiContextLevel.WAIT_FOR_INIT;
this.org = null;
this.project = null;
this.environment = null;
}

private void saveApiKeyAccessibleScope(String org, String project, String environment) {
// Do not call this method directly!
permittedOrganization = org; // cannot be null

if (project != null && environment != null) {
permittedProject = project;
permittedEnviroment = environment;
apiKeyLevel = ApiKeyLevel.ENVIRONMENT_LEVEL_API_KEY;
} else if (project != null) {
permittedProject = project;
permittedEnviroment = null;
apiKeyLevel = ApiKeyLevel.PROJECT_LEVEL_API_KEY;
} else {
permittedProject = null;
permittedEnviroment = null;
apiKeyLevel = ApiKeyLevel.ORGANIZATION_LEVEL_API_KEY;
}
}

/**
* Returns the API key level associated with the Permit context.
* Returns the access level of the API key used by the SDK.
*
* @return The API key level.
* @deprecated replaced with {@link PermitContext#getPermittedAccessLevel}
* @return The API key access level.
*/
public ApiKeyLevel getApiKeyLevel() {
return apiKeyLevel;
}

/**
* Returns the access level of the API key used by the SDK.
*
* @return The API key access level.
*/
public ApiKeyLevel getPermittedAccessLevel() {
return apiKeyLevel;
}

/**
* Returns the current SDK context level.
*
* @return the context level.
*/
public ApiContextLevel getContextLevel() {
return contextLevel;
}

/**
* Returns the organization (workspace) associated with the Permit context.
*
Expand Down Expand Up @@ -62,11 +138,86 @@ public String getEnvironment() {
return environment;
}

/**
* Sets the current context of the SDK to a specific organization.
*
* @param org The organization uuid.
* @throws PermitContextChangeError If the SDK context cannot be set due to insufficient API Key permissions.
*/
public void setOrganizationLevelContext(String org) throws PermitContextChangeError {
verifyCanAccessOrg(org);
this.contextLevel = ApiContextLevel.ORGANIZATION;
this.org = org;
this.project = null;
this.environment = null;
}

/**
* Sets the current context of the SDK to a specific project within an organization.
*
* @param org The organization uuid.
* @param project The project uuid.
* @throws PermitContextChangeError If the SDK context cannot be set due to insufficient API Key permissions.
*/
public void setProjectLevelContext(String org, String project) throws PermitContextChangeError {
verifyCanAccessProject(org, project);
this.contextLevel = ApiContextLevel.PROJECT;
this.org = org;
this.project = project;
this.environment = null;
}

/**
* Sets the current context of the SDK to a specific environment within an organization and project.
*
* @param org The organization uuid.
* @param project The project uuid.
* @param environment The environment uuid.
* @throws PermitContextChangeError If the SDK context cannot be set due to insufficient API Key permissions.
*/
public void setEnvironmentLevelContext(String org, String project, String environment) throws PermitContextChangeError {
verifyCanAccessEnvironment(org, project, environment);
this.contextLevel = ApiContextLevel.ENVIRONMENT;
this.org = org;
this.project = project;
this.environment = environment;
}

private void verifyCanAccessOrg(String org) throws PermitContextChangeError {
if (!org.equals(permittedOrganization)) {
throw new PermitContextChangeError(
"You cannot set an SDK context with org '" + org +
"' due to insufficient API Key permissions"
);
}
}

private void verifyCanAccessProject(String org, String project) throws PermitContextChangeError {
verifyCanAccessOrg(org);
if (permittedProject != null && !project.equals(permittedProject)) {
throw new PermitContextChangeError(
"You cannot set an SDK context with project '" + project +
"' due to insufficient API Key permissions"
);
}
}

private void verifyCanAccessEnvironment(String org, String project, String environment) throws PermitContextChangeError {
verifyCanAccessProject(org, project);
if (permittedEnviroment != null && !environment.equals(permittedEnviroment)) {
throw new PermitContextChangeError(
"You cannot set an SDK context with environment '" + environment +
"' due to insufficient API Key permissions"
);
}
}

/**
* The {@code Builder} class provides a builder interface for constructing {@code PermitContext} objects.
*/
public static class Builder {
private ApiKeyLevel apiKeyLevel;

private ApiContextLevel contextLevel;
private String org;
private String project;
private String environment;
Expand All @@ -78,7 +229,7 @@ public static class Builder {
* context will be stored in the SDK configuration for future API calls.
*/
public Builder() {
this.apiKeyLevel = ApiKeyLevel.WAIT_FOR_INIT;
this.contextLevel = ApiContextLevel.WAIT_FOR_INIT;
this.org = null;
this.project = null;
this.environment = null;
Expand All @@ -93,10 +244,10 @@ public Builder() {
* @return The updated {@code Builder} object.
*/
public Builder withOrganization(String org) {
this.contextLevel = ApiContextLevel.ORGANIZATION;
this.org = org;
this.project = null;
this.environment = null;
this.apiKeyLevel = ApiKeyLevel.ORGANIZATION_LEVEL_API_KEY;
return this;
}

Expand All @@ -110,10 +261,10 @@ public Builder withOrganization(String org) {
* @return The updated {@code Builder} object.
*/
public Builder withProject(String org, String project) {
this.contextLevel = ApiContextLevel.PROJECT;
this.org = org;
this.project = project;
this.environment = null;
this.apiKeyLevel = ApiKeyLevel.PROJECT_LEVEL_API_KEY;
return this;
}

Expand All @@ -127,10 +278,10 @@ public Builder withProject(String org, String project) {
* @return The updated {@code Builder} object.
*/
public Builder withEnvironment(String org, String project, String environment) {
this.contextLevel = ApiContextLevel.ENVIRONMENT;
this.org = org;
this.project = project;
this.environment = environment;
this.apiKeyLevel = ApiKeyLevel.ENVIRONMENT_LEVEL_API_KEY;
return this;
}

Expand Down
Loading

0 comments on commit 7f7a36f

Please sign in to comment.