io.minio.policy.BucketPolicy.java Source code

Java tutorial

Introduction

Here is the source code for io.minio.policy.BucketPolicy.java

Source

/*
 * Minio Java Library for Amazon S3 Compatible Cloud Storage, (C) 2016 Minio, Inc.
 *
 * Licensed 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 io.minio.policy;

import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.JsonProcessingException;

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class BucketPolicy {
    @JsonIgnore
    private String bucketName;

    @JsonProperty("Version")
    private String version;

    @JsonProperty("Statement")
    private List<Statement> statements;

    @JsonIgnore
    private static final ObjectMapper objectMapper = new ObjectMapper()
            .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
            .setSerializationInclusion(Include.NON_NULL);

    public BucketPolicy() {
    }

    public BucketPolicy(String bucketName) {
        this.bucketName = bucketName;
        this.version = "2012-10-17";
    }

    public List<Statement> statements() {
        return statements;
    }

    /**
     * Reads JSON from given {@link Reader} and returns new {@link BucketPolicy} of given bucket name.
     */
    public static BucketPolicy parseJson(Reader reader, String bucketName) throws IOException {
        BucketPolicy bucketPolicy = objectMapper.readValue(reader, BucketPolicy.class);
        bucketPolicy.bucketName = bucketName;

        return bucketPolicy;
    }

    /**
     * Generates JSON of this BucketPolicy object.
     */
    @JsonIgnore
    public String getJson() throws JsonProcessingException {
        return objectMapper.writeValueAsString(this);
    }

    /**
     * Returns new bucket statements for given policy type.
     */
    private List<Statement> newBucketStatement(PolicyType policy, String prefix) {
        List<Statement> statements = new ArrayList<Statement>();

        if (policy == PolicyType.NONE || bucketName == null || bucketName.isEmpty()) {
            return statements;
        }

        Resources resources = new Resources(Constants.AWS_RESOURCE_PREFIX + bucketName);

        Statement statement = new Statement();
        statement.setActions(Constants.COMMON_BUCKET_ACTIONS);
        statement.setEffect("Allow");
        statement.setPrincipal(new Principal("*"));
        statement.setResources(resources);
        statement.setSid("");

        statements.add(statement);

        if (policy == PolicyType.READ_ONLY || policy == PolicyType.READ_WRITE) {
            statement = new Statement();
            statement.setActions(Constants.READ_ONLY_BUCKET_ACTIONS);
            statement.setEffect("Allow");
            statement.setPrincipal(new Principal("*"));
            statement.setResources(resources);
            statement.setSid("");

            if (prefix != null && !prefix.isEmpty()) {
                statement.setConditions(new ConditionMap("StringEquals", new ConditionKeyMap("s3:prefix", prefix)));
            }

            statements.add(statement);
        }

        if (policy == PolicyType.WRITE_ONLY || policy == PolicyType.READ_WRITE) {
            statement = new Statement();
            statement.setActions(Constants.WRITE_ONLY_BUCKET_ACTIONS);
            statement.setEffect("Allow");
            statement.setPrincipal(new Principal("*"));
            statement.setResources(resources);
            statement.setSid("");

            statements.add(statement);
        }

        return statements;
    }

    /**
     * Returns new object statements for given policy type.
     */
    private List<Statement> newObjectStatement(PolicyType policy, String prefix) {
        List<Statement> statements = new ArrayList<Statement>();

        if (policy == PolicyType.NONE || bucketName == null || bucketName.isEmpty()) {
            return statements;
        }

        Resources resources = new Resources(Constants.AWS_RESOURCE_PREFIX + bucketName + "/" + prefix + "*");

        Statement statement = new Statement();
        statement.setEffect("Allow");
        statement.setPrincipal(new Principal("*"));
        statement.setResources(resources);
        statement.setSid("");

        if (policy == PolicyType.READ_ONLY) {
            statement.setActions(Constants.READ_ONLY_OBJECT_ACTIONS);
        } else if (policy == PolicyType.WRITE_ONLY) {
            statement.setActions(Constants.WRITE_ONLY_OBJECT_ACTIONS);
        } else if (policy == PolicyType.READ_WRITE) {
            statement.setActions(Constants.READ_WRITE_OBJECT_ACTIONS);
        }

        statements.add(statement);
        return statements;
    }

    /**
     * Returns new statements for given policy type.
     */
    private List<Statement> newStatements(PolicyType policy, String prefix) {
        List<Statement> statements = this.newBucketStatement(policy, prefix);
        List<Statement> objectStatements = this.newObjectStatement(policy, prefix);

        statements.addAll(objectStatements);

        return statements;
    }

    /**
     * Returns whether statements are used by other than given prefix statements.
     */
    @JsonIgnore
    private boolean[] getInUsePolicy(String prefix) {
        String resourcePrefix = Constants.AWS_RESOURCE_PREFIX + bucketName + "/";
        String objectResource = Constants.AWS_RESOURCE_PREFIX + bucketName + "/" + prefix + "*";

        boolean readOnlyInUse = false;
        boolean writeOnlyInUse = false;

        for (Statement statement : statements) {
            if (!statement.resources().contains(objectResource)
                    && !statement.resources().startsWith(resourcePrefix).isEmpty()) {

                if (statement.actions().containsAll(Constants.READ_ONLY_OBJECT_ACTIONS)) {
                    readOnlyInUse = true;
                }

                if (statement.actions().containsAll(Constants.WRITE_ONLY_OBJECT_ACTIONS)) {
                    writeOnlyInUse = true;
                }
            }

            if (readOnlyInUse && writeOnlyInUse) {
                break;
            }
        }

        boolean[] rv = { readOnlyInUse, writeOnlyInUse };
        return rv;
    }

    /**
     * Returns all statements of given prefix.
     */
    private void removeStatements(String prefix) {
        String bucketResource = Constants.AWS_RESOURCE_PREFIX + bucketName;
        String objectResource = Constants.AWS_RESOURCE_PREFIX + bucketName + "/" + prefix + "*";
        boolean[] inUse = getInUsePolicy(prefix);
        boolean readOnlyInUse = inUse[0];
        boolean writeOnlyInUse = inUse[1];

        List<Statement> out = new ArrayList<Statement>();
        Set<String> s3PrefixValues = new HashSet<String>();
        List<Statement> readOnlyBucketStatements = new ArrayList<Statement>();

        for (Statement statement : statements) {
            if (!statement.isValid(bucketName)) {
                out.add(statement);
                continue;
            }

            if (statement.resources().contains(bucketResource)) {
                if (statement.conditions() != null) {
                    statement.removeBucketActions(prefix, bucketResource, false, false);
                } else {
                    statement.removeBucketActions(prefix, bucketResource, readOnlyInUse, writeOnlyInUse);
                }
            } else if (statement.resources().contains(objectResource)) {
                statement.removeObjectActions(objectResource);
            }

            if (!statement.actions().isEmpty()) {
                if (statement.resources().contains(bucketResource)
                        && statement.actions().containsAll(Constants.READ_ONLY_BUCKET_ACTIONS)
                        && statement.effect().equals("Allow") && statement.principal().aws().contains("*")) {

                    if (statement.conditions() != null) {
                        ConditionKeyMap stringEqualsValue = statement.conditions().get("StringEquals");
                        if (stringEqualsValue != null) {
                            Set<String> values = stringEqualsValue.get("s3:prefix");
                            if (values != null) {
                                for (String v : values) {
                                    s3PrefixValues.add(bucketResource + "/" + v + "*");
                                }
                            }
                        }
                    } else if (!s3PrefixValues.isEmpty()) {
                        readOnlyBucketStatements.add(statement);
                        continue;
                    }
                }

                out.add(statement);
            }
        }

        boolean skipBucketStatement = true;
        String resourcePrefix = Constants.AWS_RESOURCE_PREFIX + bucketName + "/";
        for (Statement statement : out) {
            Set<String> intersection = new HashSet<String>(s3PrefixValues);
            intersection.retainAll(statement.resources());

            if (!statement.resources().startsWith(resourcePrefix).isEmpty() && intersection.isEmpty()) {
                skipBucketStatement = false;
                break;
            }
        }

        for (Statement statement : readOnlyBucketStatements) {
            Set<String> aws = statement.principal().aws();
            if (skipBucketStatement && statement.resources().contains(bucketResource)
                    && statement.effect().equals("Allow") && aws != null && aws.contains("*")
                    && statement.conditions() == null) {
                continue;
            }

            out.add(statement);
        }

        if (out.size() == 1) {
            Statement statement = out.get(0);
            Set<String> aws = statement.principal().aws();
            if (statement.resources().contains(bucketResource)
                    && statement.actions().containsAll(Constants.COMMON_BUCKET_ACTIONS)
                    && statement.effect().equals("Allow") && aws != null && aws.contains("*")
                    && statement.conditions() == null) {
                out = new ArrayList<Statement>();
            }
        }

        statements = out;
    }

    /**
     * Appends given statement into statement list to have unique statements.
     * - If statement already exists in statement list, it ignores.
     * - If statement exists with different conditions, they are merged.
     * - Else the statement is appended to statement list.
     */
    private void appendStatement(Statement statement) {
        for (Statement s : statements) {
            Set<String> aws = s.principal().aws();
            ConditionMap conditions = s.conditions();

            if (s.actions().containsAll(statement.actions()) && s.effect().equals(statement.effect()) && aws != null
                    && aws.containsAll(statement.principal().aws()) && conditions != null
                    && conditions.equals(statement.conditions())) {
                s.resources().addAll(statement.resources());
                return;
            }

            if (s.resources().containsAll(statement.resources()) && s.effect().equals(statement.effect())
                    && aws != null && aws.containsAll(statement.principal().aws()) && conditions != null
                    && conditions.equals(statement.conditions())) {
                s.actions().addAll(statement.actions());
                return;
            }

            if (s.resources().containsAll(statement.resources()) && s.actions().containsAll(statement.actions())
                    && s.effect().equals(statement.effect()) && aws != null
                    && aws.containsAll(statement.principal().aws())) {
                if (conditions != null && conditions.equals(statement.conditions())) {
                    return;
                }

                if (conditions != null && statement.conditions() != null) {
                    conditions.putAll(statement.conditions());
                    return;
                }
            }
        }

        if (!(statement.actions().isEmpty() && statement.resources().isEmpty())) {
            statements.add(statement);
        }
    }

    /**
     * Appends new statements for given policy type.
     */
    private void appendStatements(PolicyType policy, String prefix) {
        List<Statement> appendStatements = newStatements(policy, prefix);
        for (Statement statement : appendStatements) {
            appendStatement(statement);
        }
    }

    /**
     * Returns policy type of this bucket policy.
     */
    @JsonIgnore
    public PolicyType getPolicy(String prefix) {
        String bucketResource = Constants.AWS_RESOURCE_PREFIX + bucketName;
        String objectResource = Constants.AWS_RESOURCE_PREFIX + bucketName + "/" + prefix + "*";

        boolean bucketCommonFound = false;
        boolean bucketReadOnly = false;
        boolean bucketWriteOnly = false;
        String matchedResource = "";
        boolean objReadOnly = false;
        boolean objWriteOnly = false;

        for (Statement s : statements) {
            Set<String> matchedObjResources = new HashSet<String>();
            if (s.resources().contains(objectResource)) {
                matchedObjResources.add(objectResource);
            } else {
                matchedObjResources = s.resources().match(objectResource);
            }

            if (!matchedObjResources.isEmpty()) {
                boolean[] rv = s.getObjectPolicy();
                boolean readOnly = rv[0];
                boolean writeOnly = rv[1];

                for (String resource : matchedObjResources) {
                    if (matchedResource.length() < resource.length()) {
                        objReadOnly = readOnly;
                        objWriteOnly = writeOnly;
                        matchedResource = resource;
                    } else if (matchedResource.length() == resource.length()) {
                        objReadOnly = objReadOnly || readOnly;
                        objWriteOnly = objWriteOnly || writeOnly;
                        matchedResource = resource;
                    }
                }
            } else if (s.resources().contains(bucketResource)) {
                boolean[] rv = s.getBucketPolicy(prefix);
                boolean commonFound = rv[0];
                boolean readOnly = rv[1];
                boolean writeOnly = rv[2];
                bucketCommonFound = bucketCommonFound || commonFound;
                bucketReadOnly = bucketReadOnly || readOnly;
                bucketWriteOnly = bucketWriteOnly || writeOnly;
            }
        }

        if (bucketCommonFound) {
            if (bucketReadOnly && bucketWriteOnly && objReadOnly && objWriteOnly) {
                return PolicyType.READ_WRITE;
            } else if (bucketReadOnly && objReadOnly) {
                return PolicyType.READ_ONLY;
            } else if (bucketWriteOnly && objWriteOnly) {
                return PolicyType.WRITE_ONLY;
            }
        }

        return PolicyType.NONE;
    }

    /**
     * Returns policy type of all prefixes.
     */
    @JsonIgnore
    public Map<String, PolicyType> getPolicies() {
        Map<String, PolicyType> policyRules = new Hashtable<String, PolicyType>();
        Set<String> objResources = new HashSet<String>();

        String bucketResource = Constants.AWS_RESOURCE_PREFIX + bucketName;

        // Search all resources related to objects policy
        for (Statement s : statements) {
            objResources.addAll(s.resources().startsWith(bucketResource + "/"));
        }

        // Pretend that policy resource as an actual object and fetch its policy
        for (String r : objResources) {
            // Put trailing * if exists in asterisk
            String asterisk = "";
            if (r.endsWith("*")) {
                r = r.substring(0, r.length() - 1);
                asterisk = "*";
            }

            String objectPath = r.substring(bucketResource.length() + 1, r.length());
            PolicyType policy = this.getPolicy(objectPath);
            policyRules.put(bucketName + "/" + objectPath + asterisk, policy);
        }

        return policyRules;
    }

    /**
     * Sets policy type for given prefix.
     */
    @JsonIgnore
    public void setPolicy(PolicyType policy, String prefix) {
        if (statements == null) {
            statements = new ArrayList<Statement>();
        }

        removeStatements(prefix);
        appendStatements(policy, prefix);
    }
}