| // 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 com.cloud.bridge.util; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import org.apache.log4j.Logger; | |
| import org.json.simple.parser.ContentHandler; | |
| import org.json.simple.parser.JSONParser; | |
| import org.json.simple.parser.ParseException; | |
| import com.cloud.bridge.service.core.s3.S3BucketPolicy; | |
| import com.cloud.bridge.service.core.s3.S3ConditionFactory; | |
| import com.cloud.bridge.service.core.s3.S3PolicyAction; | |
| import com.cloud.bridge.service.core.s3.S3PolicyCondition; | |
| import com.cloud.bridge.service.core.s3.S3PolicyConditionBlock; | |
| import com.cloud.bridge.service.core.s3.S3PolicyPrincipal; | |
| import com.cloud.bridge.service.core.s3.S3PolicyStatement; | |
| import com.cloud.bridge.service.core.s3.S3BucketPolicy.PolicyAccess; | |
| import com.cloud.bridge.service.core.s3.S3PolicyAction.PolicyActions; | |
| import com.cloud.bridge.service.core.s3.S3PolicyCondition.ConditionKeys; | |
| import com.cloud.bridge.service.exception.PermissionDeniedException; | |
| /** | |
| * This class uses the JSON simple parser to convert the JSON of a Bucket Policy | |
| * into internal objects. | |
| * | |
| * Another way to implement this by use of a stack to keep track of where the current | |
| * parsing is being done. However, since we are only handling a limited JSON sequence | |
| * here simple counts and flags will do the same as a stack. | |
| */ | |
| public class PolicyParser { | |
| protected final static Logger logger = Logger.getLogger(PolicyParser.class); | |
| private S3BucketPolicy bucketPolicy = null; | |
| private S3PolicyPrincipal principals = null; | |
| private S3PolicyStatement statement = null; | |
| private S3PolicyAction actions = null; | |
| private S3PolicyAction convertActions = new S3PolicyAction(); | |
| private S3PolicyCondition condition = null; | |
| private S3ConditionFactory condFactory = null; | |
| private S3PolicyConditionBlock block = null; | |
| private PolicyActions notAction = PolicyActions.UnknownAction; | |
| private String id = null; | |
| private String sid = null; | |
| private String effect = null; | |
| private String resource = null; | |
| private String condKey = null; // -> the next key in a condition | |
| private String toUser = null; // -> text to user of a problem | |
| private List<String> valueList = new ArrayList<String>(); | |
| private JSONParser jparser = null; | |
| private int entryNesting = 0; // -> startObjectEntry() .. nesting count | |
| private int condNested = 0; // -> at what level of nesting is the condition defined | |
| private int keyNested = 0; // -> at what level of nesting is the condition key defined | |
| private boolean inId = false; // -> currently in an "Id" element | |
| private boolean inSid = false; | |
| private boolean inAWS = false; | |
| private boolean inEffect = false; | |
| private boolean inResource = false; | |
| private boolean inNotAction = false; | |
| private boolean inVersion = false; | |
| private boolean inStatement = false; | |
| public PolicyParser() | |
| { | |
| jparser = new JSONParser(); | |
| condFactory = new S3ConditionFactory(); | |
| } | |
| ContentHandler myHandler = new ContentHandler() | |
| { | |
| public boolean endArray() throws ParseException | |
| { | |
| logger.debug( "endArray()" ); | |
| return true; | |
| } | |
| public void endJSON() throws ParseException, PermissionDeniedException | |
| { | |
| logger.debug( "endJSON()" ); | |
| if (null != statement) | |
| { | |
| //System.out.println( "endJSON() - statement"); | |
| if (null != block) { | |
| block.verify(); | |
| statement.setConditionBlock( block ); | |
| } | |
| if (null != bucketPolicy) { | |
| statement.verify(); | |
| bucketPolicy.addStatement( statement ); | |
| } | |
| statement = null; | |
| block = null; | |
| } | |
| } | |
| public boolean endObject() throws ParseException, PermissionDeniedException | |
| { | |
| logger.debug( "endObject(), nesting: " + entryNesting ); | |
| if (null != statement && 1 >= entryNesting) | |
| { | |
| //System.out.println( "endObject() - statement"); | |
| if (null != block) { | |
| block.verify(); | |
| statement.setConditionBlock( block ); | |
| } | |
| if (null != bucketPolicy) { | |
| statement.verify(); | |
| bucketPolicy.addStatement( statement ); | |
| } | |
| statement = null; | |
| block = null; | |
| } | |
| if (0 == entryNesting) inStatement = false; | |
| return true; | |
| } | |
| public boolean endObjectEntry() throws ParseException, PermissionDeniedException | |
| { | |
| logger.debug( "endObjectEntry(), nesting: " + entryNesting ); | |
| if (inSid) { | |
| if (null != statement) statement.setSid( sid ); | |
| inSid = false; | |
| } | |
| else if (inEffect) | |
| { | |
| if (null != statement) | |
| { | |
| if (effect.equalsIgnoreCase("Allow")) statement.setEffect( PolicyAccess.ALLOW ); | |
| else if (effect.equalsIgnoreCase("Deny" )) statement.setEffect( PolicyAccess.DENY ); | |
| else badPolicy( "Effect", effect ); | |
| } | |
| inEffect = false; | |
| } | |
| else if (inResource) | |
| { | |
| if (null != statement && resource.startsWith("arn:aws:s3:::")) | |
| { | |
| String resourcePath = resource.substring(13); | |
| verifySameBucket( resourcePath ); | |
| statement.setResource( resourcePath ); | |
| } | |
| inResource = false; | |
| } | |
| else if (inNotAction) | |
| { | |
| if (null != statement) statement.setNotAction( notAction ); | |
| inNotAction = false; | |
| } | |
| else if (inVersion) | |
| { | |
| inVersion = false; | |
| } | |
| else if (inId) | |
| { | |
| if (null != bucketPolicy) bucketPolicy.setId( id ); | |
| inId = false; | |
| } | |
| else if (null != actions) | |
| { | |
| if (null != statement) statement.setActions( actions ); | |
| actions = null; | |
| } | |
| else if (null != principals) | |
| { | |
| if (inAWS && null != statement) statement.setPrincipals( principals ); | |
| principals = null; | |
| } | |
| else if (null != condition) | |
| { | |
| //System.out.println( "in condition: " + condNested + " " + entryNesting + " " + keyNested ); | |
| // -> is it just the current key that is done? | |
| try { | |
| if (keyNested == entryNesting) | |
| { | |
| String[] values = valueList.toArray(new String[0]); | |
| ConditionKeys tempKey = S3PolicyCondition.toConditionKeys( condKey ); | |
| if (ConditionKeys.UnknownKey == tempKey) badPolicy( "Condition Key", condKey ); | |
| condition.setKey( tempKey, values ); | |
| valueList.clear(); | |
| condKey = null; | |
| } | |
| } | |
| catch( ParseException e ) { | |
| logger.error("Policy Parser condition error: ", e); | |
| throw e; | |
| } | |
| catch( Exception e) { | |
| logger.error("Policy Parser condition error: ", e); | |
| badPolicy("Condition Key (" + condKey + ")", e.toString()); | |
| } | |
| // -> is the condition completely done? | |
| if (condNested == entryNesting) { | |
| condition.verify(); | |
| block.addCondition( condition ); | |
| condition = null; | |
| } | |
| } | |
| else if (null != statement && 1 == entryNesting) | |
| { | |
| if (null != block) { | |
| block.verify(); | |
| statement.setConditionBlock( block ); | |
| } | |
| if (null != bucketPolicy) { | |
| statement.verify(); | |
| bucketPolicy.addStatement( statement ); | |
| } | |
| statement = null; | |
| block = null; | |
| } | |
| entryNesting--; | |
| return true; | |
| } | |
| public boolean primitive(Object value) throws ParseException, PermissionDeniedException | |
| { | |
| logger.debug( "primitive(): " + value ); | |
| if (inSid) { | |
| sid = (String)value; | |
| } | |
| else if (inEffect) { | |
| effect = (String)value; | |
| } | |
| else if (inResource) { | |
| resource = (String)value; | |
| } | |
| else if (inNotAction) { | |
| notAction = convertActions.toPolicyActions((String)value); | |
| if (notAction == PolicyActions.UnknownAction) badPolicy( "NotAction", (String)value ); | |
| } | |
| else if (inId) { | |
| id = (String)value; | |
| } | |
| else if (null != actions) { | |
| PolicyActions tempAction = convertActions.toPolicyActions((String)value); | |
| if (tempAction == PolicyActions.UnknownAction) badPolicy( "Action", (String)value ); | |
| actions.addAction( tempAction ); | |
| } | |
| else if (null != principals) { | |
| principals.addPrincipal( (String)value ); | |
| } | |
| else if (null != condition) { | |
| // -> a condition key can have one or more values | |
| valueList.add( (String)value ); | |
| } | |
| else if (inVersion) { | |
| String version = (String)value; | |
| if (!version.equals( "2008-10-17" )) badPolicy( "Version", (String)value ); | |
| } | |
| return true; | |
| } | |
| public boolean startArray() throws ParseException | |
| { | |
| logger.debug( "startArray()" ); | |
| return true; | |
| } | |
| public void startJSON() throws ParseException | |
| { | |
| logger.debug( "startJSON()" ); | |
| } | |
| public boolean startObject() throws ParseException | |
| { | |
| logger.debug( "startObject(), nesting: " + entryNesting ); | |
| if (1 == entryNesting && inStatement) statement = new S3PolicyStatement(); | |
| return true; | |
| } | |
| /** | |
| * Note: A statement does not have to have a condition block to be valid. | |
| */ | |
| public boolean startObjectEntry(String key) throws ParseException | |
| { | |
| entryNesting++; | |
| logger.debug( "startObjectEntry(), key: [" + key + "]" ); | |
| inSid = false; inAWS = false; inEffect = false; inResource = false; | |
| inNotAction = false; inVersion = false; inId = false; | |
| if (key.equalsIgnoreCase( "Statement" )) inStatement = true; | |
| else if (key.equalsIgnoreCase( "Action" )) actions = new S3PolicyAction(); | |
| else if (key.equalsIgnoreCase( "Principal" )) principals = new S3PolicyPrincipal(); | |
| else if (key.equalsIgnoreCase( "Condition" )) block = new S3PolicyConditionBlock(); | |
| else if (key.equalsIgnoreCase( "AWS" ) && null != principals) inAWS = true; | |
| else if (key.equalsIgnoreCase( "CanonicalUser" ) && null != principals) inAWS = true; | |
| else if (key.equalsIgnoreCase( "Sid" )) inSid = true; | |
| else if (key.equalsIgnoreCase( "Effect" )) inEffect = true; | |
| else if (key.equalsIgnoreCase( "Resource" )) inResource = true; | |
| else if (key.equalsIgnoreCase( "NotAction" )) inNotAction = true; | |
| else if (key.equalsIgnoreCase( "Version" )) inVersion = true; | |
| else if (key.equalsIgnoreCase( "Id" )) inId = true; | |
| else if (null != condition) { | |
| condKey = key; | |
| keyNested = entryNesting; | |
| } | |
| else if (null != block) { | |
| condition = condFactory.createCondition( key ); | |
| condNested = entryNesting; | |
| if (null == condition) badPolicy( "Condition type", key ); | |
| } | |
| else logger.debug( "startObjectEntry() no match" ); | |
| return true; | |
| } | |
| }; | |
| public S3BucketPolicy parse( String policy, String bucketName ) throws ParseException, PermissionDeniedException | |
| { | |
| bucketPolicy = new S3BucketPolicy(); | |
| bucketPolicy.setBucketName( bucketName ); | |
| jparser.parse(policy, myHandler); | |
| return bucketPolicy; | |
| } | |
| /** | |
| * From Amazon on S3 Policies: | |
| * "Each policy must cover only a single bucket and resources within that bucket (when writing a | |
| * policy, don't include statements that refer to other buckets or resources in other buckets)" | |
| * | |
| * @param resourcePath | |
| */ | |
| private void verifySameBucket( String resourcePath ) throws PermissionDeniedException | |
| { | |
| String testBucketName = resourcePath; | |
| String bucketName = bucketPolicy.getBucketName(); | |
| // -> extract just the bucket name | |
| int offset = testBucketName.indexOf( "/" ); | |
| if (-1 != offset) testBucketName = testBucketName.substring( 0, offset ); | |
| if (!testBucketName.equals( bucketName )) | |
| throw new PermissionDeniedException( "The S3 Bucket Policy must only refer to the single bucket: \"" + bucketName + | |
| "\", but it referres to the following resource: \"" + resourcePath + "\"" ); | |
| } | |
| public static void badPolicy( String place, String badValue ) throws ParseException | |
| { | |
| String toUser = new String( "S3 Bucket Policy " + place + " of: \"" + badValue + "\" is unknown" ); | |
| throw new ParseException( ParseException.ERROR_UNEXPECTED_TOKEN, toUser ); | |
| } | |
| } |