/* * Copyright 2011 Cloud.com, 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 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 ); } }