diff --git a/iam/iam-client/pom.xml b/iam/iam-client/pom.xml new file mode 100644 index 00000000..cec6e465 --- /dev/null +++ b/iam/iam-client/pom.xml @@ -0,0 +1,79 @@ + + + 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.projectlombok + lombok + 1.18.34 + provided + + + + + org.mockito + mockito-core + 5.16.1 + test + + + org.junit.jupiter + junit-jupiter-api + 5.12.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 + + + + + + + + \ 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..ef61d0bd --- /dev/null +++ b/iam/iam-client/src/main/java/com/salesforce/multicloudj/iam/client/IamClient.java @@ -0,0 +1,210 @@ +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("2012-10-17")  // Use provider-specific version (AWS example)
+ *     .statement("StorageAccess")
+ *         .effect("Allow")
+ *         .addAction("storage:GetObject")
+ *         .addResource("storage://my-bucket/*")
+ *     .endStatement()
+ *     .build();
+ *
+ * // Attach policy
+ * client.attachInlinePolicy(policy, "123456789012", "us-west-2", "my-bucket");
+ * 
+ */ +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..833491d6 --- /dev/null +++ b/iam/iam-client/src/main/java/com/salesforce/multicloudj/iam/model/CreateOptions.java @@ -0,0 +1,60 @@ +package com.salesforce.multicloudj.iam.model; + +import java.util.Objects; +import lombok.Builder; +import lombok.Getter; + +/** + * 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. + * + *

Permission boundary identifiers are provider-specific and translated internally + * by the implementation layer. The client accepts the native format for the target + * cloud provider. + * + *

Usage example: + *

+ * CreateOptions options = CreateOptions.builder()
+ *     .path("/service-roles/")
+ *     .maxSessionDuration(3600) // 1 hour in seconds
+ *     .permissionBoundary("policy-identifier") // Provider-specific format
+ *     .build();
+ * 
+ */ +@Getter +@Builder +public class CreateOptions { + private final String path; + private final Integer maxSessionDuration; + private final String 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 + '\'' + + '}'; + } +} \ 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..54f74340 --- /dev/null +++ b/iam/iam-client/src/main/java/com/salesforce/multicloudj/iam/model/PolicyDocument.java @@ -0,0 +1,86 @@ +package com.salesforce.multicloudj.iam.model; + +import com.salesforce.multicloudj.common.exceptions.InvalidArgumentException; +import java.util.List; +import java.util.Objects; +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; + +/** + * 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("2012-10-17")
+ *     .statement(Statement.builder()
+ *         .sid("StorageAccess")
+ *         .effect("Allow")
+ *         .action("storage:GetObject")
+ *         .action("storage:PutObject")
+ *         .principal("arn:aws:iam::123456789012:user/ExampleUser")
+ *         .resource("storage://my-bucket/*")
+ *         .condition("StringEquals", "aws:RequestedRegion", "us-west-2")
+ *         .build())
+ *     .build();
+ * 
+ */ +@Getter +public class PolicyDocument { + private final String version; + private final List statements; + + @Builder + private PolicyDocument(String version, @Singular List statements) { + // Validate version is provided + if (version == null) { + throw new InvalidArgumentException("Version is required"); + } + + // Filter out null statements and validate at least one exists + List filteredStatements = statements != null + ? statements.stream().filter(Objects::nonNull) + .collect(java.util.stream.Collectors.toList()) + : new java.util.ArrayList<>(); + + if (filteredStatements.isEmpty()) { + throw new InvalidArgumentException("At least one statement is required"); + } + + this.version = version; + this.statements = filteredStatements; + } + + + + @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 + + '}'; + } +} \ 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..d98921ff --- /dev/null +++ b/iam/iam-client/src/main/java/com/salesforce/multicloudj/iam/model/Statement.java @@ -0,0 +1,133 @@ +package com.salesforce.multicloudj.iam.model; + +import com.salesforce.multicloudj.common.exceptions.InvalidArgumentException; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; + +/** + * 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")
+ *     .action("storage:GetObject")
+ *     .action("storage:PutObject")
+ *     .principal("arn:aws:iam::123456789012:user/ExampleUser")
+ *     .resource("storage://my-bucket/*")
+ *     .condition("StringEquals", "aws:RequestedRegion", "us-west-2")
+ *     .build();
+ * 
+ */ +@Getter +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; + + @Builder + private Statement(String sid, String effect, + @Singular List principals, + @Singular List actions, + @Singular List resources, + Map> conditions) { + // Validate effect + if (effect == null || effect.trim().isEmpty()) { + throw new InvalidArgumentException("Effect is required and cannot be empty"); + } + + // Filter out null/empty/whitespace values and validate actions + this.sid = sid; + this.effect = effect; + this.principals = filterValidStrings(principals); + this.actions = filterValidStrings(actions); + this.resources = filterValidStrings(resources); + + // Validate that at least one action exists after filtering + if (this.actions.isEmpty()) { + throw new InvalidArgumentException("At least one action is required"); + } + + this.conditions = conditions != null ? conditions : new java.util.HashMap<>(); + } + + private static List filterValidStrings(List input) { + if (input == null) { + return new java.util.ArrayList<>(); + } + return input.stream() + .filter(s -> s != null && !s.trim().isEmpty()) + .collect(java.util.stream.Collectors.toList()); + } + + /** + * Custom builder for Statement to handle conditions. + */ + public static class StatementBuilder { + /** + * Adds a condition to the statement. + * + * @param operator the condition operator + * @param key the condition key + * @param value the condition value + * @return this builder + */ + public StatementBuilder condition(String operator, String key, Object value) { + if (operator != null && key != null && value != null) { + if (this.conditions == null) { + this.conditions = new java.util.HashMap<>(); + } + this.conditions.computeIfAbsent(operator, k -> new java.util.HashMap<>()) + .put(key, value); + } + return this; + } + } + + + + @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 + + '}'; + } +} \ 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..e059a2ab --- /dev/null +++ b/iam/iam-client/src/main/java/com/salesforce/multicloudj/iam/model/TrustConfiguration.java @@ -0,0 +1,126 @@ +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; + +import lombok.Getter; + +/** + * 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. + * + *

Principal identifiers are accepted in their native cloud format and translated internally: + * - AWS: ARN format (arn:aws:iam::account:type/name) + * - GCP: Email format (serviceaccount@project.iam.gserviceaccount.com) + * - AliCloud: ACS format (acs:ram::account:type/name) or account ID + */ +@Getter +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(); + } + + + @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 identifier in cloud-native format (AWS ARN, GCP email, AliCloud ACS ARN or account ID) + * @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 identifiers in cloud-native formats + * @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", "DateGreaterThan") + * @param key the condition key in cloud-native format (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..4bb1283e --- /dev/null +++ b/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/client/IamClientTest.java @@ -0,0 +1,65 @@ +package com.salesforce.multicloudj.iam.client; + +import org.junit.jupiter.api.Test; + +import java.net.URI; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * 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); + } + +} \ 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..9feb46b7 --- /dev/null +++ b/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/model/CreateOptionsTest.java @@ -0,0 +1,258 @@ +package com.salesforce.multicloudj.iam.model; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * 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 testCreateOptionsBuilderIndividualFields() { + // Test path only + CreateOptions pathOptions = CreateOptions.builder() + .path("/application/backend/") + .build(); + + assertEquals("/application/backend/", pathOptions.getPath()); + assertNull(pathOptions.getMaxSessionDuration()); + assertNull(pathOptions.getPermissionBoundary()); + + // Test maxSessionDuration only + CreateOptions durationOptions = CreateOptions.builder() + .maxSessionDuration(7200) + .build(); + + assertNull(durationOptions.getPath()); + assertEquals(Integer.valueOf(7200), durationOptions.getMaxSessionDuration()); + assertNull(durationOptions.getPermissionBoundary()); + + // Test permissionBoundary only (AWS example) + CreateOptions boundaryOptions = CreateOptions.builder() + .permissionBoundary("arn:aws:iam::123456789012:policy/DeveloperBoundary") + .build(); + + assertNull(boundaryOptions.getPath()); + assertNull(boundaryOptions.getMaxSessionDuration()); + assertEquals("arn:aws:iam::123456789012:policy/DeveloperBoundary", boundaryOptions.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 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 testCreateOptionsEqualsAndHashCode() { + 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 differentPath = CreateOptions.builder() + .path("/different/") + .maxSessionDuration(3600) + .permissionBoundary("arn:aws:iam::123456789012:policy/TestBoundary") + .build(); + + CreateOptions differentDuration = CreateOptions.builder() + .path("/test/") + .maxSessionDuration(7200) + .permissionBoundary("arn:aws:iam::123456789012:policy/TestBoundary") + .build(); + + CreateOptions nullOptions = CreateOptions.builder().build(); + CreateOptions anotherNullOptions = CreateOptions.builder().build(); + + // Test equals + assertEquals(options1, options2); + assertEquals(options1, options1); // same object + assertNotEquals(options1, differentPath); + assertNotEquals(options1, differentDuration); + assertNotEquals(options1, null); + assertNotEquals(options1, "not create options"); + assertEquals(nullOptions, anotherNullOptions); + + // Test hashCode + assertEquals(options1.hashCode(), options2.hashCode()); + assertNotEquals(options1.hashCode(), differentPath.hashCode()); + assertEquals(nullOptions.hashCode(), anotherNullOptions.hashCode()); + } + + @Test + public void testCreateOptionsToString() { + // Test with all fields populated + CreateOptions options = CreateOptions.builder() + .path("/test/") + .maxSessionDuration(7200) + .permissionBoundary("arn:aws:iam::123456789012:policy/TestBoundary") + .build(); + + String result = options.toString(); + assertTrue(result.contains("path='/test/'")); + assertTrue(result.contains("maxSessionDuration=7200")); + assertTrue(result.contains("permissionBoundary='arn:aws:iam::123456789012:policy/TestBoundary'")); + + // Test with null values + CreateOptions nullOptions = CreateOptions.builder().build(); + String nullResult = nullOptions.toString(); + assertTrue(nullResult.contains("CreateOptions")); + assertTrue(nullResult.contains("path='null'")); + assertTrue(nullResult.contains("maxSessionDuration=null")); + assertTrue(nullResult.contains("permissionBoundary='null'")); + + // Test with partial values + CreateOptions partialOptions = CreateOptions.builder() + .path("/test/") + .maxSessionDuration(null) + .permissionBoundary("arn:aws:iam::123456789012:policy/TestBoundary") + .build(); + + String partialResult = partialOptions.toString(); + assertTrue(partialResult.contains("path='/test/'")); + assertTrue(partialResult.contains("maxSessionDuration=null")); + assertTrue(partialResult.contains("permissionBoundary='arn:aws:iam::123456789012:policy/TestBoundary'")); + } + + @Test + public void testCreateOptionsBuilderMethodChaining() { + CreateOptions.CreateOptionsBuilder 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()); + } + + + @Test + public void testCreateOptionsBuilderOverwriteValues() { + CreateOptions options = CreateOptions.builder() + .path("/first/") + .path("/second/") // This should overwrite the first value + .maxSessionDuration(3600) + .maxSessionDuration(7200) // This should overwrite the first value + .permissionBoundary("arn:aws:iam::123456789012:policy/FirstBoundary") + .permissionBoundary("arn:aws:iam::123456789012:policy/SecondBoundary") // This should overwrite + .build(); + + assertEquals("/second/", options.getPath()); + assertEquals(Integer.valueOf(7200), options.getMaxSessionDuration()); + assertEquals("arn:aws:iam::123456789012:policy/SecondBoundary", options.getPermissionBoundary()); + } + + @Test + public void testCreateOptionsBuilderProviderSpecificExamples() { + // AWS Example + CreateOptions awsOptions = CreateOptions.builder() + .path("/foo/") + .maxSessionDuration(43200) // 12 hours + .permissionBoundary("arn:aws:iam::123456789012:policy/PowerUserBoundary") + .build(); + + assertEquals("/foo/", awsOptions.getPath()); + assertEquals(Integer.valueOf(43200), awsOptions.getMaxSessionDuration()); + assertEquals("arn:aws:iam::123456789012:policy/PowerUserBoundary", awsOptions.getPermissionBoundary()); + + // GCP Example + CreateOptions gcpOptions = CreateOptions.builder() + .path("/foo/") + .maxSessionDuration(3600) // 1 hour + .permissionBoundary("constraints/compute.restrictLoadBalancerCreationForTypes") + .build(); + + assertEquals("/foo/", gcpOptions.getPath()); + assertEquals(Integer.valueOf(3600), gcpOptions.getMaxSessionDuration()); + assertEquals("constraints/compute.restrictLoadBalancerCreationForTypes", gcpOptions.getPermissionBoundary()); + + // AliCloud Example (using Control Policy) + CreateOptions aliOptions = CreateOptions.builder() + .path("/foo/") + .maxSessionDuration(7200) // 2 hours + .permissionBoundary("cp-bp1example") // Control Policy ID + .build(); + + assertEquals("/foo/", aliOptions.getPath()); + assertEquals(Integer.valueOf(7200), aliOptions.getMaxSessionDuration()); + assertEquals("cp-bp1example", aliOptions.getPermissionBoundary()); // AliCloud Control Policy + } +} \ 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..5fece1c8 --- /dev/null +++ b/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/model/PolicyDocumentTest.java @@ -0,0 +1,374 @@ +package com.salesforce.multicloudj.iam.model; + +import com.salesforce.multicloudj.common.exceptions.InvalidArgumentException; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Unit tests for PolicyDocument builder pattern. + */ +public class PolicyDocumentTest { + + private static final String TEST_VERSION = "TEST_VERSION"; + + @Test + public void testPolicyDocumentBuilder() { + PolicyDocument policy = PolicyDocument.builder() + .version("2024-01-01") + .statement(Statement.builder() + .sid("StorageAccess") + .effect("Allow") + .action("storage:GetObject") + .action("storage:PutObject") + .principal("arn:aws:iam::123456789012:user/ExampleUser") + .resource("storage://my-bucket/*") + .condition("StringEquals", "aws:RequestedRegion", "us-west-2") + .build()) + .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() + .version(TEST_VERSION) + .statement(Statement.builder() + .sid("ReadAccess") + .effect("Allow") + .action("storage:GetObject") + .resource("storage://my-bucket/*") + .build()) + .statement(Statement.builder() + .sid("WriteAccess") + .effect("Allow") + .action("storage:PutObject") + .resource("storage://my-bucket/*") + .build()) + .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") + .action("storage:GetObject") + .resource("storage://my-bucket/*") + .build(); + + PolicyDocument policy = PolicyDocument.builder() + .version(TEST_VERSION) + .statement(statement) + .build(); + + assertEquals(1, policy.getStatements().size()); + assertEquals("TestStatement", policy.getStatements().get(0).getSid()); + } + + @Test + public void testEmptyPolicyThrowsException() { + assertThrows(InvalidArgumentException.class, () -> { + PolicyDocument.builder().build(); + }); + } + + @Test + public void testMissingVersionThrowsException() { + assertThrows(InvalidArgumentException.class, () -> { + PolicyDocument.builder() + .statement(Statement.builder() + .sid("TestStatement") + .effect("Allow") + .action("storage:GetObject") + .resource("storage://test-bucket/*") + .build()) + .build(); + }); + } + + @Test + public void testStatementWithoutEffectThrowsException() { + assertThrows(InvalidArgumentException.class, () -> { + PolicyDocument.builder() + .version(TEST_VERSION) + .statement(Statement.builder() + .sid("TestStatement") + .action("storage:GetObject") + .build()) + .build(); + }); + } + + @Test + public void testStatementWithoutActionsThrowsException() { + assertThrows(InvalidArgumentException.class, () -> { + PolicyDocument.builder() + .version(TEST_VERSION) + .statement(Statement.builder() + .sid("TestStatement") + .effect("Allow") + .build()) + .build(); + }); + } + + @Test + public void testVersionHandling() { + // Test custom version + PolicyDocument customVersionPolicy = PolicyDocument.builder() + .version("2023-06-01") + .statement(Statement.builder() + .sid("TestStatement") + .effect("Allow") + .action("storage:GetObject") + .resource("storage://test-bucket/*") + .build()) + .build(); + + assertEquals("2023-06-01", customVersionPolicy.getVersion()); + + // Test default version + PolicyDocument defaultVersionPolicy = PolicyDocument.builder() + .version(TEST_VERSION) + .statement(Statement.builder() + .sid("TestStatement") + .effect("Allow") + .action("storage:GetObject") + .resource("storage://test-bucket/*") + .build()) + .build(); + + assertEquals(TEST_VERSION, defaultVersionPolicy.getVersion()); + } + + @Test + public void testBuilderMethodsWithMultipleValues() { + PolicyDocument policy = PolicyDocument.builder() + .version(TEST_VERSION) + .statement(Statement.builder() + .sid("TestStatement") + .effect("Allow") + .action("storage:GetObject") + .action("storage:PutObject") + .action("storage:DeleteObject") + .action("storage:ListObjects") + .resource("storage://bucket1/*") + .resource("storage://bucket2/*") + .resource("storage://bucket3/*") + .resource("storage://bucket4/*") + .principal("principal1") + .principal("principal2") + .principal("principal3") + .principal("principal4") + .condition("StringEquals", "aws:RequestedRegion", "us-west-2") + .condition("DateGreaterThan", "aws:CurrentTime", "2024-01-01T00:00:00Z") + .build()) + .build(); + + Statement statement = policy.getStatements().get(0); + + // Test actions + assertEquals(4, statement.getActions().size()); + assertTrue(statement.getActions().contains("storage:GetObject")); + assertTrue(statement.getActions().contains("storage:PutObject")); + assertTrue(statement.getActions().contains("storage:DeleteObject")); + assertTrue(statement.getActions().contains("storage:ListObjects")); + + // Test resources + assertEquals(4, statement.getResources().size()); + assertTrue(statement.getResources().contains("storage://bucket1/*")); + assertTrue(statement.getResources().contains("storage://bucket2/*")); + assertTrue(statement.getResources().contains("storage://bucket3/*")); + assertTrue(statement.getResources().contains("storage://bucket4/*")); + + // Test principals + assertEquals(4, statement.getPrincipals().size()); + assertTrue(statement.getPrincipals().contains("principal1")); + assertTrue(statement.getPrincipals().contains("principal2")); + assertTrue(statement.getPrincipals().contains("principal3")); + assertTrue(statement.getPrincipals().contains("principal4")); + + // Test conditions + assertTrue(statement.getConditions().containsKey("StringEquals")); + assertTrue(statement.getConditions().containsKey("DateGreaterThan")); + } + + @Test + public void testBuilderAutoInitialization() { + // Test simple statement creation with Lombok builder + PolicyDocument policy1 = PolicyDocument.builder() + .version("2024-01-01") + .statement(Statement.builder() + .effect("Allow") + .action("storage:GetObject") + .resource("storage://test-bucket/*") + .build()) + .build(); + + assertEquals(1, policy1.getStatements().size()); + assertEquals("Allow", policy1.getStatements().get(0).getEffect()); + + PolicyDocument policy2 = PolicyDocument.builder() + .version("2024-01-01") + .statement(Statement.builder() + .effect("Allow") + .principal("principal1") + .principal("principal2") + .action("storage:GetObject") + .resource("storage://test-bucket/*") + .condition("StringEquals", "key", "value") + .build()) + .build(); + + assertEquals(1, policy2.getStatements().size()); + Statement statement = policy2.getStatements().get(0); + assertEquals("Allow", statement.getEffect()); + assertEquals(1, statement.getActions().size()); + assertEquals(1, statement.getResources().size()); + assertEquals(2, statement.getPrincipals().size()); + } + + @Test + public void testAddNullStatement() { + PolicyDocument policy = PolicyDocument.builder() + .version(TEST_VERSION) + .statement((Statement) null) + .statement(Statement.builder() + .sid("ValidStatement") + .effect("Allow") + .action("storage:GetObject") + .resource("storage://test-bucket/*") + .build()) + .build(); + + assertEquals(1, policy.getStatements().size()); // Null statements are filtered out + assertEquals("ValidStatement", policy.getStatements().get(0).getSid()); + } + + @Test + public void testMixingAddStatementAndBuilder() { + Statement preBuiltStatement = Statement.builder() + .sid("PreBuilt") + .effect("Deny") + .action("storage:DeleteObject") + .resource("storage://sensitive-bucket/*") + .build(); + + PolicyDocument policy = PolicyDocument.builder() + .version(TEST_VERSION) + .statement(preBuiltStatement) + .statement(Statement.builder() + .sid("BuiltInline") + .effect("Allow") + .action("storage:GetObject") + .resource("storage://public-bucket/*") + .build()) + .build(); + + assertEquals(2, policy.getStatements().size()); + assertEquals("PreBuilt", policy.getStatements().get(0).getSid()); + assertEquals("BuiltInline", policy.getStatements().get(1).getSid()); + } + + @Test + public void testPolicyDocumentEqualsAndHashCode() { + PolicyDocument policy1 = PolicyDocument.builder() + .version("2024-01-01") + .statement(Statement.builder() + .sid("TestStatement") + .effect("Allow") + .action("storage:GetObject") + .resource("storage://test-bucket/*") + .build()) + .build(); + + PolicyDocument policy2 = PolicyDocument.builder() + .version("2024-01-01") + .statement(Statement.builder() + .sid("TestStatement") + .effect("Allow") + .action("storage:GetObject") + .resource("storage://test-bucket/*") + .build()) + .build(); + + PolicyDocument policy3 = PolicyDocument.builder() + .version("2023-01-01") + .statement(Statement.builder() + .sid("DifferentStatement") + .effect("Deny") + .action("storage:DeleteObject") + .resource("storage://test-bucket/*") + .build()) + .build(); + + // Test equals + assertEquals(policy1, policy2); + assertNotEquals(policy1, policy3); + assertNotEquals(policy1, null); + assertNotEquals(policy1, "not a policy"); + assertEquals(policy1, policy1); // same object + + // Test hashCode + assertEquals(policy1.hashCode(), policy2.hashCode()); + assertNotEquals(policy1.hashCode(), policy3.hashCode()); + } + + @Test + public void testPolicyDocumentToString() { + PolicyDocument policy = PolicyDocument.builder() + .version("2024-01-01") + .statement(Statement.builder() + .sid("TestStatement") + .effect("Allow") + .action("storage:GetObject") + .resource("storage://test-bucket/*") + .build()) + .build(); + + String result = policy.toString(); + assertTrue(result.contains("2024-01-01")); + assertTrue(result.contains("TestStatement")); + assertTrue(result.contains("PolicyDocument")); + } + + + @Test + public void testEndStatementWithoutCurrentStatement() { + // Simple test with Lombok builder + PolicyDocument policy = PolicyDocument.builder() + .version(TEST_VERSION) + .statement(Statement.builder() + .sid("TestStatement") + .effect("Allow") + .action("storage:GetObject") + .resource("storage://test-bucket/*") + .build()) + .build(); + + assertEquals(1, policy.getStatements().size()); + } +} \ 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..e31f95c4 --- /dev/null +++ b/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/model/StatementTest.java @@ -0,0 +1,306 @@ +package com.salesforce.multicloudj.iam.model; + +import com.salesforce.multicloudj.common.exceptions.InvalidArgumentException; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Unit tests for Statement builder pattern and functionality. + */ +public class StatementTest { + + @Test + public void testStatementBuilder() { + Statement statement = Statement.builder() + .sid("TestStatement") + .effect("Allow") + .action("storage:GetObject") + .action("storage:PutObject") + .resource("storage://my-bucket/*") + .principal("arn:aws:iam::123456789012:user/TestUser") + .condition("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") + .action("storage:DeleteObject") + .resource("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 testStatementBuilderMultipleResources() { + List expectedResources = Arrays.asList("storage://bucket1/*", "storage://bucket2/*"); + + Statement statement = Statement.builder() + .sid("MultiResourceStatement") + .effect("Allow") + .action("storage:GetObject") + .resource("storage://bucket1/*") + .resource("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") + .action("storage:GetObject") + .resource("storage://shared-bucket/*") + .principal("arn:aws:iam::123456789012:user/User1") + .principal("arn:aws:iam::123456789012:user/User2") + .build(); + + assertEquals(expectedPrincipals, statement.getPrincipals()); + } + + @Test + public void testStatementBuilderMultipleConditions() { + Statement statement = Statement.builder() + .sid("MultiConditionStatement") + .effect("Allow") + .action("storage:GetObject") + .resource("storage://conditional-bucket/*") + .condition("StringEquals", "aws:RequestedRegion", "us-west-2") + .condition("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 testStatementWithoutSid() { + Statement statement = Statement.builder() + .effect("Allow") + .action("storage:GetObject") + .resource("storage://no-sid-bucket/*") + .build(); + + assertNull(statement.getSid()); + assertEquals("Allow", statement.getEffect()); + } + + @Test + public void testEmptyStatementThrowsException() { + assertThrows(InvalidArgumentException.class, () -> { + Statement.builder().build(); + }); + } + + @Test + public void testStatementWithoutEffectThrowsException() { + assertThrows(InvalidArgumentException.class, () -> { + Statement.builder() + .sid("NoEffectStatement") + .action("storage:GetObject") + .resource("storage://test-bucket/*") + .build(); + }); + } + + @Test + public void testStatementWithoutActionsThrowsException() { + assertThrows(InvalidArgumentException.class, () -> { + Statement.builder() + .sid("NoActionsStatement") + .effect("Allow") + .resource("storage://test-bucket/*") + .build(); + }); + } + + @Test + public void testStatementWithEmptyEffect() { + assertThrows(InvalidArgumentException.class, () -> { + Statement.builder() + .sid("EmptyEffectStatement") + .effect("") + .action("storage:GetObject") + .build(); + }); + } + + @Test + public void testStatementWithWhitespaceEffect() { + assertThrows(InvalidArgumentException.class, () -> { + Statement.builder() + .sid("WhitespaceEffectStatement") + .effect(" ") + .action("storage:GetObject") + .build(); + }); + } + + @Test + public void testNullAndEmptyValueHandling() { + Statement statement = Statement.builder() + .effect("Allow") + .action(null) + .action("") + .action(" ") + .action("storage:GetObject") + .resource(null) + .resource("") + .resource(" ") + .resource("storage://test-bucket/*") + .principal(null) + .principal("") + .principal(" ") + .principal("valid-principal") + .condition(null, "key", "value") + .condition("StringEquals", null, "value") + .condition("StringEquals", "key", null) + .condition("StringEquals", "aws:RequestedRegion", "us-west-2") + .build(); + + assertEquals(1, statement.getActions().size()); + assertEquals("storage:GetObject", statement.getActions().get(0)); + + assertEquals(1, statement.getResources().size()); + assertEquals("storage://test-bucket/*", statement.getResources().get(0)); + + assertEquals(1, statement.getPrincipals().size()); + assertEquals("valid-principal", statement.getPrincipals().get(0)); + + assertEquals(1, statement.getConditions().size()); + assertTrue(statement.getConditions().containsKey("StringEquals")); + assertEquals("us-west-2", statement.getConditions().get("StringEquals").get("aws:RequestedRegion")); + } + + @Test + public void testListMethodsWithNullValues() { + List principals = Arrays.asList("principal1", null, "", " ", "principal2"); + List actions = Arrays.asList("storage:GetObject", null, "", " ", "storage:PutObject"); + List resources = Arrays.asList("storage://bucket1/*", null, "", " ", "storage://bucket2/*"); + + Statement statement = Statement.builder() + .effect("Allow") + .actions(actions) + .resources(resources) + .principals(principals) + .build(); + + assertEquals(2, statement.getActions().size()); + assertTrue(statement.getActions().contains("storage:GetObject")); + assertTrue(statement.getActions().contains("storage:PutObject")); + + assertEquals(2, statement.getResources().size()); + assertTrue(statement.getResources().contains("storage://bucket1/*")); + assertTrue(statement.getResources().contains("storage://bucket2/*")); + + assertEquals(2, statement.getPrincipals().size()); + assertTrue(statement.getPrincipals().contains("principal1")); + assertTrue(statement.getPrincipals().contains("principal2")); + } + + @Test + public void testListMethodsWithNullLists() { + // Test that individual actions and resources are preserved even without using list methods + Statement statement = Statement.builder() + .effect("Allow") + .action("storage:GetObject") + .resource("storage://test-bucket/*") + .build(); + + assertEquals(1, statement.getActions().size()); + assertEquals(1, statement.getResources().size()); + assertTrue(statement.getPrincipals().isEmpty()); + } + + @Test + public void testStatementEqualsAndHashCode() { + Statement statement1 = Statement.builder() + .sid("TestStatement") + .effect("Allow") + .action("storage:GetObject") + .resource("storage://test-bucket/*") + .principal("principal1") + .build(); + + Statement statement2 = Statement.builder() + .sid("TestStatement") + .effect("Allow") + .action("storage:GetObject") + .resource("storage://test-bucket/*") + .principal("principal1") + .build(); + + Statement statement3 = Statement.builder() + .sid("DifferentStatement") + .effect("Allow") + .action("storage:GetObject") + .resource("storage://test-bucket/*") + .build(); + + // Test equals + assertEquals(statement1, statement2); + assertNotEquals(statement1, statement3); + assertNotEquals(statement1, null); + assertNotEquals(statement1, "not a statement"); + assertEquals(statement1, statement1); // same object + + // Test hashCode + assertEquals(statement1.hashCode(), statement2.hashCode()); + assertNotEquals(statement1.hashCode(), statement3.hashCode()); + } + + @Test + public void testStatementToString() { + Statement statement = Statement.builder() + .sid("TestStatement") + .effect("Allow") + .action("storage:GetObject") + .resource("storage://test-bucket/*") + .principal("principal1") + .condition("StringEquals", "aws:RequestedRegion", "us-west-2") + .build(); + + String result = statement.toString(); + assertTrue(result.contains("TestStatement")); + assertTrue(result.contains("Allow")); + assertTrue(result.contains("storage:GetObject")); + assertTrue(result.contains("storage://test-bucket/*")); + assertTrue(result.contains("principal1")); + assertTrue(result.contains("StringEquals")); + } + +} \ 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..afc39e1c --- /dev/null +++ b/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/model/TrustConfigurationTest.java @@ -0,0 +1,268 @@ +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.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * 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") + .addCondition("expression", "location", "resource.name.startsWith('projects/my-project/zones/us-west')") + .build(); + + List expectedPrincipals = Arrays.asList( + "service-account@my-project.iam.gserviceaccount.com", + "another-sa@different-project.iam.gserviceaccount.com" + ); + + assertEquals(expectedPrincipals, trustConfig.getTrustedPrincipals()); + + Map> conditions = trustConfig.getConditions(); + assertTrue(conditions.containsKey("expression")); + assertEquals("resource.name.startsWith('projects/my-project/zones/us-west')", conditions.get("expression").get("location")); + } + + @Test + public void testTrustConfigurationBuilderAliCloudPrincipals() { + TrustConfiguration trustConfig = TrustConfiguration.builder() + .addTrustedPrincipal("1234567890123456") // AliCloud account ID + .addTrustedPrincipal("acs:ram::1234567890123456:user/AliUser") // AliCloud RAM user + .addCondition("StringEquals", "acs:CurrentRegion", "us-west-1") + .build(); + + List expectedPrincipals = Arrays.asList( + "1234567890123456", + "acs:ram::1234567890123456:user/AliUser" + ); + + assertEquals(expectedPrincipals, trustConfig.getTrustedPrincipals()); + + Map> conditions = trustConfig.getConditions(); + assertTrue(conditions.containsKey("StringEquals")); + assertEquals("us-west-1", conditions.get("StringEquals").get("acs:CurrentRegion")); + } + + @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 testNullAndEmptyValueHandling() { + // Test null and empty principals + TrustConfiguration trustConfig = TrustConfiguration.builder() + .addTrustedPrincipal("arn:aws:iam::123456789012:root") + .addTrustedPrincipal(null) + .addTrustedPrincipal("") + .addTrustedPrincipal(" ") // whitespace only + .addTrustedPrincipal("arn:aws:iam::987654321098:root") + .addTrustedPrincipals(null) // null list + .addTrustedPrincipals(Arrays.asList("valid-principal", null, "", " ")) + .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(); + + // Should only have valid principals + assertEquals(3, trustConfig.getTrustedPrincipals().size()); + assertTrue(trustConfig.getTrustedPrincipals().contains("arn:aws:iam::123456789012:root")); + assertTrue(trustConfig.getTrustedPrincipals().contains("arn:aws:iam::987654321098:root")); + assertTrue(trustConfig.getTrustedPrincipals().contains("valid-principal")); + + // Should only have valid condition + assertEquals(1, trustConfig.getConditions().size()); + assertTrue(trustConfig.getConditions().containsKey("StringEquals")); + assertEquals("us-west-2", trustConfig.getConditions().get("StringEquals").get("aws:RequestedRegion")); + } + + + + @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 testTrustConfigurationEqualsAndHashCodeWithNullChecks() { + TrustConfiguration trust1 = TrustConfiguration.builder() + .addTrustedPrincipal("arn:aws:iam::123456789012:root") + .addCondition("StringEquals", "aws:RequestedRegion", "us-west-2") + .build(); + + TrustConfiguration trust2 = TrustConfiguration.builder() + .addTrustedPrincipal("arn:aws:iam::123456789012:root") + .addCondition("StringEquals", "aws:RequestedRegion", "us-west-2") + .build(); + + // Test equals with null and different types + assertNotEquals(trust1, null); + assertNotEquals(trust1, "not a trust config"); + assertEquals(trust1, trust1); // same object + assertEquals(trust1, trust2); // equal objects + + // Test hashCode consistency + assertEquals(trust1.hashCode(), trust2.hashCode()); + } + +} \ 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 27ac0264..dcf18a41 100644 --- a/pom.xml +++ b/pom.xml @@ -32,6 +32,7 @@ + iam blob docstore examples