This is an automated email from the ASF dual-hosted git repository.

sammichen pushed a commit to branch HDDS-13323-sts
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/HDDS-13323-sts by this push:
     new b79a8dd4305 HDDS-13724. [STS] Part 1 - Create utility to convert IAM 
policy to groupings of OzoneObj and Acls (#9239)
b79a8dd4305 is described below

commit b79a8dd4305c85ffbbe87fef5e83de9ed62ef9e6
Author: fmorg-git <[email protected]>
AuthorDate: Tue Nov 18 03:50:49 2025 -0800

    HDDS-13724. [STS] Part 1 - Create utility to convert IAM policy to 
groupings of OzoneObj and Acls (#9239)
---
 .../security/acl/iam/IamSessionPolicyResolver.java | 336 +++++++++++++++++++++
 .../ozone/security/acl/iam/package-info.java       |  21 ++
 .../acl/iam/TestIamSessionPolicyResolver.java      | 313 +++++++++++++++++++
 3 files changed, 670 insertions(+)

diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/iam/IamSessionPolicyResolver.java
 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/iam/IamSessionPolicyResolver.java
new file mode 100644
index 00000000000..23fbf063c87
--- /dev/null
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/iam/IamSessionPolicyResolver.java
@@ -0,0 +1,336 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.security.acl.iam;
+
+import static 
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_REQUEST;
+import static 
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.NOT_SUPPORTED_OPERATION;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Objects;
+import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.security.acl.AssumeRoleRequest;
+
+/**
+ * Resolves a limited subset of AWS IAM session policies into Ozone ACL grants,
+ * according to either the RangerOzoneAuthorizer or OzoneNativeAuthorizer 
constructs.
+ * <p>
+ * Here are some differences between the RangerOzoneAuthorizer and 
OzoneNativeAuthorizer:
+ *    - RangerOzoneAuthorizer doesn't currently use ResourceType.PREFIX, 
whereas OzoneNativeAuthorizer does.
+ *    - OzoneNativeAuthorizer doesn't allow wildcards in bucket names (ex. 
ResourceArn `arn:aws:s3:::*`,
+ *    `arn:aws:s3:::bucket*` or `*`), whereas RangerOzoneAuthorizer does.
+ *    - For OzoneNativeAuthorizer, certain object wildcards are accepted.   
For example, ResourceArn
+ *    `arn:aws:s3:::myBucket/*` and `arn:aws:s3:::myBucket/folder/logs/*` are 
accepted but not
+ *    `arn:aws:s3:::myBucket/file*.txt`.
+ * <p>
+ * The only supported ResourceArn has prefix arn:aws:s3::: - all others will 
throw
+ * OMException with NOT_SUPPORTED_OPERATION.
+ * <p>
+ * The only supported Condition operator is StringEquals - all others will 
throw
+ * OMException with NOT_SUPPORTED_OPERATION.  Furthermore, only one Condition 
is supported in a
+ * statement.  The value StringEquals is case-sensitive per the
+ * <a 
href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html";>
+ * AWS spec</a>.
+ * <p>
+ * The only supported Condition key name is s3:prefix - all others will throw
+ * OMException with NOT_SUPPORTED_OPERATION.  s3:prefix is case-insensitive 
per the
+ * <a 
href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html";>AWS
 spec</a>.
+ * <p>
+ * The only supported Effect is Allow - all others will throw OMException with 
NOT_SUPPORTED_OPERATION.  This
+ * value is case-sensitive per the
+ * <a 
href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_effect.html";>AWS
 spec</a>.
+ * <p>
+ * If a (currently) unsupported S3 action is requested, such as 
s3:GetAccelerateConfiguration,
+ * it will be silently ignored.
+ * <p>
+ * Supported wildcard expansions in Actions are: s3:*, s3:Get*, s3:Put*, 
s3:List*,
+ * s3:Create*, and s3:Delete*.
+ */
+public final class IamSessionPolicyResolver {
+
+  private static final ObjectMapper MAPPER = new ObjectMapper();
+
+  // JSON length is limited per AWS policy.  See 
https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
+  // under Policy section.
+  private static final int MAX_JSON_LENGTH = 2048;
+
+  private IamSessionPolicyResolver() {
+  }
+
+  /**
+   * Resolves an S3 IAM session policy in the form of a JSON String to a data 
structure comprising
+   * the IOzoneObjs and permissions that IAM policy grants (if any).
+   * <p>
+   * Each entry represents a path (such as /s3v/bucket1 or /s3v/bucket1/*) and 
a set of
+   * permissions (such as READ, LIST, CREATE).
+   * <p>
+   * The OzoneObj can be different depending on the AuthorizerType (see main 
Javadoc at top of file
+   * for examples).
+   * <p>
+   *
+   * @param policyJson     the IAM session policy
+   * @param volumeName     the volume under which the resource(s) live.  This 
may not be s3v in
+   *                       multi-tenant scenarios
+   * @param authorizerType whether the IOzoneObjs should be generated for use 
by the
+   *                       RangerOzoneAuthorizer or the OzoneNativeAuthorizer
+   * @return the data structure comprising the paths and permission pairings 
that
+   * the session policy grants (if any)
+   * @throws OMException if the policy JSON is invalid, malformed, or contains 
unsupported features
+   */
+  public static Set<AssumeRoleRequest.OzoneGrant> resolve(String policyJson, 
String volumeName,
+      AuthorizerType authorizerType) throws OMException {
+
+    validateInputParameters(policyJson, volumeName, authorizerType);
+
+    final Set<AssumeRoleRequest.OzoneGrant> result = new LinkedHashSet<>();
+
+    // Parse JSON into set of statements
+    final Set<JsonNode> statements = 
parseJsonAndRetrieveStatements(policyJson);
+
+    for (JsonNode stmt : statements) {
+      validateEffectInJsonStatement(stmt);
+
+      final Set<String> actions = readStringOrArray(stmt.get("Action"));
+      final Set<String> resources = readStringOrArray(stmt.get("Resource"));
+
+      // Parse prefixes from conditions, if any
+      final Set<String> prefixes = parsePrefixesFromConditions(stmt);
+
+      // Map actions to S3Action enum if possible
+      final Set<S3Action> mappedS3Actions = 
mapPolicyActionsToS3Actions(actions);
+      if (mappedS3Actions.isEmpty()) {
+        // No actions recognized - no need to look at Resources for this 
Statement
+        continue;
+      }
+
+      // Categorize resources according to bucket resource, object resource, 
etc
+      final Set<ResourceSpec> resourceSpecs = 
validateAndCategorizeResources(authorizerType, resources);
+
+      // For each action, map to Ozone objects (paths) and acls based on 
resource specs and prefixes
+      final Set<AssumeRoleRequest.OzoneGrant> stmtResults = 
createPathsAndPermissions(
+          volumeName, authorizerType, mappedS3Actions, resourceSpecs, 
prefixes);
+
+      result.addAll(stmtResults);
+    }
+
+    return result;
+  }
+
+  /**
+   * Ensures required input parameters are supplied.
+   */
+  private static void validateInputParameters(String policyJson, String 
volumeName,
+      AuthorizerType authorizerType) throws OMException {
+    if (StringUtils.isBlank(policyJson)) {
+      throw new OMException("The IAM session policy JSON is required", 
INVALID_REQUEST);
+    }
+
+    if (StringUtils.isBlank(volumeName)) {
+      throw new OMException("The volume name is required", INVALID_REQUEST);
+    }
+
+    Objects.requireNonNull(authorizerType, "The authorizer type is required");
+
+    if (policyJson.length() > MAX_JSON_LENGTH) {
+      throw new OMException("Invalid policy JSON - exceeds maximum length of " 
+
+          MAX_JSON_LENGTH + " characters", INVALID_REQUEST);
+    }
+  }
+
+  /**
+   * Parses IAM session policy and retrieve the statement(s).
+   */
+  private static Set<JsonNode> parseJsonAndRetrieveStatements(String 
policyJson) throws OMException {
+    final JsonNode root;
+    try {
+      root = MAPPER.readTree(policyJson);
+    } catch (Exception e) {
+      throw new OMException("Invalid policy JSON (most likely JSON structure 
is incorrect)", e, INVALID_REQUEST);
+    }
+
+    final JsonNode statementsNode = root.path("Statement");
+    if (statementsNode.isMissingNode()) {
+      throw new OMException("Invalid policy JSON - missing Statement", 
INVALID_REQUEST);
+    }
+
+    final Set<JsonNode> statements = new HashSet<>();
+
+    if (statementsNode.isArray()) {
+      statementsNode.forEach(statements::add);
+    } else {
+      statements.add(statementsNode);
+    }
+    return statements;
+  }
+
+  /**
+   * Parses Effect from IAM session policy and ensures it is valid and 
supported.
+   */
+  private static void validateEffectInJsonStatement(JsonNode statement) throws 
OMException {
+    final JsonNode effectNode = statement.get("Effect");
+    if (effectNode != null) {
+      if (effectNode.isTextual()) {
+        final String effect = effectNode.asText();
+        if (!"Allow".equals(effect)) {
+          throw new OMException("Unsupported Effect - " + effect, 
NOT_SUPPORTED_OPERATION);
+        }
+        return;
+      }
+
+      throw new OMException(
+          "Invalid Effect in JSON policy (must be a String) - " + effectNode, 
INVALID_REQUEST);
+    }
+
+    throw new OMException("Effect is missing from JSON policy", 
INVALID_REQUEST);
+  }
+
+  /**
+   * Reads a JsonNode and converts to a Set of String, if the node represents
+   * a textual value or an array of textual values.  Otherwise, returns
+   * an empty List.
+   */
+  private static Set<String> readStringOrArray(JsonNode node) {
+    if (node == null || node.isMissingNode() || node.isNull()) {
+      return Collections.emptySet();
+    }
+    if (node.isTextual()) {
+      return Collections.singleton(node.asText());
+    }
+    if (node.isArray()) {
+      final Set<String> set = new HashSet<>();
+      node.forEach(n -> {
+        if (n.isTextual()) {
+          set.add(n.asText());
+        }
+      });
+      return set;
+    }
+
+    return Collections.emptySet();
+  }
+
+  /**
+   * Parses and returns prefixes from Conditions (if any).  Also validates
+   * that if there is a Condition, there is only one and that the Condition
+   * operator and key name are supported.
+   * <p>
+   * Only the StringEquals operator and s3:prefix key name are supported.
+   */
+  private static Set<String> parsePrefixesFromConditions(JsonNode stmt) throws 
OMException {
+    Set<String> prefixes = Collections.emptySet();
+    final JsonNode cond = stmt.get("Condition");
+    if (cond != null && !cond.isMissingNode() && !cond.isNull()) {
+      if (cond.size() != 1) {
+        throw new OMException("Only one Condition is supported", 
NOT_SUPPORTED_OPERATION);
+      }
+
+      if (!cond.isObject()) {
+        throw new OMException(
+            "Invalid Condition (must have operator StringEquals " + "and key 
name s3:prefix) - " +
+            cond, INVALID_REQUEST);
+      }
+
+      final String operator = cond.fieldNames().next();
+      if (!"StringEquals".equals(operator)) {
+        throw new OMException("Unsupported Condition operator - " + operator, 
NOT_SUPPORTED_OPERATION);
+      }
+
+      final JsonNode operatorValue = cond.get("StringEquals");
+      if ("null".equals(operatorValue.asText())) {
+        throw new OMException("Missing Condition operator - StringEquals", 
INVALID_REQUEST);
+      }
+
+      if (!operatorValue.isObject()) {
+        throw new OMException("Invalid Condition operator value structure - " 
+ operatorValue, INVALID_REQUEST);
+      }
+
+      final String keyName = operatorValue.fieldNames().hasNext() ? 
operatorValue.fieldNames().next() : null;
+      if (!"s3:prefix".equalsIgnoreCase(keyName)) {
+        throw new OMException("Unsupported Condition key name - " + keyName, 
NOT_SUPPORTED_OPERATION);
+      }
+
+      prefixes = readStringOrArray(operatorValue.get(keyName));
+    }
+
+    return prefixes;
+  }
+
+  /**
+   * Maps actions from JSON IAM policy to S3Action enum in order to determine 
what the
+   * permissions should be.
+   */
+  private static Set<S3Action> mapPolicyActionsToS3Actions(Set<String> 
actions) {
+    // TODO implement in future PR
+    return Collections.emptySet();
+  }
+
+  /**
+   * Iterates over resources in IAM policy and determines whether it is a 
bucket resource,
+   * an object resource, a prefix or a wildcard.  The categorization can be 
different
+   * depending on whether the AuthorizerType is Ranger (for 
RangerOzoneAuthorizer) or
+   * native (for OzoneNativeAuthorizer).  See main Javadoc at top of file for 
more
+   * examples of these differences.
+   * <p>
+   * It also validates that the Resource Arn(s) are valid and supported.
+   */
+  private static Set<ResourceSpec> 
validateAndCategorizeResources(AuthorizerType authorizerType,
+      Set<String> resources) throws OMException {
+    // TODO implement in future PR
+    return Collections.emptySet();
+  }
+
+  /**
+   * Iterates over all resources, finds applicable actions (if any) and 
constructs
+   * entries pairing sets of IOzoneObjs with the requisite permissions granted 
(if any).
+   */
+  private static Set<AssumeRoleRequest.OzoneGrant> 
createPathsAndPermissions(String volumeName,
+      AuthorizerType authorizerType, Set<S3Action> mappedS3Actions, 
Set<ResourceSpec> resourceSpecs,
+      Set<String> prefixes) {
+    // TODO implement in future PR
+    return Collections.emptySet();
+  }
+
+  /**
+   * The authorizer type, whether for OzoneNativeAuthorizer or 
RangerOzoneAuthorizer.
+   * The IOzoneObjs generated differ in certain cases depending on the type.
+   * See main Javadoc at top of file for differences.
+   */
+  public enum AuthorizerType {
+    NATIVE,
+    RANGER
+  }
+
+  /**
+   * Utility to help categorize IAM policy resources, whether for bucket, key, 
wildcards, etc.
+   */
+  private static final class ResourceSpec {
+    // TODO implement in future PR
+  }
+
+  /**
+   * Represents S3 actions and requisite permissions required and at what 
level.
+   */
+  private enum S3Action {
+    // TODO implement in future PR
+  }
+}
diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/iam/package-info.java
 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/iam/package-info.java
new file mode 100644
index 00000000000..dee8fe2ea0c
--- /dev/null
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/iam/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Classes related to ozone REST interface.
+ */
+package org.apache.hadoop.ozone.security.acl.iam;
diff --git 
a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/security/acl/iam/TestIamSessionPolicyResolver.java
 
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/security/acl/iam/TestIamSessionPolicyResolver.java
new file mode 100644
index 00000000000..d37f441a51e
--- /dev/null
+++ 
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/security/acl/iam/TestIamSessionPolicyResolver.java
@@ -0,0 +1,313 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.security.acl.iam;
+
+import static 
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_REQUEST;
+import static 
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.NOT_SUPPORTED_OPERATION;
+import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.AuthorizerType.NATIVE;
+import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.AuthorizerType.RANGER;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test class for {@link IamSessionPolicyResolver}.
+ * */
+public class TestIamSessionPolicyResolver {
+
+  private static final String VOLUME = "s3v";
+
+  @Test
+  public void testUnsupportedConditionOperatorThrows() {
+    final String json = "{\n" +
+        "  \"Statement\": [{\n" +
+        "    \"Effect\": \"Allow\",\n" +
+        "    \"Action\": \"s3:ListBucket\",\n" +
+        "    \"Resource\": \"arn:aws:s3:::b\",\n" +
+        "    \"Condition\": { \"StringLike\": { \"s3:prefix\": \"x/*\" } }\n" +
+        "  }]\n" +
+        "}";
+
+    expectResolveThrowsForBothAuthorizers(
+        json, "Unsupported Condition operator - StringLike", 
NOT_SUPPORTED_OPERATION);
+  }
+
+  @Test
+  public void testUnsupportedConditionAttributeThrows() {
+    final String json = "{\n" +
+        "  \"Statement\": [{\n" +
+        "    \"Effect\": \"Allow\",\n" +
+        "    \"Action\": \"s3:ListBucket\",\n" +
+        "    \"Resource\": \"arn:aws:s3:::b\",\n" +
+        "    \"Condition\": { \"StringEquals\": { \"aws:SourceArn\": 
\"arn:aws:s3:::d\" } }\n" +
+        "  }]\n" +
+        "}";
+
+    expectResolveThrowsForBothAuthorizers(
+        json, "Unsupported Condition key name - aws:SourceArn", 
NOT_SUPPORTED_OPERATION);
+  }
+
+  @Test
+  public void testUnsupportedEffectThrows() {
+    final String json = "{\n" +
+        "  \"Statement\": [{\n" +
+        "    \"Effect\": \"Deny\",\n" +                       // unsupported 
effect
+        "    \"Action\": \"s3:ListBucket\",\n" +
+        "    \"Resource\": \"arn:aws:s3:::proj-*\"\n" +
+        "  }]\n" +
+        "}";
+
+    expectResolveThrowsForBothAuthorizers(
+        json, "Unsupported Effect - Deny", NOT_SUPPORTED_OPERATION);
+  }
+
+  @Test
+  public void testInvalidJsonWithoutStatementThrows() {
+    final String json = "{\n" +
+        "  \"RandomAttribute\": [{\n" +
+        "    \"Effect\": \"Allow\",\n" +
+        "    \"Action\": \"s3:ListBucket\",\n" +
+        "    \"Resource\": \"arn:aws:s3:::b\",\n" +
+        "    \"Condition\": { \"StringEquals\": { \"s3:prefix\": \"x/*\" } 
}\n" +
+        "  }]\n" +
+        "}";
+
+    expectResolveThrowsForBothAuthorizers(
+        json, "Invalid policy JSON - missing Statement", INVALID_REQUEST);
+  }
+
+  @Test
+  public void testInvalidEffectThrows() {
+    final String json = "{\n" +
+        "  \"Statement\": [{\n" +
+        "    \"Effect\": [\"Allow\"],\n" +
+        "    \"Action\": \"s3:ListBucket\",\n" +
+        "    \"Resource\": \"arn:aws:s3:::bucket1\"\n" +
+        "  }]\n" +
+        "}";
+
+    expectResolveThrowsForBothAuthorizers(
+        json, "Invalid Effect in JSON policy (must be a String) - [\"Allow\"]",
+        INVALID_REQUEST);
+  }
+
+  @Test
+  public void testMissingEffectInStatementThrows() {
+    final String json = "{\n" +
+        "  \"Statement\": [{\n" +
+        "    \"Action\": \"s3:ListBucket\",\n" +
+        "    \"Resource\": \"arn:aws:s3:::bucket1\"\n" +
+        "  }]\n" +
+        "}";
+
+    expectResolveThrowsForBothAuthorizers(
+        json, "Effect is missing from JSON policy", INVALID_REQUEST);
+  }
+
+  @Test
+  public void testInvalidNumberOfConditionsThrows() {
+    final String json = "{\n" +
+        "  \"Statement\": [\n" +
+        "    {\n" +
+        "      \"Effect\": \"Allow\",\n" +
+        "      \"Action\": \"s3:ListBucket\",\n" +
+        "      \"Resource\": \"arn:aws:s3:::b\",\n" +
+        "      \"Condition\": [\n" +
+        "        {\n" +
+        "          \"StringEquals\": {\n" +
+        "            \"aws:SourceArn\": \"arn:aws:s3:::d\"\n" +
+        "          }\n" +
+        "        },\n" +
+        "        {\n" +
+        "          \"StringEquals\": {\n" +
+        "            \"aws:SourceArn\": \"arn:aws:s3:::e\"\n" +
+        "          }\n" +
+        "        }\n" +
+        "      ]\n" +
+        "    }\n" +
+        "  ]\n" +
+        "}";
+
+    expectResolveThrowsForBothAuthorizers(
+        json, "Only one Condition is supported", NOT_SUPPORTED_OPERATION);
+  }
+
+  @Test
+  public void testInvalidConditionThrows() {
+    final String json = "{\n" +
+        "  \"Statement\": [\n" +
+        "    {\n" +
+        "      \"Effect\": \"Allow\",\n" +
+        "      \"Action\": \"s3:ListBucket\",\n" +
+        "      \"Resource\": \"arn:aws:s3:::b\",\n" +
+        "      \"Condition\": [\"RandomCondition\"]\n" +
+        "    }\n" +
+        "  ]\n" +
+        "}";
+
+    expectResolveThrowsForBothAuthorizers(
+        json, "Invalid Condition (must have operator StringEquals and key name 
" +
+        "s3:prefix) - [\"RandomCondition\"]", INVALID_REQUEST);
+  }
+
+  @Test
+  public void testInvalidConditionAttributeMissingStringEqualsThrows() {
+    final String json = "{\n" +
+        "  \"Statement\": [{\n" +
+        "    \"Effect\": \"Allow\",\n" +
+        "    \"Action\": \"s3:ListBucket\",\n" +
+        "    \"Resource\": \"arn:aws:s3:::b\",\n" +
+        "    \"Condition\": { \"StringEquals\": null }\n" +
+        "  }]\n" +
+        "}";
+
+    expectResolveThrowsForBothAuthorizers(
+        json, "Missing Condition operator - StringEquals", INVALID_REQUEST);
+  }
+
+  @Test
+  public void testInvalidConditionAttributeStructureThrows() {
+    final String json = "{\n" +
+        "  \"Statement\": [{\n" +
+        "    \"Effect\": \"Allow\",\n" +
+        "    \"Action\": \"s3:ListBucket\",\n" +
+        "    \"Resource\": \"arn:aws:s3:::b\",\n" +
+        "    \"Condition\": { \"StringEquals\": [{ \"s3:prefix\": \"folder/\" 
}] }\n" +
+        "  }]\n" +
+        "}";
+
+    expectResolveThrowsForBothAuthorizers(
+        json, "Invalid Condition operator value structure - 
[{\"s3:prefix\":\"folder/\"}]",
+        INVALID_REQUEST);
+  }
+
+  @Test
+  public void testInvalidJsonThrows() {
+    final String invalidJson = "{[{{}]\"\"";
+
+    expectResolveThrowsForBothAuthorizers(
+        invalidJson, "Invalid policy JSON (most likely JSON structure is 
incorrect)",
+        INVALID_REQUEST);
+  }
+
+  @Test
+  public void testJsonExceedsMaxLengthThrows() {
+    final String json = createJsonStringLargerThan2048Characters();
+
+    expectResolveThrowsForBothAuthorizers(
+        json, "Invalid policy JSON - exceeds maximum length of 2048 
characters",
+        INVALID_REQUEST);
+  }
+
+  @Test
+  public void testJsonAtMaxLengthSucceeds() throws OMException {
+    // Create a JSON string that is exactly 2048 characters
+    final String json = create2048CharJsonString();
+    assertThat(json.length()).isEqualTo(2048);
+
+    // Must not throw an exception
+    IamSessionPolicyResolver.resolve(json, VOLUME, NATIVE);
+    IamSessionPolicyResolver.resolve(json, VOLUME, RANGER);
+  }
+
+  @Test
+  public void testConditionKeyMustBeCaseInsensitive() throws OMException {
+    final String json = "{\n" +
+        "  \"Statement\": [{\n" +
+        "    \"Effect\": \"Allow\",\n" +
+        "    \"Action\": \"s3:ListBucket\",\n" +
+        "    \"Resource\": \"arn:aws:s3:::b\",\n" +
+        "    \"Condition\": { \"StringEquals\": { \"S3:PRefiX\": \"x/*\" } 
}\n" +
+        "  }]\n" +
+        "}";
+
+    // Must not throw exception
+    IamSessionPolicyResolver.resolve(json, VOLUME, NATIVE);
+    IamSessionPolicyResolver.resolve(json, VOLUME, RANGER);
+  }
+
+  @Test
+  public void testEffectMustBeCaseSensitive() {
+    final String json = "{\n" +
+        "  \"Statement\": [{\n" +
+        "    \"Effect\": \"aLLOw\",\n" +
+        "    \"Action\": \"s3:ListBucket\",\n" +
+        "    \"Resource\": \"arn:aws:s3:::b\",\n" +
+        "    \"Condition\": { \"StringEquals\": { \"s3:prefix\": \"x/*\" } 
}\n" +
+        "  }]\n" +
+        "}";
+
+    expectResolveThrowsForBothAuthorizers(
+        json, "Unsupported Effect - aLLOw", NOT_SUPPORTED_OPERATION);
+  }
+
+  private static void expectResolveThrows(String json,
+      IamSessionPolicyResolver.AuthorizerType authorizerType, String 
expectedMessage,
+      OMException.ResultCodes expectedCode) {
+    try {
+      IamSessionPolicyResolver.resolve(json, VOLUME, authorizerType);
+      throw new AssertionError("Expected exception not thrown");
+    } catch (OMException ex) {
+      assertThat(ex.getMessage()).isEqualTo(expectedMessage);
+      assertThat(ex.getResult()).isEqualTo(expectedCode);
+    }
+  }
+  
+  private static void expectResolveThrowsForBothAuthorizers(String json,
+      String expectedMessage, OMException.ResultCodes expectedCode) {
+    expectResolveThrows(json, NATIVE, expectedMessage, expectedCode);
+    expectResolveThrows(json, RANGER, expectedMessage, expectedCode);
+  }
+
+  private static String createJsonStringLargerThan2048Characters() {
+    final StringBuilder jsonBuilder = new StringBuilder();
+    jsonBuilder.append("{\n");
+    jsonBuilder.append("  \"Statement\": [{\n");
+    jsonBuilder.append("    \"Effect\": \"Allow\",\n");
+    jsonBuilder.append("    \"Action\": \"s3:ListBucket\",\n");
+    jsonBuilder.append("    \"Resource\": \"arn:aws:s3:::");
+    // Add enough characters to exceed 2048
+    while (jsonBuilder.length() < 2048) {
+      jsonBuilder.append("very-long-bucket-name-that-exceeds-the-limit-");
+    }
+    jsonBuilder.append("\"\n");
+    jsonBuilder.append("  }]\n");
+    jsonBuilder.append('}');
+
+    return jsonBuilder.toString();
+  }
+
+  private static String create2048CharJsonString() {
+    final StringBuilder jsonBuilder = new StringBuilder();
+    jsonBuilder.append("{\n");
+    jsonBuilder.append("  \"Statement\": [{\n");
+    jsonBuilder.append("    \"Effect\": \"Allow\",\n");
+    jsonBuilder.append("    \"Action\": \"s3:ListBucket\",\n");
+    jsonBuilder.append("    \"Resource\": \"arn:aws:s3:::");
+    // Add characters to reach exactly 2048 (accounting for closing brackets 
and newlines)
+    // Closing part: "\"\n  }]\n}" = 8 chars
+    while (jsonBuilder.length() < 2048 - 8) {
+      jsonBuilder.append('a');
+    }
+    jsonBuilder.append("\"\n  }]\n}");
+
+    return jsonBuilder.toString();
+  }
+}
+


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to