From cb8249d74b419f9a98f71d97aeaf58b79ede27d8 Mon Sep 17 00:00:00 2001 From: Slavik Markovich Date: Wed, 11 Oct 2023 08:17:30 -0700 Subject: [PATCH] Authz support (#69) * Added authz support for mgmt * Updade version --- examples/management-cli/pom.xml | 2 +- pom.xml | 7 +- .../com/descope/enums/NodeExpressionType.java | 19 + src/main/java/com/descope/enums/NodeType.java | 19 + .../java/com/descope/literals/Routes.java | 17 + .../model/authz/HasRelationsResponse.java | 15 + .../model/authz/LoadSchemaResponse.java | 14 + .../com/descope/model/authz/Namespace.java | 19 + .../java/com/descope/model/authz/Node.java | 24 + .../descope/model/authz/NodeExpression.java | 22 + .../com/descope/model/authz/Relation.java | 24 + .../model/authz/RelationDefinition.java | 18 + .../descope/model/authz/RelationQuery.java | 21 + .../model/authz/RelationsResponse.java | 15 + .../java/com/descope/model/authz/Schema.java | 19 + .../com/descope/model/authz/UserQuery.java | 26 + .../model/authz/WhoCanAccessResponse.java | 15 + .../model/mgmt/ManagementServices.java | 2 + .../com/descope/sdk/mgmt/AuthzService.java | 152 ++++++ .../sdk/mgmt/impl/AuthzServiceImpl.java | 231 +++++++++ .../mgmt/impl/ManagementServiceBuilder.java | 1 + src/test/data/files.yaml | 130 +++++ .../sdk/mgmt/impl/AuthzServiceImplTest.java | 443 ++++++++++++++++++ .../sdk/mgmt/impl/UserServiceImplTest.java | 16 +- 24 files changed, 1268 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/descope/enums/NodeExpressionType.java create mode 100644 src/main/java/com/descope/enums/NodeType.java create mode 100644 src/main/java/com/descope/model/authz/HasRelationsResponse.java create mode 100644 src/main/java/com/descope/model/authz/LoadSchemaResponse.java create mode 100644 src/main/java/com/descope/model/authz/Namespace.java create mode 100644 src/main/java/com/descope/model/authz/Node.java create mode 100644 src/main/java/com/descope/model/authz/NodeExpression.java create mode 100644 src/main/java/com/descope/model/authz/Relation.java create mode 100644 src/main/java/com/descope/model/authz/RelationDefinition.java create mode 100644 src/main/java/com/descope/model/authz/RelationQuery.java create mode 100644 src/main/java/com/descope/model/authz/RelationsResponse.java create mode 100644 src/main/java/com/descope/model/authz/Schema.java create mode 100644 src/main/java/com/descope/model/authz/UserQuery.java create mode 100644 src/main/java/com/descope/model/authz/WhoCanAccessResponse.java create mode 100644 src/main/java/com/descope/sdk/mgmt/AuthzService.java create mode 100644 src/main/java/com/descope/sdk/mgmt/impl/AuthzServiceImpl.java create mode 100644 src/test/data/files.yaml create mode 100644 src/test/java/com/descope/sdk/mgmt/impl/AuthzServiceImplTest.java diff --git a/examples/management-cli/pom.xml b/examples/management-cli/pom.xml index 7bb5f7d5..81cf88cc 100644 --- a/examples/management-cli/pom.xml +++ b/examples/management-cli/pom.xml @@ -19,7 +19,7 @@ com.descope java-sdk - 1.0.7 + 1.0.8 info.picocli diff --git a/pom.xml b/pom.xml index aba64129..2e396c5b 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.descope java-sdk 4.0.0 - 1.0.7 + 1.0.8 ${project.groupId}:${project.artifactId} Java library used to integrate with Descope. https://github.com/descope/descope-java @@ -153,6 +153,11 @@ com.fasterxml.jackson.core 2.15.2 + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.15.2 + ${project.groupId}.${project.artifactId}-${project.version} diff --git a/src/main/java/com/descope/enums/NodeExpressionType.java b/src/main/java/com/descope/enums/NodeExpressionType.java new file mode 100644 index 00000000..a8b77c71 --- /dev/null +++ b/src/main/java/com/descope/enums/NodeExpressionType.java @@ -0,0 +1,19 @@ +package com.descope.enums; + +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; + +public enum NodeExpressionType { + SELF("self"), + TARGET_SET("targetSet"), + RELATION_LEFT("relationLeft"), + RELATION_RIGHT("relationRight"); + + @Getter + @JsonValue + private final String value; + + NodeExpressionType(String value) { + this.value = value; + } +} diff --git a/src/main/java/com/descope/enums/NodeType.java b/src/main/java/com/descope/enums/NodeType.java new file mode 100644 index 00000000..13d00185 --- /dev/null +++ b/src/main/java/com/descope/enums/NodeType.java @@ -0,0 +1,19 @@ +package com.descope.enums; + +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; + +public enum NodeType { + CHILD("child"), + UNION("union"), + INTERSECT("intersect"), + SUB("sub"); + + @Getter + @JsonValue + private final String value; + + NodeType(String value) { + this.value = value; + } +} diff --git a/src/main/java/com/descope/literals/Routes.java b/src/main/java/com/descope/literals/Routes.java index 3a82d879..98f90966 100644 --- a/src/main/java/com/descope/literals/Routes.java +++ b/src/main/java/com/descope/literals/Routes.java @@ -140,5 +140,22 @@ public static class ManagementEndPoints { // Audit public static final String MANAGEMENT_AUDIT_SEARCH_LINK = "/v1/mgmt/audit/search"; + + // Authz + public static final String MANAGEMENT_AUTHZ_SCHEMA_SAVE = "/v1/mgmt/authz/schema/save"; + public static final String MANAGEMENT_AUTHZ_SCHEMA_DELETE = "/v1/mgmt/authz/schema/delete"; + public static final String MANAGEMENT_AUTHZ_SCHEMA_LOAD = "/v1/mgmt/authz/schema/load"; + public static final String MANAGEMENT_AUTHZ_NS_SAVE = "/v1/mgmt/authz/ns/save"; + public static final String MANAGEMENT_AUTHZ_NS_DELETE = "/v1/mgmt/authz/ns/delete"; + public static final String MANAGEMENT_AUTHZ_RD_SAVE = "/v1/mgmt/authz/rd/save"; + public static final String MANAGEMENT_AUTHZ_RD_DELETE = "/v1/mgmt/authz/rd/delete"; + public static final String MANAGEMENT_AUTHZ_RE_CREATE = "/v1/mgmt/authz/re/create"; + public static final String MANAGEMENT_AUTHZ_RE_DELETE = "/v1/mgmt/authz/re/delete"; + public static final String MANAGEMENT_AUTHZ_RE_DELETE_RESOURCES = "/v1/mgmt/authz/re/deleteresources"; + public static final String MANAGEMENT_AUTHZ_RE_HAS_RELATIONS = "/v1/mgmt/authz/re/has"; + public static final String MANAGEMENT_AUTHZ_RE_WHO = "/v1/mgmt/authz/re/who"; + public static final String MANAGEMENT_AUTHZ_RE_RESOURCE = "/v1/mgmt/authz/re/resource"; + public static final String MANAGEMENT_AUTHZ_RE_TARGETS = "/v1/mgmt/authz/re/targets"; + public static final String MANAGEMENT_AUTHZ_RE_TARGET_ALL = "/v1/mgmt/authz/re/targetall"; } } diff --git a/src/main/java/com/descope/model/authz/HasRelationsResponse.java b/src/main/java/com/descope/model/authz/HasRelationsResponse.java new file mode 100644 index 00000000..7b770460 --- /dev/null +++ b/src/main/java/com/descope/model/authz/HasRelationsResponse.java @@ -0,0 +1,15 @@ +package com.descope.model.authz; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HasRelationsResponse { + List relationQueries; +} diff --git a/src/main/java/com/descope/model/authz/LoadSchemaResponse.java b/src/main/java/com/descope/model/authz/LoadSchemaResponse.java new file mode 100644 index 00000000..647bed93 --- /dev/null +++ b/src/main/java/com/descope/model/authz/LoadSchemaResponse.java @@ -0,0 +1,14 @@ +package com.descope.model.authz; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LoadSchemaResponse { + Schema schema; +} diff --git a/src/main/java/com/descope/model/authz/Namespace.java b/src/main/java/com/descope/model/authz/Namespace.java new file mode 100644 index 00000000..a9c86792 --- /dev/null +++ b/src/main/java/com/descope/model/authz/Namespace.java @@ -0,0 +1,19 @@ +package com.descope.model.authz; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(Include.NON_NULL) +public class Namespace { + String name; + List relationDefinitions; +} diff --git a/src/main/java/com/descope/model/authz/Node.java b/src/main/java/com/descope/model/authz/Node.java new file mode 100644 index 00000000..4b390448 --- /dev/null +++ b/src/main/java/com/descope/model/authz/Node.java @@ -0,0 +1,24 @@ +package com.descope.model.authz; + +import com.descope.enums.NodeType; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@SuppressWarnings("checkstyle:MemberName") +@JsonInclude(Include.NON_NULL) +public class Node { + @JsonProperty("nType") + NodeType nType; + List children; + NodeExpression expression; +} diff --git a/src/main/java/com/descope/model/authz/NodeExpression.java b/src/main/java/com/descope/model/authz/NodeExpression.java new file mode 100644 index 00000000..a4f1e48a --- /dev/null +++ b/src/main/java/com/descope/model/authz/NodeExpression.java @@ -0,0 +1,22 @@ +package com.descope.model.authz; + +import com.descope.enums.NodeExpressionType; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(Include.NON_NULL) +public class NodeExpression { + NodeExpressionType neType; + String relationDefinition; + String relationDefinitionNamespace; + String targetRelationDefinition; + String targetRelationDefinitionNamespace; +} diff --git a/src/main/java/com/descope/model/authz/Relation.java b/src/main/java/com/descope/model/authz/Relation.java new file mode 100644 index 00000000..d7ee8433 --- /dev/null +++ b/src/main/java/com/descope/model/authz/Relation.java @@ -0,0 +1,24 @@ +package com.descope.model.authz; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(Include.NON_NULL) +public class Relation { + String resource; + String relationDefinition; + String namespace; + String target; + String targetSetResource; + String targetSetRelationDefinition; + String targetSetRelationDefinitionNamespace; + UserQuery query; +} diff --git a/src/main/java/com/descope/model/authz/RelationDefinition.java b/src/main/java/com/descope/model/authz/RelationDefinition.java new file mode 100644 index 00000000..1e28b4dd --- /dev/null +++ b/src/main/java/com/descope/model/authz/RelationDefinition.java @@ -0,0 +1,18 @@ +package com.descope.model.authz; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(Include.NON_NULL) +public class RelationDefinition { + String name; + Node complexDefinition; +} diff --git a/src/main/java/com/descope/model/authz/RelationQuery.java b/src/main/java/com/descope/model/authz/RelationQuery.java new file mode 100644 index 00000000..a350cbf5 --- /dev/null +++ b/src/main/java/com/descope/model/authz/RelationQuery.java @@ -0,0 +1,21 @@ +package com.descope.model.authz; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(Include.NON_NULL) +public class RelationQuery { + String resource; + String relationDefinition; + String namespace; + String target; + boolean hasRelation; +} diff --git a/src/main/java/com/descope/model/authz/RelationsResponse.java b/src/main/java/com/descope/model/authz/RelationsResponse.java new file mode 100644 index 00000000..deb9197b --- /dev/null +++ b/src/main/java/com/descope/model/authz/RelationsResponse.java @@ -0,0 +1,15 @@ +package com.descope.model.authz; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RelationsResponse { + List relations; +} diff --git a/src/main/java/com/descope/model/authz/Schema.java b/src/main/java/com/descope/model/authz/Schema.java new file mode 100644 index 00000000..c5669f47 --- /dev/null +++ b/src/main/java/com/descope/model/authz/Schema.java @@ -0,0 +1,19 @@ +package com.descope.model.authz; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(Include.NON_NULL) +public class Schema { + String name; + List namespaces; +} diff --git a/src/main/java/com/descope/model/authz/UserQuery.java b/src/main/java/com/descope/model/authz/UserQuery.java new file mode 100644 index 00000000..775f83b1 --- /dev/null +++ b/src/main/java/com/descope/model/authz/UserQuery.java @@ -0,0 +1,26 @@ +package com.descope.model.authz; + +import com.descope.enums.UserStatus; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(Include.NON_NULL) +public class UserQuery { + List tenants; + List roles; + String text; + List statuses; + boolean ssoOnly; + boolean withTestUser; + Map customAttributes; +} diff --git a/src/main/java/com/descope/model/authz/WhoCanAccessResponse.java b/src/main/java/com/descope/model/authz/WhoCanAccessResponse.java new file mode 100644 index 00000000..66499143 --- /dev/null +++ b/src/main/java/com/descope/model/authz/WhoCanAccessResponse.java @@ -0,0 +1,15 @@ +package com.descope.model.authz; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WhoCanAccessResponse { + List targets; +} diff --git a/src/main/java/com/descope/model/mgmt/ManagementServices.java b/src/main/java/com/descope/model/mgmt/ManagementServices.java index c62498e1..f85f6816 100644 --- a/src/main/java/com/descope/model/mgmt/ManagementServices.java +++ b/src/main/java/com/descope/model/mgmt/ManagementServices.java @@ -2,6 +2,7 @@ import com.descope.sdk.mgmt.AccessKeyService; import com.descope.sdk.mgmt.AuditService; +import com.descope.sdk.mgmt.AuthzService; import com.descope.sdk.mgmt.FlowService; import com.descope.sdk.mgmt.GroupService; import com.descope.sdk.mgmt.JwtService; @@ -26,4 +27,5 @@ public class ManagementServices { FlowService flowService; GroupService groupService; AuditService auditService; + AuthzService authzService; } diff --git a/src/main/java/com/descope/sdk/mgmt/AuthzService.java b/src/main/java/com/descope/sdk/mgmt/AuthzService.java new file mode 100644 index 00000000..786f4d57 --- /dev/null +++ b/src/main/java/com/descope/sdk/mgmt/AuthzService.java @@ -0,0 +1,152 @@ +package com.descope.sdk.mgmt; + +import com.descope.exception.DescopeException; +import com.descope.model.authz.Namespace; +import com.descope.model.authz.Relation; +import com.descope.model.authz.RelationDefinition; +import com.descope.model.authz.RelationQuery; +import com.descope.model.authz.Schema; +import java.util.List; + +/** Provides ReBAC authorization service APIs. */ +public interface AuthzService { + /** + * Save (create or update) the given schema. + * In case of update, will update only given namespaces and will not delete namespaces unless upgrade flag is true. + * Schema name can be used for projects to track versioning. + * + * @param schema {@link Schema} to save. + * @throws DescopeException If there occurs any exception, a subtype of this exception will be thrown. + */ + void saveSchema(Schema schema, boolean upgrade) throws DescopeException; + + /** + * Delete the schema for the project which will also delete all relations. + * + * @throws DescopeException If there occurs any exception, a subtype of this exception will be thrown. + */ + void deleteSchema() throws DescopeException; + + /** + * Load the schema for the project. + * + * @return {@link Schema} + * @throws DescopeException If there occurs any exception, a subtype of this exception will be thrown. + */ + Schema loadSchema() throws DescopeException; + + /** + * Save (create or update) the given namespace. + * Will not delete relation definitions not mentioned in the namespace. + * + * @param namespace {@link Namespace} to save. + * @param oldName if we are changing the namespace name, what was the old name we are updating. + * @param schemaName optional and used to track the current schema version. + * @throws DescopeException If there occurs any exception, a subtype of this exception will be thrown. + */ + void saveNamespace(Namespace namespace, String oldName, String schemaName) throws DescopeException; + + /** + * Delete the given namespace. + * Will also delete the relevant relations. + * + * @param name to delete. + * @param schemaName optional and used to track the current schema version. + * @throws DescopeException If there occurs any exception, a subtype of this exception will be thrown. + */ + void deleteNamespace(String name, String schemaName) throws DescopeException; + + /** + * Save (create or update) the given relation definition. + * + * @param relationDefinition {@link RelationDefinition} to save. + * @param namespace that it belongs to. + * @param oldName if we are changing the relation definition name, what was the old name we are updating. + * @param schemaName optional and used to track the current schema version. + * @throws DescopeException If there occurs any exception, a subtype of this exception will be thrown. + */ + void saveRelationDefinition(RelationDefinition relationDefinition, String namespace, String oldName, + String schemaName) throws DescopeException; + + /** + * Delete the given relation definition. + * Will also delete the relevant relations. + * + * @param name to delete. + * @param namespace it belongs to. + * @param schemaName optional and used to track the current schema version. + * @throws DescopeException If there occurs any exception, a subtype of this exception will be thrown. + */ + void deleteRelationDefinition(String name, String namespace, String schemaName) throws DescopeException; + + /** + * Create the given relations. + * + * @param relations {@link List} of {@link Relation} to create. + * @throws DescopeException If there occurs any exception, a subtype of this exception will be thrown. + */ + void createRelations(List relations) throws DescopeException; + + /** + * Delete the given relations. + * + * @param relations {@link List} of {@link Relation} to delete. + * @throws DescopeException If there occurs any exception, a subtype of this exception will be thrown. + */ + void deleteRelations(List relations) throws DescopeException; + + /** + * Delete the given relations. + * + * @param resources {@link List} of resources to delete. + * @throws DescopeException If there occurs any exception, a subtype of this exception will be thrown. + */ + void deleteRelationsForResources(List resources) throws DescopeException; + + /** + * Query relations to see what relations exists. + * + * @param relationQueries {@link List} of {@link RelationQuery} to check. + * @return {@link List} of {@link RelationQuery} responses with the boolean flag indicating if relation exists + * @throws DescopeException If there occurs any exception, a subtype of this exception will be thrown. + */ + List hasRelations(List relationQueries) throws DescopeException; + + /** + * List all the users that have the given relation definition to the given resource. + * + * @param resource The resource we are checking + * @param relationDefinition The relation definition we are querying + * @param namespace The namespace for the relation definition + * @return {@link List} of users who have the given relation definition + * @throws DescopeException If there occurs any exception, a subtype of this exception will be thrown. + */ + List whoCanAccess(String resource, String relationDefinition, String namespace) throws DescopeException; + + /** + * Return the list of all defined relations (not recursive) on the given resource. + * + * @param resource The resource we are checking + * @return {@link List} of {@link Relation} that exist for the given resource + * @throws DescopeException If there occurs any exception, a subtype of this exception will be thrown. + */ + List resourceRelations(String resource) throws DescopeException; + + /** + * Return the list of all defined relations (not recursive) for the given targets. + * + * @param targets {@link List} of targets we want to check + * @return {@link List} of {@link Relation} that exist for the given targets + * @throws DescopeException If there occurs any exception, a subtype of this exception will be thrown. + */ + List targetsRelations(List targets) throws DescopeException; + + /** + * Return the list of all relations for the given target including derived relations from the schema tree. + * + * @param target The target to check relations for + * @return {@link List} of {@link Relation} that exist for the given target + * @throws DescopeException If there occurs any exception, a subtype of this exception will be thrown. + */ + List whatCanTargetAccess(String target) throws DescopeException; +} diff --git a/src/main/java/com/descope/sdk/mgmt/impl/AuthzServiceImpl.java b/src/main/java/com/descope/sdk/mgmt/impl/AuthzServiceImpl.java new file mode 100644 index 00000000..e47de02f --- /dev/null +++ b/src/main/java/com/descope/sdk/mgmt/impl/AuthzServiceImpl.java @@ -0,0 +1,231 @@ +package com.descope.sdk.mgmt.impl; + +import static com.descope.literals.Routes.ManagementEndPoints.MANAGEMENT_AUTHZ_NS_DELETE; +import static com.descope.literals.Routes.ManagementEndPoints.MANAGEMENT_AUTHZ_NS_SAVE; +import static com.descope.literals.Routes.ManagementEndPoints.MANAGEMENT_AUTHZ_RD_DELETE; +import static com.descope.literals.Routes.ManagementEndPoints.MANAGEMENT_AUTHZ_RD_SAVE; +import static com.descope.literals.Routes.ManagementEndPoints.MANAGEMENT_AUTHZ_RE_CREATE; +import static com.descope.literals.Routes.ManagementEndPoints.MANAGEMENT_AUTHZ_RE_DELETE; +import static com.descope.literals.Routes.ManagementEndPoints.MANAGEMENT_AUTHZ_RE_DELETE_RESOURCES; +import static com.descope.literals.Routes.ManagementEndPoints.MANAGEMENT_AUTHZ_RE_HAS_RELATIONS; +import static com.descope.literals.Routes.ManagementEndPoints.MANAGEMENT_AUTHZ_RE_RESOURCE; +import static com.descope.literals.Routes.ManagementEndPoints.MANAGEMENT_AUTHZ_RE_TARGETS; +import static com.descope.literals.Routes.ManagementEndPoints.MANAGEMENT_AUTHZ_RE_TARGET_ALL; +import static com.descope.literals.Routes.ManagementEndPoints.MANAGEMENT_AUTHZ_RE_WHO; +import static com.descope.literals.Routes.ManagementEndPoints.MANAGEMENT_AUTHZ_SCHEMA_DELETE; +import static com.descope.literals.Routes.ManagementEndPoints.MANAGEMENT_AUTHZ_SCHEMA_LOAD; +import static com.descope.literals.Routes.ManagementEndPoints.MANAGEMENT_AUTHZ_SCHEMA_SAVE; + +import com.descope.exception.DescopeException; +import com.descope.exception.ServerCommonException; +import com.descope.model.authz.HasRelationsResponse; +import com.descope.model.authz.LoadSchemaResponse; +import com.descope.model.authz.Namespace; +import com.descope.model.authz.Relation; +import com.descope.model.authz.RelationDefinition; +import com.descope.model.authz.RelationQuery; +import com.descope.model.authz.RelationsResponse; +import com.descope.model.authz.Schema; +import com.descope.model.authz.WhoCanAccessResponse; +import com.descope.model.client.Client; +import com.descope.model.mgmt.ManagementParams; +import com.descope.sdk.mgmt.AuthzService; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; + +class AuthzServiceImpl extends ManagementsBase implements AuthzService { + + AuthzServiceImpl(Client client, ManagementParams managementParams) { + super(client, managementParams); + } + + @Override + public void saveSchema(Schema schema, boolean upgrade) throws DescopeException { + if (schema == null) { + throw ServerCommonException.invalidArgument("schema"); + } + if (schema.getNamespaces() == null || schema.getNamespaces().isEmpty()) { + throw ServerCommonException.invalidArgument("schema"); + } + var apiProxy = getApiProxy(); + Map request = Map.of("schema", schema, "upgrade", upgrade); + apiProxy.post(getUri(MANAGEMENT_AUTHZ_SCHEMA_SAVE), request, Void.class); + } + + @Override + public void deleteSchema() throws DescopeException { + var apiProxy = getApiProxy(); + Map request = Map.of(); + apiProxy.post(getUri(MANAGEMENT_AUTHZ_SCHEMA_DELETE), request, Void.class); + } + + @Override + public Schema loadSchema() throws DescopeException { + var apiProxy = getApiProxy(); + Map request = Map.of(); + var resp = apiProxy.post(getUri(MANAGEMENT_AUTHZ_SCHEMA_LOAD), request, LoadSchemaResponse.class); + return resp.getSchema(); + } + + @Override + public void saveNamespace(Namespace namespace, String oldName, String schemaName) throws DescopeException { + if (namespace == null || StringUtils.isBlank(namespace.getName()) || namespace.getRelationDefinitions() == null + || namespace.getRelationDefinitions().isEmpty()) { + throw ServerCommonException.invalidArgument("namespace"); + } + var apiProxy = getApiProxy(); + Map request = new HashMap<>(Map.of("namespace", namespace)); + if (!StringUtils.isBlank(oldName)) { + request.put("oldName", oldName); + } + if (!StringUtils.isBlank(schemaName)) { + request.put("schemaName", schemaName); + } + apiProxy.post(getUri(MANAGEMENT_AUTHZ_NS_SAVE), request, Void.class); + } + + @Override + public void deleteNamespace(String name, String schemaName) throws DescopeException { + if (StringUtils.isBlank(name)) { + throw ServerCommonException.invalidArgument("name"); + } + var apiProxy = getApiProxy(); + Map request = new HashMap<>(Map.of("name", name)); + if (!StringUtils.isBlank(schemaName)) { + request.put("schemaName", schemaName); + } + apiProxy.post(getUri(MANAGEMENT_AUTHZ_NS_DELETE), request, Void.class); + } + + @Override + public void saveRelationDefinition(RelationDefinition relationDefinition, String namespace, String oldName, + String schemaName) throws DescopeException { + if (relationDefinition == null || StringUtils.isBlank(relationDefinition.getName())) { + throw ServerCommonException.invalidArgument("relationDefinition"); + } + if (StringUtils.isBlank(namespace)) { + throw ServerCommonException.invalidArgument("namespace"); + } + var apiProxy = getApiProxy(); + Map request = + new HashMap<>(Map.of("relationDefinition", relationDefinition, "namespace", namespace)); + if (!StringUtils.isBlank(oldName)) { + request.put("oldName", oldName); + } + if (!StringUtils.isBlank(schemaName)) { + request.put("schemaName", schemaName); + } + apiProxy.post(getUri(MANAGEMENT_AUTHZ_RD_SAVE), request, Void.class); + } + + @Override + public void deleteRelationDefinition(String name, String namespace, String schemaName) throws DescopeException { + if (StringUtils.isBlank(name)) { + throw ServerCommonException.invalidArgument("name"); + } + if (StringUtils.isBlank(namespace)) { + throw ServerCommonException.invalidArgument("namespace"); + } + var apiProxy = getApiProxy(); + Map request = new HashMap<>(Map.of("name", name, "namespace", namespace)); + if (!StringUtils.isBlank(schemaName)) { + request.put("schemaName", schemaName); + } + apiProxy.post(getUri(MANAGEMENT_AUTHZ_RD_DELETE), request, Void.class); + } + + @Override + public void createRelations(List relations) throws DescopeException { + if (relations == null || relations.isEmpty()) { + throw ServerCommonException.invalidArgument("relations"); + } + var apiProxy = getApiProxy(); + Map request = Map.of("relations", relations); + apiProxy.post(getUri(MANAGEMENT_AUTHZ_RE_CREATE), request, Void.class); + } + + @Override + public void deleteRelations(List relations) throws DescopeException { + if (relations == null || relations.isEmpty()) { + throw ServerCommonException.invalidArgument("relations"); + } + var apiProxy = getApiProxy(); + Map request = Map.of("relations", relations); + apiProxy.post(getUri(MANAGEMENT_AUTHZ_RE_DELETE), request, Void.class); + } + + @Override + public void deleteRelationsForResources(List resources) throws DescopeException { + if (resources == null || resources.isEmpty()) { + throw ServerCommonException.invalidArgument("resources"); + } + var apiProxy = getApiProxy(); + Map request = Map.of("resources", resources); + apiProxy.post(getUri(MANAGEMENT_AUTHZ_RE_DELETE_RESOURCES), request, Void.class); + } + + @Override + public List hasRelations(List relationQueries) throws DescopeException { + if (relationQueries == null || relationQueries.isEmpty()) { + throw ServerCommonException.invalidArgument("relationQueries"); + } + var apiProxy = getApiProxy(); + Map request = Map.of("relationQueries", relationQueries); + var resp = apiProxy.post(getUri(MANAGEMENT_AUTHZ_RE_HAS_RELATIONS), request, HasRelationsResponse.class); + return resp.getRelationQueries(); + } + + @Override + public List whoCanAccess(String resource, String relationDefinition, String namespace) + throws DescopeException { + if (StringUtils.isBlank(resource)) { + throw ServerCommonException.invalidArgument("resource"); + } + if (StringUtils.isBlank(relationDefinition)) { + throw ServerCommonException.invalidArgument("relationDefinition"); + } + if (StringUtils.isBlank(namespace)) { + throw ServerCommonException.invalidArgument("namespace"); + } + var apiProxy = getApiProxy(); + Map request = + Map.of("resource", resource, "relationDefinition", relationDefinition, "namespace", namespace); + var resp = apiProxy.post(getUri(MANAGEMENT_AUTHZ_RE_WHO), request, WhoCanAccessResponse.class); + return resp.getTargets(); + } + + @Override + public List resourceRelations(String resource) throws DescopeException { + if (StringUtils.isBlank(resource)) { + throw ServerCommonException.invalidArgument("resource"); + } + var apiProxy = getApiProxy(); + Map request = Map.of("resource", resource); + var resp = apiProxy.post(getUri(MANAGEMENT_AUTHZ_RE_RESOURCE), request, RelationsResponse.class); + return resp.getRelations(); + } + + @Override + public List targetsRelations(List targets) throws DescopeException { + if (targets == null || targets.isEmpty()) { + throw ServerCommonException.invalidArgument("targets"); + } + var apiProxy = getApiProxy(); + Map request = Map.of("targets", targets); + var resp = apiProxy.post(getUri(MANAGEMENT_AUTHZ_RE_TARGETS), request, RelationsResponse.class); + return resp.getRelations(); + } + + @Override + public List whatCanTargetAccess(String target) throws DescopeException { + if (StringUtils.isBlank(target)) { + throw ServerCommonException.invalidArgument("user"); + } + var apiProxy = getApiProxy(); + Map request = Map.of("target", target); + var resp = apiProxy.post(getUri(MANAGEMENT_AUTHZ_RE_TARGET_ALL), request, RelationsResponse.class); + return resp.getRelations(); + } +} diff --git a/src/main/java/com/descope/sdk/mgmt/impl/ManagementServiceBuilder.java b/src/main/java/com/descope/sdk/mgmt/impl/ManagementServiceBuilder.java index 09a493f8..ca88af15 100644 --- a/src/main/java/com/descope/sdk/mgmt/impl/ManagementServiceBuilder.java +++ b/src/main/java/com/descope/sdk/mgmt/impl/ManagementServiceBuilder.java @@ -19,6 +19,7 @@ public static ManagementServices buildServices(Client client, ManagementParams m .accessKeyService(new AccessKeyServiceImpl(client, managementParams)) .permissionService(new PermissionServiceImpl(client, managementParams)) .auditService(new AuditServiceImpl(client, managementParams)) + .authzService(new AuthzServiceImpl(client, managementParams)) .build(); } } diff --git a/src/test/data/files.yaml b/src/test/data/files.yaml new file mode 100644 index 00000000..97f5bc77 --- /dev/null +++ b/src/test/data/files.yaml @@ -0,0 +1,130 @@ +# Example schema for the authz tests +name: Files +namespaces: + - name: org + relationDefinitions: + - name: parent + - name: member + complexDefinition: + nType: union + children: + - nType: child + expression: + neType: self + - nType: child + expression: + neType: relationLeft + relationDefinition: parent + relationDefinitionNamespace: org + targetRelationDefinition: member + targetRelationDefinitionNamespace: org + - name: folder + relationDefinitions: + - name: parent + - name: owner + complexDefinition: + nType: union + children: + - nType: child + expression: + neType: self + - nType: child + expression: + neType: relationRight + relationDefinition: parent + relationDefinitionNamespace: folder + targetRelationDefinition: owner + targetRelationDefinitionNamespace: folder + - name: editor + complexDefinition: + nType: union + children: + - nType: child + expression: + neType: self + - nType: child + expression: + neType: relationRight + relationDefinition: parent + relationDefinitionNamespace: folder + targetRelationDefinition: editor + targetRelationDefinitionNamespace: folder + - nType: child + expression: + neType: targetSet + targetRelationDefinition: owner + targetRelationDefinitionNamespace: folder + - name: viewer + complexDefinition: + nType: union + children: + - nType: child + expression: + neType: self + - nType: child + expression: + neType: relationRight + relationDefinition: parent + relationDefinitionNamespace: folder + targetRelationDefinition: viewer + targetRelationDefinitionNamespace: folder + - nType: child + expression: + neType: targetSet + targetRelationDefinition: editor + targetRelationDefinitionNamespace: folder + - name: doc + relationDefinitions: + - name: parent + - name: owner + complexDefinition: + nType: union + children: + - nType: child + expression: + neType: self + - nType: child + expression: + neType: relationRight + relationDefinition: parent + relationDefinitionNamespace: doc + targetRelationDefinition: owner + targetRelationDefinitionNamespace: folder + - name: editor + complexDefinition: + nType: union + children: + - nType: child + expression: + neType: self + - nType: child + expression: + neType: relationRight + relationDefinition: parent + relationDefinitionNamespace: doc + targetRelationDefinition: editor + targetRelationDefinitionNamespace: folder + - nType: child + expression: + neType: targetSet + targetRelationDefinition: owner + targetRelationDefinitionNamespace: doc + - name: viewer + complexDefinition: + nType: union + children: + - nType: child + expression: + neType: self + - nType: child + expression: + neType: relationRight + relationDefinition: parent + relationDefinitionNamespace: doc + targetRelationDefinition: viewer + targetRelationDefinitionNamespace: folder + - nType: child + expression: + neType: targetSet + targetRelationDefinition: editor + targetRelationDefinitionNamespace: doc diff --git a/src/test/java/com/descope/sdk/mgmt/impl/AuthzServiceImplTest.java b/src/test/java/com/descope/sdk/mgmt/impl/AuthzServiceImplTest.java new file mode 100644 index 00000000..0f7477d5 --- /dev/null +++ b/src/test/java/com/descope/sdk/mgmt/impl/AuthzServiceImplTest.java @@ -0,0 +1,443 @@ +package com.descope.sdk.mgmt.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; + +import com.descope.exception.RateLimitExceededException; +import com.descope.exception.ServerCommonException; +import com.descope.model.authz.HasRelationsResponse; +import com.descope.model.authz.LoadSchemaResponse; +import com.descope.model.authz.Namespace; +import com.descope.model.authz.Relation; +import com.descope.model.authz.RelationDefinition; +import com.descope.model.authz.RelationQuery; +import com.descope.model.authz.RelationsResponse; +import com.descope.model.authz.Schema; +import com.descope.model.authz.WhoCanAccessResponse; +import com.descope.proxy.ApiProxy; +import com.descope.proxy.impl.ApiProxyBuilder; +import com.descope.sdk.TestUtils; +import com.descope.sdk.mgmt.AuthzService; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import java.io.File; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.RetryingTest; +import org.mockito.MockedStatic; + +public class AuthzServiceImplTest { + private AuthzService authzService; + + @BeforeEach + void setUp() { + var authParams = TestUtils.getManagementParams(); + var client = TestUtils.getClient(); + var mgmtServices = ManagementServiceBuilder.buildServices(client, authParams); + this.authzService = mgmtServices.getAuthzService(); + } + + @Test + void testSaveSchemaForNoSchema() { + ServerCommonException thrown = + assertThrows( + ServerCommonException.class, + () -> authzService.saveSchema(null, false)); + assertNotNull(thrown); + assertEquals("The schema argument is invalid", thrown.getMessage()); + } + + @Test + void testSaveSchemaForEmptySchema() { + ServerCommonException thrown = + assertThrows( + ServerCommonException.class, + () -> authzService.saveSchema(new Schema(), false)); + assertNotNull(thrown); + assertEquals("The schema argument is invalid", thrown.getMessage()); + } + + @Test + void testSaveSchemaForSuccess() { + var apiProxy = mock(ApiProxy.class); + doReturn(null).when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + authzService.saveSchema(Schema.builder().namespaces(List.of(new Namespace())).build(), false); + } + } + + @Test + void testDeleteSchemaForSuccess() { + var apiProxy = mock(ApiProxy.class); + doReturn(null).when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + authzService.deleteSchema(); + } + } + + @Test + void testLoadSchemaForSuccess() { + var schemaResponse = new LoadSchemaResponse(new Schema("kuku", null)); + var apiProxy = mock(ApiProxy.class); + doReturn(schemaResponse).when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + var schema = authzService.loadSchema(); + assertNotNull(schema); + assertEquals("kuku", schema.getName()); + } + } + + @Test + void testSaveNamespaceForInvalidNamespace() { + ServerCommonException thrown = + assertThrows( + ServerCommonException.class, + () -> authzService.saveNamespace(null, null, null)); + assertNotNull(thrown); + assertEquals("The namespace argument is invalid", thrown.getMessage()); + thrown = + assertThrows( + ServerCommonException.class, + () -> authzService.saveNamespace(new Namespace(), null, null)); + assertNotNull(thrown); + assertEquals("The namespace argument is invalid", thrown.getMessage()); + thrown = + assertThrows( + ServerCommonException.class, + () -> authzService.saveNamespace(Namespace.builder().name("kuku").build(), null, null)); + assertNotNull(thrown); + assertEquals("The namespace argument is invalid", thrown.getMessage()); + } + + @Test + void testSaveNamespaceForSuccess() { + var apiProxy = mock(ApiProxy.class); + doReturn(null).when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + authzService.saveNamespace(new Namespace("kuku", List.of(new RelationDefinition())), null, null); + } + } + + @Test + void testDeleteNamespaceForNoName() { + ServerCommonException thrown = + assertThrows( + ServerCommonException.class, + () -> authzService.deleteNamespace(null, null)); + assertNotNull(thrown); + assertEquals("The name argument is invalid", thrown.getMessage()); + } + + @Test + void testDeleteNamespaceForSuccess() { + var apiProxy = mock(ApiProxy.class); + doReturn(null).when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + authzService.deleteNamespace("kuku", null); + } + } + + @Test + void testSaveRelationDefinitionForInvalidRD() { + ServerCommonException thrown = + assertThrows( + ServerCommonException.class, + () -> authzService.saveRelationDefinition(null, null, null, null)); + assertNotNull(thrown); + assertEquals("The relationDefinition argument is invalid", thrown.getMessage()); + thrown = + assertThrows( + ServerCommonException.class, + () -> authzService.saveRelationDefinition(new RelationDefinition(), null, null, null)); + assertNotNull(thrown); + assertEquals("The relationDefinition argument is invalid", thrown.getMessage()); + thrown = + assertThrows( + ServerCommonException.class, + () -> authzService.saveRelationDefinition(RelationDefinition.builder().name("kuku").build(), + null, null, null)); + assertNotNull(thrown); + assertEquals("The namespace argument is invalid", thrown.getMessage()); + } + + @Test + void testSaveRelationDefinitionForSuccess() { + var apiProxy = mock(ApiProxy.class); + doReturn(null).when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + authzService.saveRelationDefinition(new RelationDefinition("kuku", null), "kiki", null, null); + } + } + + @Test + void testDeleteRelationDefinitionForNoName() { + ServerCommonException thrown = + assertThrows( + ServerCommonException.class, + () -> authzService.deleteRelationDefinition(null, null, null)); + assertNotNull(thrown); + assertEquals("The name argument is invalid", thrown.getMessage()); + thrown = + assertThrows( + ServerCommonException.class, + () -> authzService.deleteRelationDefinition("kuku", null, null)); + assertNotNull(thrown); + assertEquals("The namespace argument is invalid", thrown.getMessage()); + } + + @Test + void testDeleteRelationDefinitionForSuccess() { + var apiProxy = mock(ApiProxy.class); + doReturn(null).when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + authzService.deleteRelationDefinition("kuku", "kiki", null); + } + } + + @Test + void testCreateRelationsForNoRelations() { + ServerCommonException thrown = + assertThrows( + ServerCommonException.class, + () -> authzService.createRelations(null)); + assertNotNull(thrown); + assertEquals("The relations argument is invalid", thrown.getMessage()); + } + + @Test + void testCreateRelationsForSuccess() { + var apiProxy = mock(ApiProxy.class); + doReturn(null).when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + authzService.createRelations(List.of(new Relation())); + } + } + + @Test + void testDeleteRelationsForNoRelations() { + ServerCommonException thrown = + assertThrows( + ServerCommonException.class, + () -> authzService.deleteRelations(null)); + assertNotNull(thrown); + assertEquals("The relations argument is invalid", thrown.getMessage()); + } + + @Test + void testDeleteRelationsForSuccess() { + var apiProxy = mock(ApiProxy.class); + doReturn(null).when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + authzService.deleteRelations(List.of(new Relation())); + } + } + + @Test + void testDeleteRelationsForResourcesForNoResources() { + ServerCommonException thrown = + assertThrows( + ServerCommonException.class, + () -> authzService.deleteRelationsForResources(null)); + assertNotNull(thrown); + assertEquals("The resources argument is invalid", thrown.getMessage()); + } + + @Test + void testDeleteRelationsForResourcesForSuccess() { + var apiProxy = mock(ApiProxy.class); + doReturn(null).when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + authzService.deleteRelationsForResources(List.of("kuku")); + } + } + + @Test + void testHasRelationsForNoQueries() { + ServerCommonException thrown = + assertThrows( + ServerCommonException.class, + () -> authzService.hasRelations(null)); + assertNotNull(thrown); + assertEquals("The relationQueries argument is invalid", thrown.getMessage()); + } + + @Test + void testHasRelationsForSuccess() { + var apiProxy = mock(ApiProxy.class); + doReturn(new HasRelationsResponse(List.of(new RelationQuery()))).when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + authzService.hasRelations(List.of(new RelationQuery())); + } + } + + @Test + void testWhoCanAccessForInvalidInputs() { + ServerCommonException thrown = + assertThrows( + ServerCommonException.class, + () -> authzService.whoCanAccess(null, null, null)); + assertNotNull(thrown); + assertEquals("The resource argument is invalid", thrown.getMessage()); + thrown = + assertThrows( + ServerCommonException.class, + () -> authzService.whoCanAccess("kuku", null, null)); + assertNotNull(thrown); + assertEquals("The relationDefinition argument is invalid", thrown.getMessage()); + thrown = + assertThrows( + ServerCommonException.class, + () -> authzService.whoCanAccess("kuku", "kiki", null)); + assertNotNull(thrown); + assertEquals("The namespace argument is invalid", thrown.getMessage()); + } + + @Test + void testWhoCanAccessForSuccess() { + var apiProxy = mock(ApiProxy.class); + doReturn(new WhoCanAccessResponse(List.of("kuku"))).when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + authzService.whoCanAccess("kiki", "kuku", "kaka"); + } + } + + @Test + void testResourceRelationsForNoResource() { + ServerCommonException thrown = + assertThrows( + ServerCommonException.class, + () -> authzService.resourceRelations(null)); + assertNotNull(thrown); + assertEquals("The resource argument is invalid", thrown.getMessage()); + } + + @Test + void testResourceRelationsForSuccess() { + var apiProxy = mock(ApiProxy.class); + doReturn(new RelationsResponse(List.of(new Relation()))).when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + authzService.resourceRelations("kiki"); + } + } + + @Test + void testUsersRelationsForNoUsers() { + ServerCommonException thrown = + assertThrows( + ServerCommonException.class, + () -> authzService.targetsRelations(null)); + assertNotNull(thrown); + assertEquals("The targets argument is invalid", thrown.getMessage()); + } + + @Test + void testUsersRelationsForSuccess() { + var apiProxy = mock(ApiProxy.class); + doReturn(new RelationsResponse(List.of(new Relation()))).when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + authzService.targetsRelations(List.of("kiki")); + } + } + + @Test + void testWhatCanUserAccessForNoUser() { + ServerCommonException thrown = + assertThrows( + ServerCommonException.class, + () -> authzService.whatCanTargetAccess(null)); + assertNotNull(thrown); + assertEquals("The user argument is invalid", thrown.getMessage()); + } + + @Test + void testWhatCanUserAccessForSuccess() { + var apiProxy = mock(ApiProxy.class); + doReturn(new RelationsResponse(List.of(new Relation()))).when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + authzService.whatCanTargetAccess("kiki"); + } + } + + @RetryingTest(value = 3, suspendForMs = 30000, onExceptions = RateLimitExceededException.class) + void testFunctionalFullCycle() throws Exception { + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + mapper.findAndRegisterModules(); + Schema s = mapper.readValue(new File("src/test/data/files.yaml"), Schema.class); + authzService.deleteSchema(); + authzService.saveSchema(s, true); + authzService.createRelations(List.of( + new Relation("Dev", "parent", "org", "Descope", null, null, null, null), + new Relation("Sales", "parent", "org", "Descope", null, null, null, null), + new Relation("Dev", "member", "org", "u1", null, null, null, null), + new Relation("Dev", "member", "org", "u3", null, null, null, null), + new Relation("Sales", "member", "org", "u2", null, null, null, null), + new Relation("Presentations", "parent", "folder", "Internal", null, null, null, null), + new Relation("roadmap.ppt", "parent", "doc", "Presentations", null, null, null, null), + new Relation("roadmap.ppt", "owner", "doc", "u1", null, null, null, null), + new Relation("Internal", "viewer", "folder", null, "Descope", "member", "org", null), + new Relation("Presentations", "editor", "folder", null, "Sales", "member", "org", null) + )); + var resp = authzService.hasRelations(List.of( + new RelationQuery("roadmap.ppt", "owner", "doc", "u1", false), + new RelationQuery("roadmap.ppt", "editor", "doc", "u1", false), + new RelationQuery("roadmap.ppt", "viewer", "doc", "u1", false), + new RelationQuery("roadmap.ppt", "viewer", "doc", "u3", false), + new RelationQuery("roadmap.ppt", "editor", "doc", "u3", false), + new RelationQuery("roadmap.ppt", "editor", "doc", "u2", false) + )); + assertTrue(resp.get(0).isHasRelation()); + assertTrue(resp.get(1).isHasRelation()); + assertTrue(resp.get(2).isHasRelation()); + assertTrue(resp.get(3).isHasRelation()); + assertFalse(resp.get(4).isHasRelation()); + assertTrue(resp.get(5).isHasRelation()); + var respWho = authzService.whoCanAccess("roadmap.ppt", "editor", "doc"); + assertThat(respWho).hasSameElementsAs(List.of("u1", "u2")); + var respResourceRelations = authzService.resourceRelations("roadmap.ppt"); + assertThat(respResourceRelations).size().isEqualTo(2); + var respUsersRelations = authzService.targetsRelations(List.of("u1")); + assertThat(respUsersRelations).size().isEqualTo(2); + var respWhat = authzService.whatCanTargetAccess("u1"); + assertThat(respWhat).size().isEqualTo(7); + authzService.deleteSchema(); + } +} diff --git a/src/test/java/com/descope/sdk/mgmt/impl/UserServiceImplTest.java b/src/test/java/com/descope/sdk/mgmt/impl/UserServiceImplTest.java index 49a4fdf9..77ff9d4a 100644 --- a/src/test/java/com/descope/sdk/mgmt/impl/UserServiceImplTest.java +++ b/src/test/java/com/descope/sdk/mgmt/impl/UserServiceImplTest.java @@ -837,7 +837,7 @@ void testFunctionalUserWithTenantAndRole() { .build()); UserResponse user = createResponse.getUser(); assertNotNull(user); - Assertions.assertThat(user.getLoginIds()).contains(loginId); + assertThat(user.getLoginIds()).contains(loginId); assertEquals(email, user.getEmail()); assertEquals("+15555555555", user.getPhone()); assertEquals(true, user.getVerifiedEmail()); @@ -846,6 +846,20 @@ void testFunctionalUserWithTenantAndRole() { assertEquals("invited", user.getStatus()); assertThat(user.getUserTenants()).containsExactly( AssociatedTenant.builder().tenantId(tenantId).tenantName(tenantName).roleNames(List.of(roleName)).build()); + var updateResponse = userService.update(loginId, + UserRequest.builder() + .loginId(loginId) + .roleNames(List.of(roleName)) + .email(email) + .verifiedEmail(true) + .phone(phone) + .verifiedPhone(true) + .displayName("Testing Test") + .invite(false) + .build()); + user = updateResponse.getUser(); + assertNotNull(user); + assertThat(user.getRoleNames()).containsExactly(roleName); // Delete userService.delete(loginId); tenantService.delete(tenantId);