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]