/*************************************************************************
* Copyright 2009-2015 Eucalyptus Systems, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
* Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta
* CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need
* additional information or have any questions.
*
* This file may incorporate work covered under the following copyright
* and permission notice:
*
* Software License Agreement (BSD License)
*
* Copyright (c) 2008, Regents of the University of California
* All rights reserved.
*
* Redistribution and use of this software in source and binary forms,
* with or without modification, are permitted provided that the
* following conditions are met:
*
* Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE. USERS OF THIS SOFTWARE ACKNOWLEDGE
* THE POSSIBLE PRESENCE OF OTHER OPEN SOURCE LICENSED MATERIAL,
* COPYRIGHTED MATERIAL OR PATENTED MATERIAL IN THIS SOFTWARE,
* AND IF ANY SUCH MATERIAL IS DISCOVERED THE PARTY DISCOVERING
* IT MAY INFORM DR. RICH WOLSKI AT THE UNIVERSITY OF CALIFORNIA,
* SANTA BARBARA WHO WILL THEN ASCERTAIN THE MOST APPROPRIATE REMEDY,
* WHICH IN THE REGENTS' DISCRETION MAY INCLUDE, WITHOUT LIMITATION,
* REPLACEMENT OF THE CODE SO IDENTIFIED, LICENSING OF THE CODE SO
* IDENTIFIED, OR WITHDRAWAL OF THE CODE CAPABILITY TO THE EXTENT
* NEEDED TO COMPLY WITH ANY SUCH LICENSES OR RIGHTS.
************************************************************************/
package com.eucalyptus.auth.policy;
import static org.hamcrest.Matchers.notNullValue;
import static com.eucalyptus.auth.principal.Principal.PrincipalType;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import net.sf.json.JSONException;
import net.sf.json.JSONObject;
import org.apache.log4j.Logger;
import com.eucalyptus.auth.AuthenticationLimitProvider;
import com.eucalyptus.auth.Debugging;
import com.eucalyptus.auth.PolicyParseException;
import com.eucalyptus.auth.json.JsonUtils;
import com.eucalyptus.auth.policy.condition.ConditionOp;
import com.eucalyptus.auth.policy.condition.Conditions;
import com.eucalyptus.auth.policy.condition.NullConditionOp;
import com.eucalyptus.auth.policy.ern.Ern;
import com.eucalyptus.auth.policy.key.Key;
import com.eucalyptus.auth.policy.key.Keys;
import com.eucalyptus.auth.policy.key.QuotaKey;
import com.eucalyptus.auth.principal.Authorization.EffectType;
import com.eucalyptus.auth.principal.Condition;
import com.eucalyptus.records.Logs;
import com.eucalyptus.util.Json;
import com.eucalyptus.util.Parameters;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonPointer;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
/**
* The IAM policy parser.
*/
public class PolicyParser {
private static final Logger LOG = Logger.getLogger( PolicyParser.class );
private static final Pattern VARIABLE_MATCHER = Pattern.compile( "\\$\\{(?:[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z]+|[*]|[?]|[$])}" );
private enum PolicyAttachmentType {
Identity( true/*requireResource*/, false/*requirePrincipal*/ ),
Resource( false/*requireResource*/, true/*requirePrincipal*/ );
private final boolean requireResource;
private final boolean requirePrincipal;
PolicyAttachmentType( final boolean requireResource,
final boolean requirePrincipal ) {
this.requireResource = requireResource;
this.requirePrincipal = requirePrincipal;
}
public boolean isResourceRequired() {
return requireResource;
}
public boolean isPrincipalRequired() {
return requirePrincipal;
}
}
private static final class PolicyParseContext {
private final String version;
private PolicyParseContext( final String version ) {
this.version = version;
}
public String getVersion() {
return version;
}
}
private final PolicyAttachmentType attachmentType;
private final boolean validating; // true for json validation
public static PolicyParser getLaxInstance( ) {
return new PolicyParser( PolicyAttachmentType.Identity, false );
}
public static PolicyParser getLaxResourceInstance( ) {
return new PolicyParser( PolicyAttachmentType.Resource, false );
}
public static PolicyParser getInstance( ) {
return new PolicyParser( PolicyAttachmentType.Identity, true );
}
public static PolicyParser getResourceInstance( ) {
return new PolicyParser( PolicyAttachmentType.Resource, true );
}
private PolicyParser(
final PolicyAttachmentType attachmentType,
final boolean validating
) {
this.attachmentType = attachmentType;
this.validating = validating;
}
/**
* Parse the input policy text and returns an PolicyEntity object that
* represents the policy internally.
*
* @param policy The input policy text.
* @return The parsed the policy entity.
* @throws PolicyParseException for policy syntax error.
*/
public PolicyPolicy parse( String policy ) throws PolicyParseException {
return parse( policy, null );
}
/**
* Parse the input policy text and returns an PolicyEntity object that
* represents the policy internally.
*
* @param policy The input policy text.
* @param minimumVersion The required (minimum) policy version
* @return The parsed the policy entity.
* @throws PolicyParseException for policy syntax error.
*/
public PolicyPolicy parse( String policy, String minimumVersion ) throws PolicyParseException {
if ( policy == null ) {
throw new PolicyParseException( PolicyParseException.EMPTY_POLICY );
}
if ( policy.length( ) > AuthenticationLimitProvider.Values.getPolicySizeLimit( ) ) {
throw new PolicyParseException( PolicyParseException.SIZE_TOO_LARGE );
}
if ( validating && AuthenticationLimitProvider.Values.getUseValidatingPolicyParser( ) ) {
try { // parser that ensures policy is valid json
Json.parseObject( policy );
} catch ( IOException e ) {
Debugging.logError( LOG, e, "Syntax error in input policy" );
throw new PolicyParseException( e.getMessage( ), e );
}
}
try {
JSONObject policyJsonObj = JSONObject.fromObject( policy );
String version = JsonUtils.getByType( String.class, policyJsonObj, PolicySpec.VERSION );
if ( minimumVersion != null && ( version == null || minimumVersion.compareTo( version ) < 0 ) ) {
throw new PolicyParseException( "Version must be at least " + minimumVersion );
}
// Policy statements
List<PolicyAuthorization> authorizations = parseStatements( new PolicyParseContext( version ), policyJsonObj );
return PolicyUtils.intern( new PolicyPolicy( version, authorizations ) );
} catch ( JSONException e ) {
Debugging.logError( LOG, e, "Syntax error in input policy" );
throw new PolicyParseException( e );
}
}
/**
* Normalize the given policy.
*
* Normalization requires a valid (JSON) policy so should NOT be performed
* for existing policies, only user input.
*
* The current implementation removes meaningless whitespace, adds a version,
* and ensure statements are a list not a single value.
*/
public String normalize( final String policy ) throws PolicyParseException {
try {
final ObjectNode jsonPolicy = Json.parseObject( policy );
final ObjectNode normalizedPolicy = jsonPolicy.objectNode( );
property( normalizedPolicy, jsonPolicy, PolicySpec.VERSION, "2008-10-17" );
propertyArray( normalizedPolicy, jsonPolicy, PolicySpec.STATEMENT );
return Json.writeObjectAsString( normalizedPolicy );
} catch ( IOException e ) {
throw new PolicyParseException( e.getMessage( ), e );
}
}
private void property(
final ObjectNode target,
final ObjectNode source,
final String name,
final String defaultValue
) {
JsonNode propertyNode = source.get( name );
if ( propertyNode == null && defaultValue != null ) {
propertyNode = target.textNode( defaultValue );
}
if ( propertyNode != null ) {
target.set( name, propertyNode );
}
}
private void propertyArray(
final ObjectNode target,
final ObjectNode source,
final String name
) {
JsonNode propertyNode = source.get( name );
if ( propertyNode != null && !propertyNode.isArray( ) ) {
ArrayNode arrayPropertyNode = target.arrayNode( );
arrayPropertyNode.add( propertyNode );
propertyNode = arrayPropertyNode;
}
if ( propertyNode != null ) {
target.set( name, propertyNode );
}
}
/**
* Parse all statements.
*
* @param policy Input policy text.
* @return A list of statement entities from the input policy.
* @throws JSONException for syntax error.
*/
private List<PolicyAuthorization> parseStatements(
final PolicyParseContext context,
final JSONObject policy
) throws JSONException {
List<JSONObject> objs;
if ( policy.get( PolicySpec.STATEMENT ) instanceof JSONObject ) {
objs = Lists.newArrayList( JsonUtils.getRequiredByType( JSONObject.class, policy, PolicySpec.STATEMENT ) );
} else {
objs = JsonUtils.getRequiredArrayByType( JSONObject.class, policy, PolicySpec.STATEMENT );
}
List<PolicyAuthorization> authorizations = Lists.newArrayList( );
for ( JSONObject o : objs ) {
authorizations.addAll( parseStatement( context, o ) );
}
return authorizations;
}
/**
* Parse one statement. A statement is internally represented by a list of authorizations
* and a list of conditions. The action list and the resource list of the statement are
* parsed into authorizations (which action is allowed on which resource). The condition
* block is translated into conditions (keys, values and their relationships).
*
* @param statement The JSON object of the statement
* @return The parsed statement entity
* @throws JSONException for syntax error
*/
private List<PolicyAuthorization> parseStatement(
final PolicyParseContext context,
final JSONObject statement
) throws JSONException {
// statement ID
String sid = JsonUtils.getByType( String.class, statement, PolicySpec.SID );
// effect
JsonUtils.checkRequired( statement, PolicySpec.EFFECT );
String effect = JsonUtils.getByType( String.class, statement, PolicySpec.EFFECT );
checkEffect( effect );
// principal
PolicyPrincipal principal = parsePrincipal( statement );
// conditions
List<PolicyCondition> conditions = parseConditions( statement, effect );
return parseAuthorizations( context, statement, sid, effect, principal, conditions );
}
/**
* Parse the principal part of a statement.
*
* @param statement The input statement in JSON object.
* @return The optional principal entity entities.
* @throws JSONException for syntax error.
*/
private PolicyPrincipal parsePrincipal(
final JSONObject statement
) {
final String principalElement =
JsonUtils.checkBinaryOption( statement, PolicySpec.PRINCIPAL, PolicySpec.NOTPRINCIPAL, attachmentType.isPrincipalRequired() );
final boolean notPrincipal = PolicySpec.NOTPRINCIPAL.equals( principalElement );
if ( PolicySpec.ALL_PRINCIPALS.equals( statement.get( principalElement ) ) ) {
return new PolicyPrincipal( notPrincipal, PrincipalType.AWS, Sets.newHashSet( PolicySpec.ALL_PRINCIPALS ) );
}
final JSONObject principal = JsonUtils.getByType( JSONObject.class, statement, principalElement );
if ( principal == null ) return null;
String principalType = null;
for ( final PrincipalType type : PrincipalType.values( ) ) {
if ( principal.containsKey( type.name( ) ) ) {
if ( principalType != null ) {
throw new JSONException( "Element " + principalType + " and " + type.name( ) + " can not be both present" );
}
principalType = type.name( );
}
}
if ( principalType == null ) {
throw new JSONException( "One of element " + ( Joiner.on( " or " ).join( PrincipalType.values( ) ) ) + " is required" );
}
final List<String> values = JsonUtils.parseStringOrStringList( principal, principalType );
if ( values.size( ) < 1 && attachmentType.isPrincipalRequired() ) {
throw new JSONException( "Empty principal values" );
}
if ( values.size( ) > 0 && !attachmentType.isPrincipalRequired() ) {
throw new JSONException( "Policy document should not specify a principal." );
}
return new PolicyPrincipal( notPrincipal, PrincipalType.valueOf( principalType ), Sets.newHashSet( values ) );
}
/**
* Parse the authorization part of a statement.
*
* @param statement The input statement in JSON object.
* @param effect The effect of the statement
* @return A list of authorization entities.
* @throws JSONException for syntax error.
*/
private List<PolicyAuthorization> parseAuthorizations(
final PolicyParseContext context,
final JSONObject statement,
final String sid,
final String effect,
final PolicyPrincipal principal,
final List<PolicyCondition> conditions ) throws JSONException {
// actions
String actionElement = JsonUtils.checkBinaryOption( statement, PolicySpec.ACTION, PolicySpec.NOTACTION );
List<String> actions = JsonUtils.parseStringOrStringList( statement, actionElement );
if ( actions.size( ) < 1 ) {
throw new JSONException( "Empty action values" );
}
// resources
String resourceElement = JsonUtils.checkBinaryOption( statement, PolicySpec.RESOURCE, PolicySpec.NOTRESOURCE, attachmentType.isResourceRequired() );
List<String> resources = JsonUtils.parseStringOrStringList( statement, resourceElement );
if ( resources.size( ) < 1 && ( attachmentType.isResourceRequired() || PolicySpec.RESOURCE.equals( resourceElement ) ) ) {
throw new JSONException( "Empty resource values" );
}
// decompose actions and resources and re-combine them into a list of authorizations
return decomposeStatement( context, effect, sid, actionElement, actions, resourceElement, resources, principal, conditions );
}
/**
* The algorithm of decomposing the actions and resources of a statement into authorizations:
* 1. Group actions into different vendors.
* 2. Group resources into different resource types.
* 3. Permute all combinations of action groups and resource groups, matching them by the same
* vendors.
*/
private List<PolicyAuthorization> decomposeStatement(
final PolicyParseContext context,
final String effect,
final String sid,
final String actionElement,
final List<String> actions,
final String resourceElement,
final List<String> resources,
final PolicyPrincipal principal,
final List<PolicyCondition> conditions
) {
// Group actions by vendor
final SetMultimap<String, String> actionMap = HashMultimap.create( );
for ( String action : actions ) {
action = normalizeString( action );
final String vendor = checkAction( action );
actionMap.put( vendor, action );
}
// Group resources by type, key is a pair of (optional) account + resource type
final SetMultimap<PolicyResourceSetKey, String> resourceMap = HashMultimap.create( );
for ( final String resource : resources ) {
for ( final Ern ern : Ern.parse( resource ).explode( ) ) {
resourceMap.put(
key( ern.getRegion( ), ern.getAccount( ), ern.getResourceType( ) ),
ern.getResourceName( ) );
}
}
final boolean notAction = PolicySpec.NOTACTION.equals( actionElement );
final boolean notResource = PolicySpec.NOTRESOURCE.equals( resourceElement );
// Permute action and resource groups and construct authorizations.
final List<PolicyAuthorization> results = Lists.newArrayList( );
for ( final Map.Entry<String, Collection<String>> actionSetEntry : actionMap.asMap( ).entrySet() ) {
final String vendor = actionSetEntry.getKey( );
final Set<String> actionSet = (Set<String>) actionSetEntry.getValue( );
boolean added = false;
for ( final Map.Entry<PolicyResourceSetKey, Collection<String>> resourceSetEntry : resourceMap.asMap().entrySet() ) {
final Optional<String> region = Optional.fromNullable( resourceSetEntry.getKey( ).region );
final Optional<String> accountIdOrName = Optional.fromNullable( resourceSetEntry.getKey( ).account );
final String type = resourceSetEntry.getKey( ).type;
final Set<String> resourceSet = (Set<String>) resourceSetEntry.getValue( );
if ( PolicySpec.ALL_ACTION.equals( vendor )
|| PolicySpec.ALL_RESOURCE.equals( type )
|| PolicySpec.isPermittedResourceVendor( vendor, PolicySpec.vendor( type ) ) ) {
results.add( new PolicyAuthorization(
sid,
EffectType.valueOf( effect ),
region.orNull( ),
accountIdOrName.orNull( ),
type,
principal,
conditions,
actionSet,
notAction,
resourceSet,
notResource,
variableSet( context, conditions, resourceSet ) ) );
added = true;
}
}
if ( !added ) {
results.add( new PolicyAuthorization(
sid, EffectType.valueOf( effect ), principal, conditions, actionSet, notAction, variableSet( context, conditions ) ) );
}
}
return results;
}
/**
* Parse the conditions of a statement
*
* @param statement The JSON object of the statement
* @param effect The effect of the statement
* @return A list of parsed condition entity.
* @throws JSONException for syntax error.
*/
private List<PolicyCondition> parseConditions(
final JSONObject statement,
final String effect
) throws JSONException {
JSONObject condsObj = JsonUtils.getByType( JSONObject.class, statement, PolicySpec.CONDITION );
boolean isQuota = EffectType.Limit.name( ).equals( effect );
List<PolicyCondition> results = Lists.newArrayList( );
if ( condsObj != null ) {
for ( Object t : condsObj.keySet( ) ) {
String type = ( String ) t;
Class<? extends ConditionOp> typeClass = checkConditionType( type );
JSONObject paramsObj = JsonUtils.getByType( JSONObject.class, condsObj, type );
for ( Object k : paramsObj.keySet( ) ) {
String key = ( String ) k;
Set<String> values = Sets.newHashSet( );
values.addAll( JsonUtils.parseStringOrStringList(
Sets.newHashSet( String.class, Boolean.class, Integer.class, Double.class ), paramsObj, key ) );
key = normalizeString( key );
checkConditionKeyAndValues( key, values, typeClass, isQuota );
results.add( new PolicyCondition( type, key, values ) );
}
}
}
return results;
}
/**
* Check validity of the action value.
*
* @param action The input action pattern.
* @return The vendor of the action.
* @throws JSONException for any error
*/
private String checkAction( String action ) throws JSONException {
final Matcher matcher = PolicySpec.ACTION_PATTERN.matcher( action );
if ( !matcher.matches( ) ) {
throw new JSONException( "'" + action + "' is not a valid action" );
}
if ( PolicySpec.ALL_ACTION.equals( action ) ) {
return PolicySpec.ALL_ACTION;
}
return matcher.group( 1 ); // vendor
}
/**
* Check the validity of a condition type.
*
* @param type The condition type string.
* @return The class represents the condition type.
* @throws JSONException for syntax error.
*/
private Class<? extends ConditionOp> checkConditionType( String type ) throws JSONException {
if ( type == null ) {
throw new JSONException( "Empty condition type" );
}
Class<? extends ConditionOp> typeClass = Conditions.getConditionOpClass( type );
if ( typeClass == null ) {
throw new JSONException( "Condition type '" + type + "' is not supported" );
}
return typeClass;
}
/**
* Check the condition key and value validity.
*
* @param key Condition key.
* @param values Condition values.
* @param typeClass The condition type
* @param isQuota If it is for a quota statement
* @throws JSONException for syntax error.
*/
private void checkConditionKeyAndValues(
final String key,
final Set<String> values,
final Class<? extends ConditionOp> typeClass,
final boolean isQuota
) throws JSONException {
if ( key == null ) {
throw new JSONException( "Empty key name" );
}
final Key keyObj = Keys.getKeyByName( key );
if ( keyObj == null ) {
throw new JSONException( "Condition key '" + key + "' is not supported" );
}
if ( isQuota && !(keyObj instanceof QuotaKey)) {
throw new JSONException( "Quota statement can only use quota keys.'" + key + "' is invalid." );
}
if ( !NullConditionOp.class.equals( typeClass ) ) {
keyObj.validateConditionType( typeClass );
}
if ( values.size( ) < 1 ) {
throw new JSONException( "No value for key '" + key + "'" );
}
if ( isQuota && values.size( ) > 1 ) {
throw new JSONException( "Quota key '" + key + "' can only have one value" );
}
if ( !NullConditionOp.class.equals( typeClass ) ) for ( final String v : values ) {
keyObj.validateValueType( v );
}
}
/**
* Check the validity of effect.
*/
private void checkEffect( String effect ) throws JSONException {
if ( effect == null ) {
throw new JSONException( "Effect can not be empty" );
}
if ( !PolicySpec.EFFECTS.contains( effect ) ) {
throw new JSONException( "Invalid Effect value: " + effect );
}
}
private String normalizeString( String value ) {
return ( value != null ) ? value.trim( ).toLowerCase( ) : null;
}
private Set<String> variableSet(
final PolicyParseContext context,
final List<PolicyCondition> conditions
) {
return variableSet( context, conditions, Collections.<String>emptySet( ) );
}
private static boolean supportsPolicyVariables( final PolicyParseContext context ) {
return context.getVersion( ) != null && "2012-10-17".compareTo( context.getVersion( ) ) <= 0;
}
private Set<String> variableSet(
final PolicyParseContext context,
final List<PolicyCondition> conditions,
final Set<String> resources
) {
if ( !supportsPolicyVariables( context ) ) {
return Collections.emptySet( );
}
final Set<String> variables = Sets.newHashSet( );
for ( final Condition condition : conditions ) {
for ( final String value : condition.getValues( ) ) {
addVariablesFrom( variables, value );
}
}
for ( final String resource : resources ) {
try {
addVariablesFrom( variables, resource );
} catch ( final Exception e ) {
Logs.exhaust( ).error( e, e );
}
}
return variables;
}
private void addVariablesFrom( final Set<String> variables, final String text ) {
final Matcher matcher = VARIABLE_MATCHER.matcher( text );
while ( matcher.find( ) ) {
variables.add( matcher.group( ) );
}
}
private static PolicyResourceSetKey key( final String region, final String account, final String type ) {
return new PolicyResourceSetKey( Strings.emptyToNull( region ), Strings.emptyToNull( account ), type );
}
private static final class PolicyResourceSetKey {
@Nullable
private final String region;
@Nullable
private final String account;
@Nonnull
private final String type;
public PolicyResourceSetKey(
@Nullable final String region,
@Nullable final String account,
@Nonnull final String type ) {
Parameters.checkParam( "type", type, notNullValue( ) );
this.region = region;
this.account = account;
this.type = type;
}
@Override
public boolean equals( final Object o ) {
if ( this == o ) return true;
if ( o == null || getClass( ) != o.getClass( ) ) return false;
final PolicyResourceSetKey that = (PolicyResourceSetKey) o;
return Objects.equals( region, that.region ) &&
Objects.equals( account, that.account ) &&
Objects.equals( type, that.type );
}
@Override
public int hashCode() {
return Objects.hash( region, account, type );
}
}
}