diff --git a/iam/iam-client/pom.xml b/iam/iam-client/pom.xml new file mode 100644 index 00000000..e371cf45 --- /dev/null +++ b/iam/iam-client/pom.xml @@ -0,0 +1,97 @@ + + + 4.0.0 + + iam-client + jar + MultiCloudJ - IAM Client + + + com.salesforce.multicloudj + iam + ${revision} + ../pom.xml + + + + + + com.salesforce.multicloudj + multicloudj-common + + + com.salesforce.multicloudj + sts-client + + + + + org.junit.jupiter + junit-jupiter-api + 5.12.1 + test + + + org.mockito + mockito-core + 5.16.1 + test + + + org.mockito + mockito-junit-jupiter + 5.16.1 + test + + + com.salesforce.multicloudj + multicloudj-common + test-jar + test + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + + test-jar + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.4.0 + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.4.0 + + + run-integration-tests + integration-test + + integration-test + + + + verify-integration-results + verify + + verify + + + + + + + \ No newline at end of file diff --git a/iam/iam-client/src/main/java/com/salesforce/multicloudj/iam/client/IamClient.java b/iam/iam-client/src/main/java/com/salesforce/multicloudj/iam/client/IamClient.java new file mode 100644 index 00000000..8189e09a --- /dev/null +++ b/iam/iam-client/src/main/java/com/salesforce/multicloudj/iam/client/IamClient.java @@ -0,0 +1,212 @@ +package com.salesforce.multicloudj.iam.client; + +import com.salesforce.multicloudj.iam.model.CreateOptions; +import com.salesforce.multicloudj.iam.model.PolicyDocument; +import com.salesforce.multicloudj.iam.model.TrustConfiguration; +import com.salesforce.multicloudj.sts.model.CredentialsOverrider; + +import java.net.URI; +import java.util.List; +import java.util.Optional; + +/** + * Entry point for client code to interact with Identity and Access Management (IAM) services + * in a substrate-agnostic way. + * + *

This client provides unified IAM operations across multiple cloud providers including + * AWS IAM, GCP IAM, and AliCloud RAM. It handles the complexity of different cloud IAM models + * and provides a consistent API for identity lifecycle management and policy operations. + * + *

Usage example: + *

+ * IamClient client = IamClient.builder("aws")
+ *     .withRegion("us-west-2")
+ *     .build();
+ *
+ * // Create identity
+ * String identityId = client.createIdentity("MyRole", "Example role", "123456789012", "us-west-2",
+ *     Optional.empty(), Optional.empty());
+ *
+ * // Create policy
+ * PolicyDocument policy = PolicyDocument.builder()
+ *     .version("2024-01-01")
+ *     .statement("StorageAccess")
+ *         .effect("Allow")
+ *         .addAction("storage:GetObject")
+ *         .addResource("storage://my-bucket/*")
+ *     .endStatement()
+ *     .build();
+ *
+ * // Attach policy
+ * client.attachInlinePolicy(policy, "123456789012", "us-west-2", "my-bucket");
+ * 
+ * + * @since 0.3.0 + */ +public class IamClient { + + /** + * Protected constructor for IamClient. + * Use the builder pattern to create instances. + */ + protected IamClient() { + // Implementation will be added later when AbstractIamService is available + } + + /** + * Creates a new IamClientBuilder for the specified provider. + * + * @param providerId the ID of the provider such as "aws", "gcp", or "ali" + * @return a new IamClientBuilder instance + */ + public static IamClientBuilder builder(String providerId) { + return new IamClientBuilder(providerId); + } + + /** + * Creates a new identity (role/service account) in the cloud provider. + * + * @param identityName the name of the identity to create + * @param description optional description for the identity (can be null) + * @param tenantId the tenant ID (AWS Account ID, GCP Project ID, or AliCloud Account ID) + * @param region the region for IAM operations + * @param trustConfig optional trust configuration + * @param options optional creation options + * @return the unique identifier of the created identity + */ + public String createIdentity(String identityName, String description, String tenantId, String region, + Optional trustConfig, Optional options) { + // Implementation will be added when driver layer is available + throw new UnsupportedOperationException("Implementation will be added when driver layer is available"); + } + + /** + * Attaches an inline policy to a resource. + * + * @param policyDocument the policy document in substrate-neutral format + * @param tenantId the tenant ID + * @param region the region + * @param resource the resource to attach the policy to + */ + public void attachInlinePolicy(PolicyDocument policyDocument, String tenantId, String region, String resource) { + // Implementation will be added when driver layer is available + throw new UnsupportedOperationException("Implementation will be added when driver layer is available"); + } + + /** + * Retrieves the details of a specific inline policy attached to an identity. + * + * @param identityName the name of the identity + * @param policyName the name of the policy + * @param tenantId the tenant ID + * @param region the region + * @return the policy document details as a string + */ + public String getInlinePolicyDetails(String identityName, String policyName, String tenantId, String region) { + // Implementation will be added when driver layer is available + throw new UnsupportedOperationException("Implementation will be added when driver layer is available"); + } + + /** + * Lists all inline policies attached to an identity. + * + * @param identityName the name of the identity + * @param tenantId the tenant ID + * @param region the region + * @return a list of policy names + */ + public List getAttachedPolicies(String identityName, String tenantId, String region) { + // Implementation will be added when driver layer is available + throw new UnsupportedOperationException("Implementation will be added when driver layer is available"); + } + + /** + * Removes an inline policy from an identity. + * + * @param identityName the name of the identity + * @param policyName the name of the policy to remove + * @param tenantId the tenant ID + * @param region the region + */ + public void removePolicy(String identityName, String policyName, String tenantId, String region) { + // Implementation will be added when driver layer is available + throw new UnsupportedOperationException("Implementation will be added when driver layer is available"); + } + + /** + * Deletes an identity from the cloud provider. + * + * @param identityName the name of the identity to delete + * @param tenantId the tenant ID + * @param region the region + */ + public void deleteIdentity(String identityName, String tenantId, String region) { + // Implementation will be added when driver layer is available + throw new UnsupportedOperationException("Implementation will be added when driver layer is available"); + } + + /** + * Retrieves metadata about an identity. + * + * @param identityName the name of the identity + * @param tenantId the tenant ID + * @param region the region + * @return the unique identity identifier (ARN, email, or roleId) + */ + public String getIdentity(String identityName, String tenantId, String region) { + // Implementation will be added when driver layer is available + throw new UnsupportedOperationException("Implementation will be added when driver layer is available"); + } + + /** + * Builder class for IamClient. + */ + public static class IamClientBuilder { + protected String region; + protected URI endpoint; + + /** + * Constructor for IamClientBuilder. + * + * @param providerId the ID of the provider such as "aws", "gcp", or "ali" + */ + public IamClientBuilder(String providerId) { + // Implementation will be added when ServiceLoader and AbstractIamService are available + // Will find and initialize the provider builder here + } + + /** + * Sets the region for the IAM client. + * + * @param region the region to set + * @return this IamClientBuilder instance + */ + public IamClientBuilder withRegion(String region) { + this.region = region; + // Implementation will be added later to delegate to underlying provider builder + return this; + } + + /** + * Sets the endpoint to override for the IAM client. + * + * @param endpoint the endpoint to set + * @return this IamClientBuilder instance + */ + public IamClientBuilder withEndpoint(URI endpoint) { + this.endpoint = endpoint; + // Implementation will be added later to delegate to underlying provider builder + return this; + } + + /** + * Builds and returns an IamClient instance. + * + * @return a new IamClient instance + */ + public IamClient build() { + // Implementation will be added when ServiceLoader and AbstractIamService are available + return new IamClient(); + } + } +} \ No newline at end of file diff --git a/iam/iam-client/src/main/java/com/salesforce/multicloudj/iam/model/CreateOptions.java b/iam/iam-client/src/main/java/com/salesforce/multicloudj/iam/model/CreateOptions.java new file mode 100644 index 00000000..3da2946d --- /dev/null +++ b/iam/iam-client/src/main/java/com/salesforce/multicloudj/iam/model/CreateOptions.java @@ -0,0 +1,146 @@ +package com.salesforce.multicloudj.iam.model; + +import java.util.Objects; + +/** + * Optional configuration for identity creation operations. + * + *

This class provides additional options that can be set during identity creation, + * such as path specifications, session duration limits, and permission boundaries. + * + *

Usage example: + *

+ * CreateOptions options = CreateOptions.builder()
+ *     .path("/orgstore/")
+ *     .maxSessionDuration(43200) // 12 hours
+ *     .permissionBoundary("arn:aws:iam::123456789012:policy/PowerUserBoundary")
+ *     .build();
+ * 
+ * + * @since 0.3.0 + */ +public class CreateOptions { + private final String path; + private final Integer maxSessionDuration; + private final String permissionBoundary; + + private CreateOptions(Builder builder) { + this.path = builder.path; + this.maxSessionDuration = builder.maxSessionDuration; + this.permissionBoundary = builder.permissionBoundary; + } + + /** + * Creates a new builder for CreateOptions. + * + * @return a new Builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Gets the path for the identity. + * + * @return the path, or null if not set + */ + public String getPath() { + return path; + } + + /** + * Gets the maximum session duration in seconds. + * + * @return the maximum session duration, or null if not set + */ + public Integer getMaxSessionDuration() { + return maxSessionDuration; + } + + /** + * Gets the permission boundary ARN. + * + * @return the permission boundary ARN, or null if not set + */ + public String getPermissionBoundary() { + return permissionBoundary; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CreateOptions that = (CreateOptions) o; + return Objects.equals(path, that.path) && + Objects.equals(maxSessionDuration, that.maxSessionDuration) && + Objects.equals(permissionBoundary, that.permissionBoundary); + } + + @Override + public int hashCode() { + return Objects.hash(path, maxSessionDuration, permissionBoundary); + } + + @Override + public String toString() { + return "CreateOptions{" + + "path='" + path + '\'' + + ", maxSessionDuration=" + maxSessionDuration + + ", permissionBoundary='" + permissionBoundary + '\'' + + '}'; + } + + /** + * Builder class for CreateOptions. + */ + public static class Builder { + private String path; + private Integer maxSessionDuration; + private String permissionBoundary; + + private Builder() { + } + + /** + * Sets the path for the identity. + * + * @param path the path (e.g., "/orgstore/") for organizing identities + * @return this Builder instance + */ + public Builder path(String path) { + this.path = path; + return this; + } + + /** + * Sets the maximum session duration in seconds. + * + * @param maxSessionDuration the maximum session duration (typically up to 12 hours = 43200 seconds) + * @return this Builder instance + */ + public Builder maxSessionDuration(Integer maxSessionDuration) { + this.maxSessionDuration = maxSessionDuration; + return this; + } + + /** + * Sets the permission boundary ARN. + * + * @param permissionBoundary the ARN of the policy that acts as a permissions boundary + * @return this Builder instance + */ + public Builder permissionBoundary(String permissionBoundary) { + this.permissionBoundary = permissionBoundary; + return this; + } + + /** + * Builds and returns a CreateOptions instance. + * + * @return a new CreateOptions instance + */ + public CreateOptions build() { + return new CreateOptions(this); + } + } +} \ No newline at end of file diff --git a/iam/iam-client/src/main/java/com/salesforce/multicloudj/iam/model/PolicyDocument.java b/iam/iam-client/src/main/java/com/salesforce/multicloudj/iam/model/PolicyDocument.java new file mode 100644 index 00000000..c6acae2e --- /dev/null +++ b/iam/iam-client/src/main/java/com/salesforce/multicloudj/iam/model/PolicyDocument.java @@ -0,0 +1,272 @@ +package com.salesforce.multicloudj.iam.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Represents a substrate-neutral policy document containing multiple statements. + * + *

This class provides a cloud-agnostic way to define IAM policies that can be + * translated to AWS, GCP, or AliCloud native formats. The policy uses a builder + * pattern to prevent JSON parsing errors and provides type safety. + * + *

Usage example: + *

+ * PolicyDocument policy = PolicyDocument.builder()
+ *     .version("2024-01-01")
+ *     .statement("StorageAccess")
+ *         .effect("Allow")
+ *         .addAction("storage:GetObject")
+ *         .addAction("storage:PutObject")
+ *         .addPrincipal("arn:aws:iam::123456789012:user/ExampleUser")
+ *         .addResource("storage://my-bucket/*")
+ *         .addCondition("StringEquals", "aws:RequestedRegion", "us-west-2")
+ *     .endStatement()
+ *     .build();
+ * 
+ * + * @since 0.3.0 + */ +public class PolicyDocument { + private final String version; + private final List statements; + + private PolicyDocument(Builder builder) { + this.version = builder.version; + this.statements = new ArrayList<>(builder.statements); + } + + /** + * Creates a new builder for PolicyDocument. + * + * @return a new Builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Gets the policy version. + * + * @return the policy version + */ + public String getVersion() { + return version; + } + + /** + * Gets the list of statements. + * + * @return an immutable copy of the statements list + */ + public List getStatements() { + return new ArrayList<>(statements); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PolicyDocument that = (PolicyDocument) o; + return Objects.equals(version, that.version) && + Objects.equals(statements, that.statements); + } + + @Override + public int hashCode() { + return Objects.hash(version, statements); + } + + @Override + public String toString() { + return "PolicyDocument{" + + "version='" + version + '\'' + + ", statements=" + statements + + '}'; + } + + /** + * Builder class for PolicyDocument. + */ + public static class Builder { + private String version = "2024-01-01"; + private final List statements = new ArrayList<>(); + private Statement.Builder currentStatementBuilder; + + private Builder() { + } + + /** + * Sets the policy version. + * + * @param version the policy version (default: "2024-01-01") + * @return this Builder instance + */ + public Builder version(String version) { + this.version = version; + return this; + } + + /** + * Starts building a new statement with the given SID. + * + * @param sid the statement ID + * @return this Builder instance configured for statement building + */ + public Builder statement(String sid) { + finalizeCurrentStatement(); + this.currentStatementBuilder = Statement.builder().sid(sid); + return this; + } + + /** + * Sets the effect for the current statement. + * + * @param effect "Allow" or "Deny" + * @return this Builder instance + */ + public Builder effect(String effect) { + validateCurrentStatement(); + this.currentStatementBuilder.effect(effect); + return this; + } + + /** + * Adds a principal to the current statement. + * + * @param principal the principal (fully qualified principal required) + * @return this Builder instance + */ + public Builder addPrincipal(String principal) { + validateCurrentStatement(); + this.currentStatementBuilder.addPrincipal(principal); + return this; + } + + /** + * Adds multiple principals to the current statement. + * + * @param principals the list of principals + * @return this Builder instance + */ + public Builder addPrincipals(List principals) { + validateCurrentStatement(); + this.currentStatementBuilder.addPrincipals(principals); + return this; + } + + /** + * Adds an action to the current statement. + * + * @param action the action in substrate-neutral format + * @return this Builder instance + */ + public Builder addAction(String action) { + validateCurrentStatement(); + this.currentStatementBuilder.addAction(action); + return this; + } + + /** + * Adds multiple actions to the current statement. + * + * @param actions the list of actions + * @return this Builder instance + */ + public Builder addActions(List actions) { + validateCurrentStatement(); + this.currentStatementBuilder.addActions(actions); + return this; + } + + /** + * Adds a resource to the current statement. + * + * @param resource the resource in URI format + * @return this Builder instance + */ + public Builder addResource(String resource) { + validateCurrentStatement(); + this.currentStatementBuilder.addResource(resource); + return this; + } + + /** + * Adds multiple resources to the current statement. + * + * @param resources the list of resources + * @return this Builder instance + */ + public Builder addResources(List resources) { + validateCurrentStatement(); + this.currentStatementBuilder.addResources(resources); + return this; + } + + /** + * Adds a condition to the current statement. + * + * @param operator the condition operator + * @param key the condition key + * @param value the condition value + * @return this Builder instance + */ + public Builder addCondition(String operator, String key, Object value) { + validateCurrentStatement(); + this.currentStatementBuilder.addCondition(operator, key, value); + return this; + } + + /** + * Ends the current statement and adds it to the policy. + * + * @return this Builder instance + */ + public Builder endStatement() { + finalizeCurrentStatement(); + return this; + } + + /** + * Adds a complete statement to the policy document. + * + * @param statement the statement to add + * @return this Builder instance + */ + public Builder addStatement(Statement statement) { + finalizeCurrentStatement(); + if (statement != null) { + this.statements.add(statement); + } + return this; + } + + /** + * Builds and returns a PolicyDocument instance. + * + * @return a new PolicyDocument instance + * @throws IllegalArgumentException if no statements are defined + */ + public PolicyDocument build() { + finalizeCurrentStatement(); + if (statements.isEmpty()) { + throw new IllegalArgumentException("at least one statement is required"); + } + return new PolicyDocument(this); + } + + private void validateCurrentStatement() { + if (currentStatementBuilder == null) { + throw new IllegalStateException("No statement is currently being built. Call statement(sid) first."); + } + } + + private void finalizeCurrentStatement() { + if (currentStatementBuilder != null) { + statements.add(currentStatementBuilder.build()); + currentStatementBuilder = null; + } + } + } +} \ No newline at end of file diff --git a/iam/iam-client/src/main/java/com/salesforce/multicloudj/iam/model/Statement.java b/iam/iam-client/src/main/java/com/salesforce/multicloudj/iam/model/Statement.java new file mode 100644 index 00000000..d14ea250 --- /dev/null +++ b/iam/iam-client/src/main/java/com/salesforce/multicloudj/iam/model/Statement.java @@ -0,0 +1,291 @@ +package com.salesforce.multicloudj.iam.model; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Represents a single statement within a policy document. + * + *

A statement defines the permissions, principals, resources, and conditions + * for a specific set of actions in a substrate-neutral format. + * + *

Usage example: + *

+ * Statement statement = Statement.builder()
+ *     .sid("StorageAccess")
+ *     .effect("Allow")
+ *     .addAction("storage:GetObject")
+ *     .addAction("storage:PutObject")
+ *     .addPrincipal("arn:aws:iam::123456789012:user/ExampleUser")
+ *     .addResource("storage://my-bucket/*")
+ *     .addCondition("StringEquals", "aws:RequestedRegion", "us-west-2")
+ *     .build();
+ * 
+ * + * @since 0.3.0 + */ +public class Statement { + private final String sid; + private final String effect; + private final List principals; + private final List actions; + private final List resources; + private final Map> conditions; + + private Statement(Builder builder) { + this.sid = builder.sid; + this.effect = builder.effect; + this.principals = new ArrayList<>(builder.principals); + this.actions = new ArrayList<>(builder.actions); + this.resources = new ArrayList<>(builder.resources); + this.conditions = new HashMap<>(builder.conditions); + } + + /** + * Creates a new builder for Statement. + * + * @return a new Builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Gets the statement ID. + * + * @return the statement ID, or null if not set + */ + public String getSid() { + return sid; + } + + /** + * Gets the effect (Allow or Deny). + * + * @return the effect + */ + public String getEffect() { + return effect; + } + + /** + * Gets the list of principals. + * + * @return an immutable copy of the principals list + */ + public List getPrincipals() { + return new ArrayList<>(principals); + } + + /** + * Gets the list of actions. + * + * @return an immutable copy of the actions list + */ + public List getActions() { + return new ArrayList<>(actions); + } + + /** + * Gets the list of resources. + * + * @return an immutable copy of the resources list + */ + public List getResources() { + return new ArrayList<>(resources); + } + + /** + * Gets the conditions map. + * + * @return an immutable copy of the conditions map + */ + public Map> getConditions() { + return new HashMap<>(conditions); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Statement statement = (Statement) o; + return Objects.equals(sid, statement.sid) && + Objects.equals(effect, statement.effect) && + Objects.equals(principals, statement.principals) && + Objects.equals(actions, statement.actions) && + Objects.equals(resources, statement.resources) && + Objects.equals(conditions, statement.conditions); + } + + @Override + public int hashCode() { + return Objects.hash(sid, effect, principals, actions, resources, conditions); + } + + @Override + public String toString() { + return "Statement{" + + "sid='" + sid + '\'' + + ", effect='" + effect + '\'' + + ", principals=" + principals + + ", actions=" + actions + + ", resources=" + resources + + ", conditions=" + conditions + + '}'; + } + + /** + * Builder class for Statement. + */ + public static class Builder { + private String sid; + private String effect; + private final List principals = new ArrayList<>(); + private final List actions = new ArrayList<>(); + private final List resources = new ArrayList<>(); + private final Map> conditions = new HashMap<>(); + + private Builder() { + } + + /** + * Sets the statement ID. + * + * @param sid the unique identifier for this statement within the policy + * @return this Builder instance + */ + public Builder sid(String sid) { + this.sid = sid; + return this; + } + + /** + * Sets the effect. + * + * @param effect "Allow" or "Deny" + * @return this Builder instance + */ + public Builder effect(String effect) { + this.effect = effect; + return this; + } + + /** + * Adds a principal to the statement. + * + * @param principal the principal (fully qualified principal required) + * @return this Builder instance + */ + public Builder addPrincipal(String principal) { + if (principal != null && !principal.trim().isEmpty()) { + this.principals.add(principal); + } + return this; + } + + /** + * Adds multiple principals to the statement. + * + * @param principals the list of principals + * @return this Builder instance + */ + public Builder addPrincipals(List principals) { + if (principals != null) { + principals.stream() + .filter(p -> p != null && !p.trim().isEmpty()) + .forEach(this.principals::add); + } + return this; + } + + /** + * Adds an action to the statement. + * + * @param action the action in substrate-neutral format (e.g., "storage:GetObject") + * @return this Builder instance + */ + public Builder addAction(String action) { + if (action != null && !action.trim().isEmpty()) { + this.actions.add(action); + } + return this; + } + + /** + * Adds multiple actions to the statement. + * + * @param actions the list of actions + * @return this Builder instance + */ + public Builder addActions(List actions) { + if (actions != null) { + actions.stream() + .filter(a -> a != null && !a.trim().isEmpty()) + .forEach(this.actions::add); + } + return this; + } + + /** + * Adds a resource to the statement. + * + * @param resource the resource in URI format (e.g., "storage://my-bucket/*") + * @return this Builder instance + */ + public Builder addResource(String resource) { + if (resource != null && !resource.trim().isEmpty()) { + this.resources.add(resource); + } + return this; + } + + /** + * Adds multiple resources to the statement. + * + * @param resources the list of resources + * @return this Builder instance + */ + public Builder addResources(List resources) { + if (resources != null) { + resources.stream() + .filter(r -> r != null && !r.trim().isEmpty()) + .forEach(this.resources::add); + } + return this; + } + + /** + * Adds a condition to the statement. + * + * @param operator the condition operator (e.g., "StringEquals", "IpAddress") + * @param key the condition key (e.g., "aws:RequestedRegion") + * @param value the condition value + * @return this Builder instance + */ + public Builder addCondition(String operator, String key, Object value) { + if (operator != null && key != null && value != null) { + conditions.computeIfAbsent(operator, k -> new HashMap<>()).put(key, value); + } + return this; + } + + /** + * Builds and returns a Statement instance. + * + * @return a new Statement instance + * @throws IllegalArgumentException if required fields are missing + */ + public Statement build() { + if (effect == null || effect.trim().isEmpty()) { + throw new IllegalArgumentException("effect is required"); + } + if (actions.isEmpty()) { + throw new IllegalArgumentException("at least one action is required"); + } + return new Statement(this); + } + } +} \ No newline at end of file diff --git a/iam/iam-client/src/main/java/com/salesforce/multicloudj/iam/model/TrustConfiguration.java b/iam/iam-client/src/main/java/com/salesforce/multicloudj/iam/model/TrustConfiguration.java new file mode 100644 index 00000000..20f9f114 --- /dev/null +++ b/iam/iam-client/src/main/java/com/salesforce/multicloudj/iam/model/TrustConfiguration.java @@ -0,0 +1,146 @@ +package com.salesforce.multicloudj.iam.model; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Configuration for trust relationships in identity creation. + * + *

This class defines which principals can assume or impersonate the identity being created, + * along with any conditions that must be met for the trust relationship to be valid. + * + *

Usage example: + *

+ * TrustConfiguration trust = TrustConfiguration.builder()
+ *     .addTrustedPrincipal("arn:aws:iam::111122223333:root")
+ *     .addTrustedPrincipal("arn:aws:iam::444455556666:user/ExampleUser")
+ *     .addCondition("StringEquals", "aws:RequestedRegion", "us-west-2")
+ *     .build();
+ * 
+ * + * @since 0.3.0 + */ +public class TrustConfiguration { + private final List trustedPrincipals; + private final Map> conditions; + + private TrustConfiguration(Builder builder) { + this.trustedPrincipals = new ArrayList<>(builder.trustedPrincipals); + this.conditions = new HashMap<>(builder.conditions); + } + + /** + * Creates a new builder for TrustConfiguration. + * + * @return a new Builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Gets the list of trusted principals. + * + * @return an immutable copy of the trusted principals list + */ + public List getTrustedPrincipals() { + return new ArrayList<>(trustedPrincipals); + } + + /** + * Gets the trust conditions. + * + * @return an immutable copy of the conditions map + */ + public Map> getConditions() { + return new HashMap<>(conditions); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TrustConfiguration that = (TrustConfiguration) o; + return Objects.equals(trustedPrincipals, that.trustedPrincipals) && + Objects.equals(conditions, that.conditions); + } + + @Override + public int hashCode() { + return Objects.hash(trustedPrincipals, conditions); + } + + @Override + public String toString() { + return "TrustConfiguration{" + + "trustedPrincipals=" + trustedPrincipals + + ", conditions=" + conditions + + '}'; + } + + /** + * Builder class for TrustConfiguration. + */ + public static class Builder { + private final List trustedPrincipals = new ArrayList<>(); + private final Map> conditions = new HashMap<>(); + + private Builder() { + } + + /** + * Adds a trusted principal to the trust configuration. + * + * @param principal the principal ARN or identifier that can assume this identity + * @return this Builder instance + */ + public Builder addTrustedPrincipal(String principal) { + if (principal != null && !principal.trim().isEmpty()) { + this.trustedPrincipals.add(principal); + } + return this; + } + + /** + * Adds multiple trusted principals to the trust configuration. + * + * @param principals the list of principal ARNs or identifiers + * @return this Builder instance + */ + public Builder addTrustedPrincipals(List principals) { + if (principals != null) { + principals.stream() + .filter(p -> p != null && !p.trim().isEmpty()) + .forEach(this.trustedPrincipals::add); + } + return this; + } + + /** + * Adds a condition to the trust configuration. + * + * @param operator the condition operator (e.g., "StringEquals", "IpAddress") + * @param key the condition key (e.g., "aws:RequestedRegion", "aws:SourceIp") + * @param value the condition value + * @return this Builder instance + */ + public Builder addCondition(String operator, String key, Object value) { + if (operator != null && key != null && value != null) { + conditions.computeIfAbsent(operator, k -> new HashMap<>()).put(key, value); + } + return this; + } + + /** + * Builds and returns a TrustConfiguration instance. + * + * @return a new TrustConfiguration instance + */ + public TrustConfiguration build() { + return new TrustConfiguration(this); + } + } +} \ No newline at end of file diff --git a/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/client/IamClientTest.java b/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/client/IamClientTest.java new file mode 100644 index 00000000..d802fea8 --- /dev/null +++ b/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/client/IamClientTest.java @@ -0,0 +1,148 @@ +package com.salesforce.multicloudj.iam.client; + +import org.junit.jupiter.api.Test; + +import java.net.URI; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for IamClient builder pattern and basic functionality. + */ +public class IamClientTest { + + @Test + public void testIamClientBuilder() { + // Test builder creation with different provider IDs + IamClient.IamClientBuilder builder = IamClient.builder("aws"); + assertNotNull(builder); + + // Test method chaining + IamClient.IamClientBuilder result = builder + .withRegion("us-west-2") + .withEndpoint(URI.create("https://iam.amazonaws.com")); + + assertSame(builder, result, "Builder methods should return the same instance for chaining"); + } + + @Test + public void testIamClientBuilderWithDifferentProviders() { + // Test with AWS + IamClient.IamClientBuilder awsBuilder = IamClient.builder("aws"); + assertNotNull(awsBuilder); + + // Test with GCP + IamClient.IamClientBuilder gcpBuilder = IamClient.builder("gcp"); + assertNotNull(gcpBuilder); + + // Test with AliCloud + IamClient.IamClientBuilder aliBuilder = IamClient.builder("ali"); + assertNotNull(aliBuilder); + } + + @Test + public void testIamClientBuild() { + IamClient client = IamClient.builder("aws") + .withRegion("us-east-1") + .build(); + + assertNotNull(client); + } + + @Test + public void testIamClientBuildWithEndpoint() { + URI customEndpoint = URI.create("https://custom-iam-endpoint.com"); + + IamClient client = IamClient.builder("gcp") + .withRegion("us-central1") + .withEndpoint(customEndpoint) + .build(); + + assertNotNull(client); + } + + // TODO: Modify this test to verify actual createIdentity functionality + // Expected behavior: Should return the unique identifier of the created identity + @Test + public void testCreateIdentity() { + IamClient client = IamClient.builder("aws").build(); + + // Currently throws UnsupportedOperationException, will be implemented later + assertThrows(UnsupportedOperationException.class, () -> { + client.createIdentity("TestRole", "Test description", "123456789012", "us-west-2", + java.util.Optional.empty(), java.util.Optional.empty()); + }); + } + + // TODO: Modify this test to verify actual attachInlinePolicy functionality + // Expected behavior: Should successfully attach policy without throwing exceptions + @Test + public void testAttachInlinePolicy() { + IamClient client = IamClient.builder("aws").build(); + + // Currently throws UnsupportedOperationException, will be implemented later + assertThrows(UnsupportedOperationException.class, () -> { + client.attachInlinePolicy(null, "123456789012", "us-west-2", "test-resource"); + }); + } + + // TODO: Modify this test to verify actual getInlinePolicyDetails functionality + // Expected behavior: Should return the policy document details as a string + @Test + public void testGetInlinePolicyDetails() { + IamClient client = IamClient.builder("aws").build(); + + // Currently throws UnsupportedOperationException, will be implemented later + assertThrows(UnsupportedOperationException.class, () -> { + client.getInlinePolicyDetails("TestRole", "TestPolicy", "123456789012", "us-west-2"); + }); + } + + // TODO: Modify this test to verify actual getAttachedPolicies functionality + // Expected behavior: Should return a list of policy names attached to the identity + @Test + public void testGetAttachedPolicies() { + IamClient client = IamClient.builder("aws").build(); + + // Currently throws UnsupportedOperationException, will be implemented later + assertThrows(UnsupportedOperationException.class, () -> { + client.getAttachedPolicies("TestRole", "123456789012", "us-west-2"); + }); + } + + // TODO: Modify this test to verify actual removePolicy functionality + // Expected behavior: Should successfully remove policy without throwing exceptions + @Test + public void testRemovePolicy() { + IamClient client = IamClient.builder("aws").build(); + + // Currently throws UnsupportedOperationException, will be implemented later + assertThrows(UnsupportedOperationException.class, () -> { + client.removePolicy("TestRole", "TestPolicy", "123456789012", "us-west-2"); + }); + } + + // TODO: Modify this test to verify actual deleteIdentity functionality + // Expected behavior: Should successfully delete identity without throwing exceptions + @Test + public void testDeleteIdentity() { + IamClient client = IamClient.builder("aws").build(); + + // Currently throws UnsupportedOperationException, will be implemented later + assertThrows(UnsupportedOperationException.class, () -> { + client.deleteIdentity("TestRole", "123456789012", "us-west-2"); + }); + } + + // TODO: Modify this test to verify actual getIdentity functionality + // Expected behavior: Should return the unique identity identifier (ARN, email, or roleId) + @Test + public void testGetIdentity() { + IamClient client = IamClient.builder("aws").build(); + + // Currently throws UnsupportedOperationException, will be implemented later + assertThrows(UnsupportedOperationException.class, () -> { + client.getIdentity("TestRole", "123456789012", "us-west-2"); + }); + } +} \ No newline at end of file diff --git a/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/model/CreateOptionsTest.java b/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/model/CreateOptionsTest.java new file mode 100644 index 00000000..51d83ff3 --- /dev/null +++ b/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/model/CreateOptionsTest.java @@ -0,0 +1,199 @@ +package com.salesforce.multicloudj.iam.model; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for CreateOptions builder pattern and functionality. + */ +public class CreateOptionsTest { + + @Test + public void testCreateOptionsBuilder() { + CreateOptions options = CreateOptions.builder() + .path("/service-roles/") + .maxSessionDuration(3600) + .permissionBoundary("arn:aws:iam::123456789012:policy/PowerUserBoundary") + .build(); + + assertEquals("/service-roles/", options.getPath()); + assertEquals(Integer.valueOf(3600), options.getMaxSessionDuration()); + assertEquals("arn:aws:iam::123456789012:policy/PowerUserBoundary", options.getPermissionBoundary()); + } + + @Test + public void testCreateOptionsBuilderMinimal() { + CreateOptions options = CreateOptions.builder() + .build(); + + assertNull(options.getPath()); + assertNull(options.getMaxSessionDuration()); + assertNull(options.getPermissionBoundary()); + } + + @Test + public void testCreateOptionsBuilderWithPath() { + CreateOptions options = CreateOptions.builder() + .path("/application/backend/") + .build(); + + assertEquals("/application/backend/", options.getPath()); + assertNull(options.getMaxSessionDuration()); + assertNull(options.getPermissionBoundary()); + } + + @Test + public void testCreateOptionsBuilderWithMaxSessionDuration() { + CreateOptions options = CreateOptions.builder() + .maxSessionDuration(7200) + .build(); + + assertNull(options.getPath()); + assertEquals(Integer.valueOf(7200), options.getMaxSessionDuration()); + assertNull(options.getPermissionBoundary()); + } + + @Test + public void testCreateOptionsBuilderWithPermissionBoundary() { + CreateOptions options = CreateOptions.builder() + .permissionBoundary("arn:aws:iam::123456789012:policy/DeveloperBoundary") + .build(); + + assertNull(options.getPath()); + assertNull(options.getMaxSessionDuration()); + assertEquals("arn:aws:iam::123456789012:policy/DeveloperBoundary", options.getPermissionBoundary()); + } + + @Test + public void testCreateOptionsBuilderWithCustomSessionDurations() { + // Test minimum duration (900 seconds = 15 minutes) + CreateOptions minOptions = CreateOptions.builder() + .maxSessionDuration(900) + .build(); + assertEquals(Integer.valueOf(900), minOptions.getMaxSessionDuration()); + + // Test maximum duration (43200 seconds = 12 hours) + CreateOptions maxOptions = CreateOptions.builder() + .maxSessionDuration(43200) + .build(); + assertEquals(Integer.valueOf(43200), maxOptions.getMaxSessionDuration()); + + // Test common duration (7200 seconds = 2 hours) + CreateOptions commonOptions = CreateOptions.builder() + .maxSessionDuration(7200) + .build(); + assertEquals(Integer.valueOf(7200), commonOptions.getMaxSessionDuration()); + } + + @Test + public void testCreateOptionsBuilderWithDifferentPaths() { + // Test root path + CreateOptions rootOptions = CreateOptions.builder() + .path("/") + .build(); + assertEquals("/", rootOptions.getPath()); + + // Test nested path + CreateOptions nestedOptions = CreateOptions.builder() + .path("/division/team/service/") + .build(); + assertEquals("/division/team/service/", nestedOptions.getPath()); + + // Test simple path + CreateOptions simpleOptions = CreateOptions.builder() + .path("/service-roles/") + .build(); + assertEquals("/service-roles/", simpleOptions.getPath()); + } + + @Test + public void testCreateOptionsBuilderWithDifferentPermissionBoundaries() { + // Test AWS IAM policy ARN + CreateOptions awsOptions = CreateOptions.builder() + .permissionBoundary("arn:aws:iam::123456789012:policy/PowerUserBoundary") + .build(); + assertEquals("arn:aws:iam::123456789012:policy/PowerUserBoundary", awsOptions.getPermissionBoundary()); + + // Test different policy name + CreateOptions devOptions = CreateOptions.builder() + .permissionBoundary("arn:aws:iam::987654321098:policy/DeveloperBoundary") + .build(); + assertEquals("arn:aws:iam::987654321098:policy/DeveloperBoundary", devOptions.getPermissionBoundary()); + } + + @Test + public void testCreateOptionsBuilderComplexScenario() { + CreateOptions options = CreateOptions.builder() + .path("/microservices/user-service/") + .maxSessionDuration(14400) // 4 hours + .permissionBoundary("arn:aws:iam::123456789012:policy/MicroserviceBoundary") + .build(); + + assertEquals("/microservices/user-service/", options.getPath()); + assertEquals(Integer.valueOf(14400), options.getMaxSessionDuration()); + assertEquals("arn:aws:iam::123456789012:policy/MicroserviceBoundary", options.getPermissionBoundary()); + } + + @Test + public void testCreateOptionsEquality() { + CreateOptions options1 = CreateOptions.builder() + .path("/test/") + .maxSessionDuration(3600) + .permissionBoundary("arn:aws:iam::123456789012:policy/TestBoundary") + .build(); + + CreateOptions options2 = CreateOptions.builder() + .path("/test/") + .maxSessionDuration(3600) + .permissionBoundary("arn:aws:iam::123456789012:policy/TestBoundary") + .build(); + + CreateOptions options3 = CreateOptions.builder() + .path("/different/") + .maxSessionDuration(3600) + .permissionBoundary("arn:aws:iam::123456789012:policy/TestBoundary") + .build(); + + assertEquals(options1, options2); + assertNotEquals(options1, options3); + assertEquals(options1.hashCode(), options2.hashCode()); + } + + @Test + public void testCreateOptionsToString() { + CreateOptions options = CreateOptions.builder() + .path("/test/") + .maxSessionDuration(7200) + .permissionBoundary("arn:aws:iam::123456789012:policy/TestBoundary") + .build(); + + String toString = options.toString(); + assertTrue(toString.contains("path='/test/'")); + assertTrue(toString.contains("maxSessionDuration=7200")); + assertTrue(toString.contains("permissionBoundary='arn:aws:iam::123456789012:policy/TestBoundary'")); + } + + @Test + public void testCreateOptionsBuilderMethodChaining() { + CreateOptions.Builder builder = CreateOptions.builder(); + + // Test that each method returns the same builder instance + assertSame(builder, builder.path("/test/")); + assertSame(builder, builder.maxSessionDuration(3600)); + assertSame(builder, builder.permissionBoundary("arn:aws:iam::123456789012:policy/TestBoundary")); + } + + @Test + public void testCreateOptionsBuilderNullValues() { + CreateOptions options = CreateOptions.builder() + .path(null) + .maxSessionDuration(null) + .permissionBoundary(null) + .build(); + + assertNull(options.getPath()); + assertNull(options.getMaxSessionDuration()); + assertNull(options.getPermissionBoundary()); + } +} \ No newline at end of file diff --git a/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/model/PolicyDocumentTest.java b/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/model/PolicyDocumentTest.java new file mode 100644 index 00000000..ce92a989 --- /dev/null +++ b/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/model/PolicyDocumentTest.java @@ -0,0 +1,107 @@ +package com.salesforce.multicloudj.iam.model; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for PolicyDocument builder pattern. + */ +public class PolicyDocumentTest { + + @Test + public void testPolicyDocumentBuilder() { + PolicyDocument policy = PolicyDocument.builder() + .version("2024-01-01") + .statement("StorageAccess") + .effect("Allow") + .addAction("storage:GetObject") + .addAction("storage:PutObject") + .addPrincipal("arn:aws:iam::123456789012:user/ExampleUser") + .addResource("storage://my-bucket/*") + .addCondition("StringEquals", "aws:RequestedRegion", "us-west-2") + .endStatement() + .build(); + + assertEquals("2024-01-01", policy.getVersion()); + assertEquals(1, policy.getStatements().size()); + + Statement statement = policy.getStatements().get(0); + assertEquals("StorageAccess", statement.getSid()); + assertEquals("Allow", statement.getEffect()); + assertEquals(Arrays.asList("storage:GetObject", "storage:PutObject"), statement.getActions()); + assertEquals(Arrays.asList("arn:aws:iam::123456789012:user/ExampleUser"), statement.getPrincipals()); + assertEquals(Arrays.asList("storage://my-bucket/*"), statement.getResources()); + + assertTrue(statement.getConditions().containsKey("StringEquals")); + assertEquals("us-west-2", statement.getConditions().get("StringEquals").get("aws:RequestedRegion")); + } + + @Test + public void testMultipleStatements() { + PolicyDocument policy = PolicyDocument.builder() + .statement("ReadAccess") + .effect("Allow") + .addAction("storage:GetObject") + .addResource("storage://my-bucket/*") + .endStatement() + .statement("WriteAccess") + .effect("Allow") + .addAction("storage:PutObject") + .addResource("storage://my-bucket/*") + .endStatement() + .build(); + + assertEquals(2, policy.getStatements().size()); + assertEquals("ReadAccess", policy.getStatements().get(0).getSid()); + assertEquals("WriteAccess", policy.getStatements().get(1).getSid()); + } + + @Test + public void testAddCompleteStatement() { + Statement statement = Statement.builder() + .sid("TestStatement") + .effect("Allow") + .addAction("storage:GetObject") + .addResource("storage://my-bucket/*") + .build(); + + PolicyDocument policy = PolicyDocument.builder() + .addStatement(statement) + .build(); + + assertEquals(1, policy.getStatements().size()); + assertEquals("TestStatement", policy.getStatements().get(0).getSid()); + } + + @Test + public void testEmptyPolicyThrowsException() { + assertThrows(IllegalArgumentException.class, () -> { + PolicyDocument.builder().build(); + }); + } + + @Test + public void testStatementWithoutEffectThrowsException() { + assertThrows(IllegalArgumentException.class, () -> { + PolicyDocument.builder() + .statement("TestStatement") + .addAction("storage:GetObject") + .endStatement() + .build(); + }); + } + + @Test + public void testStatementWithoutActionsThrowsException() { + assertThrows(IllegalArgumentException.class, () -> { + PolicyDocument.builder() + .statement("TestStatement") + .effect("Allow") + .endStatement() + .build(); + }); + } +} \ No newline at end of file diff --git a/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/model/StatementTest.java b/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/model/StatementTest.java new file mode 100644 index 00000000..ef1fef0d --- /dev/null +++ b/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/model/StatementTest.java @@ -0,0 +1,174 @@ +package com.salesforce.multicloudj.iam.model; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for Statement builder pattern and functionality. + */ +public class StatementTest { + + @Test + public void testStatementBuilder() { + Statement statement = Statement.builder() + .sid("TestStatement") + .effect("Allow") + .addAction("storage:GetObject") + .addAction("storage:PutObject") + .addResource("storage://my-bucket/*") + .addPrincipal("arn:aws:iam::123456789012:user/TestUser") + .addCondition("StringEquals", "aws:RequestedRegion", "us-west-2") + .build(); + + assertEquals("TestStatement", statement.getSid()); + assertEquals("Allow", statement.getEffect()); + assertEquals(Arrays.asList("storage:GetObject", "storage:PutObject"), statement.getActions()); + assertEquals(Arrays.asList("storage://my-bucket/*"), statement.getResources()); + assertEquals(Arrays.asList("arn:aws:iam::123456789012:user/TestUser"), statement.getPrincipals()); + + assertTrue(statement.getConditions().containsKey("StringEquals")); + assertEquals("us-west-2", statement.getConditions().get("StringEquals").get("aws:RequestedRegion")); + } + + @Test + public void testStatementBuilderMinimal() { + Statement statement = Statement.builder() + .sid("MinimalStatement") + .effect("Deny") + .addAction("storage:DeleteObject") + .addResource("storage://sensitive-bucket/*") + .build(); + + assertEquals("MinimalStatement", statement.getSid()); + assertEquals("Deny", statement.getEffect()); + assertEquals(Arrays.asList("storage:DeleteObject"), statement.getActions()); + assertEquals(Arrays.asList("storage://sensitive-bucket/*"), statement.getResources()); + assertTrue(statement.getPrincipals().isEmpty()); + assertTrue(statement.getConditions().isEmpty()); + } + + @Test + public void testStatementBuilderMultipleActions() { + List expectedActions = Arrays.asList("storage:GetObject", "storage:PutObject", "storage:ListObjects"); + + Statement statement = Statement.builder() + .sid("MultiActionStatement") + .effect("Allow") + .addAction("storage:GetObject") + .addAction("storage:PutObject") + .addAction("storage:ListObjects") + .addResource("storage://multi-action-bucket/*") + .build(); + + assertEquals(expectedActions, statement.getActions()); + } + + @Test + public void testStatementBuilderMultipleResources() { + List expectedResources = Arrays.asList("storage://bucket1/*", "storage://bucket2/*"); + + Statement statement = Statement.builder() + .sid("MultiResourceStatement") + .effect("Allow") + .addAction("storage:GetObject") + .addResource("storage://bucket1/*") + .addResource("storage://bucket2/*") + .build(); + + assertEquals(expectedResources, statement.getResources()); + } + + @Test + public void testStatementBuilderMultiplePrincipals() { + List expectedPrincipals = Arrays.asList( + "arn:aws:iam::123456789012:user/User1", + "arn:aws:iam::123456789012:user/User2" + ); + + Statement statement = Statement.builder() + .sid("MultiPrincipalStatement") + .effect("Allow") + .addAction("storage:GetObject") + .addResource("storage://shared-bucket/*") + .addPrincipal("arn:aws:iam::123456789012:user/User1") + .addPrincipal("arn:aws:iam::123456789012:user/User2") + .build(); + + assertEquals(expectedPrincipals, statement.getPrincipals()); + } + + @Test + public void testStatementBuilderMultipleConditions() { + Statement statement = Statement.builder() + .sid("MultiConditionStatement") + .effect("Allow") + .addAction("storage:GetObject") + .addResource("storage://conditional-bucket/*") + .addCondition("StringEquals", "aws:RequestedRegion", "us-west-2") + .addCondition("DateGreaterThan", "aws:CurrentTime", "2024-01-01T00:00:00Z") + .build(); + + assertTrue(statement.getConditions().containsKey("StringEquals")); + assertTrue(statement.getConditions().containsKey("DateGreaterThan")); + assertEquals("us-west-2", statement.getConditions().get("StringEquals").get("aws:RequestedRegion")); + assertEquals("2024-01-01T00:00:00Z", statement.getConditions().get("DateGreaterThan").get("aws:CurrentTime")); + } + + @Test + public void testStatementBuilderDenyEffect() { + Statement statement = Statement.builder() + .sid("DenyStatement") + .effect("Deny") + .addAction("*") + .addResource("storage://restricted-bucket/*") + .build(); + + assertEquals("Deny", statement.getEffect()); + assertEquals(Arrays.asList("*"), statement.getActions()); + } + + @Test + public void testStatementWithoutSid() { + Statement statement = Statement.builder() + .effect("Allow") + .addAction("storage:GetObject") + .addResource("storage://no-sid-bucket/*") + .build(); + + assertNull(statement.getSid()); + assertEquals("Allow", statement.getEffect()); + } + + @Test + public void testEmptyStatementThrowsException() { + assertThrows(IllegalArgumentException.class, () -> { + Statement.builder().build(); + }); + } + + @Test + public void testStatementWithoutEffectThrowsException() { + assertThrows(IllegalArgumentException.class, () -> { + Statement.builder() + .sid("NoEffectStatement") + .addAction("storage:GetObject") + .addResource("storage://test-bucket/*") + .build(); + }); + } + + @Test + public void testStatementWithoutActionsThrowsException() { + assertThrows(IllegalArgumentException.class, () -> { + Statement.builder() + .sid("NoActionsStatement") + .effect("Allow") + .addResource("storage://test-bucket/*") + .build(); + }); + } +} \ No newline at end of file diff --git a/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/model/TrustConfigurationTest.java b/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/model/TrustConfigurationTest.java new file mode 100644 index 00000000..2d37a529 --- /dev/null +++ b/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/model/TrustConfigurationTest.java @@ -0,0 +1,276 @@ +package com.salesforce.multicloudj.iam.model; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for TrustConfiguration builder pattern and functionality. + */ +public class TrustConfigurationTest { + + @Test + public void testTrustConfigurationBuilder() { + TrustConfiguration trustConfig = TrustConfiguration.builder() + .addTrustedPrincipal("arn:aws:iam::123456789012:root") + .addTrustedPrincipal("service-account@project.iam.gserviceaccount.com") + .addCondition("StringEquals", "aws:RequestedRegion", "us-west-2") + .addCondition("StringEquals", "aws:userid", "AIDACKCEVSQ6C2EXAMPLE") + .build(); + + List expectedPrincipals = Arrays.asList( + "arn:aws:iam::123456789012:root", + "service-account@project.iam.gserviceaccount.com" + ); + + assertEquals(expectedPrincipals, trustConfig.getTrustedPrincipals()); + + Map> conditions = trustConfig.getConditions(); + assertTrue(conditions.containsKey("StringEquals")); + assertEquals("us-west-2", conditions.get("StringEquals").get("aws:RequestedRegion")); + assertEquals("AIDACKCEVSQ6C2EXAMPLE", conditions.get("StringEquals").get("aws:userid")); + } + + @Test + public void testTrustConfigurationBuilderMinimal() { + TrustConfiguration trustConfig = TrustConfiguration.builder() + .addTrustedPrincipal("arn:aws:iam::987654321098:root") + .build(); + + assertEquals(Arrays.asList("arn:aws:iam::987654321098:root"), trustConfig.getTrustedPrincipals()); + assertTrue(trustConfig.getConditions().isEmpty()); + } + + @Test + public void testTrustConfigurationBuilderMultipleTrustedPrincipals() { + List expectedPrincipals = Arrays.asList( + "arn:aws:iam::111111111111:root", + "arn:aws:iam::222222222222:root", + "arn:aws:iam::333333333333:user/CrossAccountUser" + ); + + TrustConfiguration trustConfig = TrustConfiguration.builder() + .addTrustedPrincipal("arn:aws:iam::111111111111:root") + .addTrustedPrincipal("arn:aws:iam::222222222222:root") + .addTrustedPrincipal("arn:aws:iam::333333333333:user/CrossAccountUser") + .build(); + + assertEquals(expectedPrincipals, trustConfig.getTrustedPrincipals()); + } + + @Test + public void testTrustConfigurationBuilderAddTrustedPrincipals() { + List principalsToAdd = Arrays.asList( + "arn:aws:iam::111111111111:root", + "arn:aws:iam::222222222222:root" + ); + + TrustConfiguration trustConfig = TrustConfiguration.builder() + .addTrustedPrincipal("arn:aws:iam::123456789012:root") + .addTrustedPrincipals(principalsToAdd) + .build(); + + List expectedPrincipals = Arrays.asList( + "arn:aws:iam::123456789012:root", + "arn:aws:iam::111111111111:root", + "arn:aws:iam::222222222222:root" + ); + + assertEquals(expectedPrincipals, trustConfig.getTrustedPrincipals()); + } + + @Test + public void testTrustConfigurationBuilderMultipleConditions() { + TrustConfiguration trustConfig = TrustConfiguration.builder() + .addTrustedPrincipal("arn:aws:iam::123456789012:root") + .addCondition("StringEquals", "aws:RequestedRegion", "us-west-2") + .addCondition("DateGreaterThan", "aws:CurrentTime", "2024-01-01T00:00:00Z") + .addCondition("IpAddress", "aws:SourceIp", "203.0.113.0/24") + .build(); + + Map> conditions = trustConfig.getConditions(); + + assertTrue(conditions.containsKey("StringEquals")); + assertTrue(conditions.containsKey("DateGreaterThan")); + assertTrue(conditions.containsKey("IpAddress")); + + assertEquals("us-west-2", conditions.get("StringEquals").get("aws:RequestedRegion")); + assertEquals("2024-01-01T00:00:00Z", conditions.get("DateGreaterThan").get("aws:CurrentTime")); + assertEquals("203.0.113.0/24", conditions.get("IpAddress").get("aws:SourceIp")); + } + + @Test + public void testTrustConfigurationBuilderSameOperatorMultipleConditions() { + TrustConfiguration trustConfig = TrustConfiguration.builder() + .addTrustedPrincipal("arn:aws:iam::123456789012:root") + .addCondition("StringEquals", "aws:RequestedRegion", "us-west-2") + .addCondition("StringEquals", "aws:userid", "AIDACKCEVSQ6C2EXAMPLE") + .build(); + + Map> conditions = trustConfig.getConditions(); + assertTrue(conditions.containsKey("StringEquals")); + + Map stringEqualsConditions = conditions.get("StringEquals"); + assertEquals(2, stringEqualsConditions.size()); + assertEquals("us-west-2", stringEqualsConditions.get("aws:RequestedRegion")); + assertEquals("AIDACKCEVSQ6C2EXAMPLE", stringEqualsConditions.get("aws:userid")); + } + + @Test + public void testTrustConfigurationBuilderGcpServiceAccount() { + TrustConfiguration trustConfig = TrustConfiguration.builder() + .addTrustedPrincipal("service-account@my-project.iam.gserviceaccount.com") + .addTrustedPrincipal("another-sa@different-project.iam.gserviceaccount.com") + .build(); + + List expectedPrincipals = Arrays.asList( + "service-account@my-project.iam.gserviceaccount.com", + "another-sa@different-project.iam.gserviceaccount.com" + ); + + assertEquals(expectedPrincipals, trustConfig.getTrustedPrincipals()); + } + + @Test + public void testTrustConfigurationBuilderAliCloudPrincipals() { + TrustConfiguration trustConfig = TrustConfiguration.builder() + .addTrustedPrincipal("1234567890123456") // AliCloud account ID + .addTrustedPrincipal("acs:ram::1234567890123456:user/AliUser") // AliCloud RAM user + .build(); + + List expectedPrincipals = Arrays.asList( + "1234567890123456", + "acs:ram::1234567890123456:user/AliUser" + ); + + assertEquals(expectedPrincipals, trustConfig.getTrustedPrincipals()); + } + + @Test + public void testTrustConfigurationBuilderComplexScenario() { + TrustConfiguration trustConfig = TrustConfiguration.builder() + .addTrustedPrincipal("arn:aws:iam::123456789012:root") // AWS account + .addTrustedPrincipal("service-account@project.iam.gserviceaccount.com") // GCP service account + .addTrustedPrincipal("1234567890123456") // AliCloud account + .addCondition("StringEquals", "aws:RequestedRegion", "us-west-2") + .addCondition("StringEquals", "sts:ExternalId", "cross-cloud-external-id") + .addCondition("Bool", "aws:MultiFactorAuthPresent", "true") + .build(); + + List expectedPrincipals = Arrays.asList( + "arn:aws:iam::123456789012:root", + "service-account@project.iam.gserviceaccount.com", + "1234567890123456" + ); + + assertEquals(expectedPrincipals, trustConfig.getTrustedPrincipals()); + + Map> conditions = trustConfig.getConditions(); + assertTrue(conditions.containsKey("StringEquals")); + assertTrue(conditions.containsKey("Bool")); + + assertEquals("us-west-2", conditions.get("StringEquals").get("aws:RequestedRegion")); + assertEquals("cross-cloud-external-id", conditions.get("StringEquals").get("sts:ExternalId")); + assertEquals("true", conditions.get("Bool").get("aws:MultiFactorAuthPresent")); + } + + @Test + public void testTrustConfigurationBuilderEmptyBuilder() { + TrustConfiguration trustConfig = TrustConfiguration.builder().build(); + + assertTrue(trustConfig.getTrustedPrincipals().isEmpty()); + assertTrue(trustConfig.getConditions().isEmpty()); + } + + @Test + public void testTrustConfigurationBuilderIgnoreNullAndEmptyPrincipals() { + TrustConfiguration trustConfig = TrustConfiguration.builder() + .addTrustedPrincipal("arn:aws:iam::123456789012:root") + .addTrustedPrincipal(null) + .addTrustedPrincipal("") + .addTrustedPrincipal(" ") // whitespace only + .addTrustedPrincipal("arn:aws:iam::987654321098:root") + .build(); + + List expectedPrincipals = Arrays.asList( + "arn:aws:iam::123456789012:root", + "arn:aws:iam::987654321098:root" + ); + + assertEquals(expectedPrincipals, trustConfig.getTrustedPrincipals()); + } + + @Test + public void testTrustConfigurationBuilderIgnoreNullConditions() { + TrustConfiguration trustConfig = TrustConfiguration.builder() + .addTrustedPrincipal("arn:aws:iam::123456789012:root") + .addCondition("StringEquals", "aws:RequestedRegion", "us-west-2") + .addCondition(null, "key", "value") // null operator + .addCondition("StringEquals", null, "value") // null key + .addCondition("StringEquals", "key", null) // null value + .build(); + + Map> conditions = trustConfig.getConditions(); + assertEquals(1, conditions.size()); + assertTrue(conditions.containsKey("StringEquals")); + assertEquals(1, conditions.get("StringEquals").size()); + assertEquals("us-west-2", conditions.get("StringEquals").get("aws:RequestedRegion")); + } + + @Test + public void testTrustConfigurationEquality() { + TrustConfiguration trustConfig1 = TrustConfiguration.builder() + .addTrustedPrincipal("arn:aws:iam::123456789012:root") + .addCondition("StringEquals", "aws:RequestedRegion", "us-west-2") + .build(); + + TrustConfiguration trustConfig2 = TrustConfiguration.builder() + .addTrustedPrincipal("arn:aws:iam::123456789012:root") + .addCondition("StringEquals", "aws:RequestedRegion", "us-west-2") + .build(); + + TrustConfiguration trustConfig3 = TrustConfiguration.builder() + .addTrustedPrincipal("arn:aws:iam::987654321098:root") + .addCondition("StringEquals", "aws:RequestedRegion", "us-west-2") + .build(); + + assertEquals(trustConfig1, trustConfig2); + assertNotEquals(trustConfig1, trustConfig3); + assertEquals(trustConfig1.hashCode(), trustConfig2.hashCode()); + } + + @Test + public void testTrustConfigurationToString() { + TrustConfiguration trustConfig = TrustConfiguration.builder() + .addTrustedPrincipal("arn:aws:iam::123456789012:root") + .addCondition("StringEquals", "aws:RequestedRegion", "us-west-2") + .build(); + + String toString = trustConfig.toString(); + assertTrue(toString.contains("trustedPrincipals")); + assertTrue(toString.contains("conditions")); + assertTrue(toString.contains("arn:aws:iam::123456789012:root")); + } + + @Test + public void testTrustConfigurationImmutable() { + TrustConfiguration trustConfig = TrustConfiguration.builder() + .addTrustedPrincipal("arn:aws:iam::123456789012:root") + .build(); + + List principals = trustConfig.getTrustedPrincipals(); + Map> conditions = trustConfig.getConditions(); + + // Modifying returned lists/maps should not affect the original + principals.add("new-principal"); + conditions.put("NewOperator", Map.of("key", "value")); + + // Original should remain unchanged + assertEquals(1, trustConfig.getTrustedPrincipals().size()); + assertEquals(0, trustConfig.getConditions().size()); + } +} \ No newline at end of file diff --git a/iam/pom.xml b/iam/pom.xml new file mode 100644 index 00000000..9bc5d270 --- /dev/null +++ b/iam/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + + com.salesforce.multicloudj + multicloudj-parent + ${revision} + ../pom.xml + + iam + pom + MultiCloudJ IAM + MultiCloudJ Identity and Access Management + + iam-client + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index ae33fb07..e7018395 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,7 @@ + iam blob docstore examples