/*************************************************************************
* 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.util.Parameters.checkParam;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.log4j.Logger;
import com.eucalyptus.auth.Accounts;
import com.eucalyptus.auth.AuthEvaluationContext;
import com.eucalyptus.auth.AuthException;
import com.eucalyptus.auth.Contract;
import com.eucalyptus.auth.api.PolicyEngine;
import com.eucalyptus.auth.policy.condition.ConditionOp;
import com.eucalyptus.auth.policy.condition.Conditions;
import com.eucalyptus.auth.policy.condition.NumericGreaterThan;
import com.eucalyptus.auth.policy.ern.AddressUtil;
import com.eucalyptus.auth.policy.key.ContractKey;
import com.eucalyptus.auth.policy.key.ContractKeyEvaluator;
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.policy.variable.PolicyVariables;
import com.eucalyptus.auth.principal.AccountIdentifiers;
import com.eucalyptus.auth.principal.Authorization;
import com.eucalyptus.auth.principal.Condition;
import com.eucalyptus.auth.principal.PolicyScope;
import com.eucalyptus.auth.principal.PolicyVersion;
import com.eucalyptus.auth.principal.Principal;
import com.eucalyptus.auth.principal.TypedPrincipal;
import com.eucalyptus.auth.principal.User;
import com.eucalyptus.auth.principal.Authorization.EffectType;
import com.eucalyptus.util.Exceptions;
import com.eucalyptus.util.Pair;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
/**
* The implementation of policy engine, which evaluates a request against specified policies.
*/
public class PolicyEngineImpl implements PolicyEngine {
private static final Logger LOG = Logger.getLogger( PolicyEngineImpl.class );
private static final Cache<String,ImmutableList<Authorization>> authorizationCache = CacheBuilder
.<String,ImmutableList<Authorization>>newBuilder()
.maximumSize( 10_000 )
.expireAfterWrite( 1, TimeUnit.HOURS )
.build( );
@Nonnull
private final Function<String,String> accountResolver;
@Nonnull
private final Supplier<Boolean> enableSystemQuotas;
@Nonnull
private final Supplier<String> region;
private enum Decision {
DEFAULT, // no match
DENY, // explicit deny
ALLOW, // explicit allow
}
private interface Matcher {
boolean match( String pattern, String instance );
}
private static final Matcher PATTERN_MATCHER = new Matcher( ) {
@Override
public boolean match( String pattern, String instance ) {
pattern = PolicyUtils.toJavaPattern( pattern );
if ( pattern == null ) {
return false;
}
return Pattern.matches( pattern, instance );
}
};
private static final Matcher ADDRESS_MATCHER = new Matcher( ) {
@Override
public boolean match( String pattern, String instance ) {
if ( pattern == null ) {
return false;
}
return AddressUtil.addressRangeMatch( pattern, instance );
}
};
private static final Matcher SERVER_CERTIFICATE_MATCHER = new Matcher( ) {
@Override
public boolean match( String pattern, String instance ) {
if(pattern==null)
return false;
// instance is in full ARN form while pattern is /{cert_name};
if(! instance.startsWith("arn:aws:iam::"))
return false;
int idx = instance.indexOf(":server-certificate");
if(idx<0)
return false;
idx = idx + ":server-certificate".length();
if(idx>=instance.length())
return false;
final String certPathAndName = instance.substring(idx);
return Pattern.matches( pattern, certPathAndName );
}
};
public PolicyEngineImpl(
@Nonnull final Supplier<Boolean> enableSystemQuotas,
@Nonnull final Supplier<String> region
) {
this( DefaultAccountResolver.INSTANCE, enableSystemQuotas, region );
}
public PolicyEngineImpl(
@Nonnull final Function<String,String> accountResolver,
@Nonnull final Supplier<Boolean> enableSystemQuotas,
@Nonnull final Supplier<String> region
) {
this.accountResolver = checkParam( "accountResolver", accountResolver, notNullValue( ) );
this.enableSystemQuotas = checkParam( "enableSystemQuotas", enableSystemQuotas, notNullValue( ) );
this.region = checkParam( "region", region, notNullValue( ) );
}
/*
* The authorization evaluation algorithm is a combination of AWS IAM policy evaluation logic and
* AWS inter-account permission checking logic (including EC2 image and snapshot permission, and
* S3 bucket ACL and bucket policy). The algorithm is described in the following:
*
* 1. If request user is system admin, access is GRANTED.
* 2. Otherwise, check global (inter-account) authorizations, which are attached to account admin.
* If explicitly denied, access is DENIED.
* If explicitly allowed, continue.
* If no matching authorization, check request user's account ID and resource's account ID:
* If not match, access is DENIED.
* If match, continue.
* 3. If request user is account admin, access is GRANTED.
* 4. Otherwise, check local (intra-account) authorizations.
* If explicitly or default denied, access is DENIED.
* If explicitly allowed, access is GRANTED.
*
* (non-Javadoc)
* @see com.eucalyptus.auth.api.PolicyEngine#evaluateAuthorization(java.lang.Class, java.lang.String, java.lang.String)
*/
@Override
public void evaluateAuthorization( @Nonnull final AuthEvaluationContext context,
@Nonnull final AuthorizationMatch authorizationMatch,
@Nullable final String resourceAccountNumber,
@Nonnull final String resourceName,
@Nonnull final Map<Contract.Type, Contract> contracts ) throws AuthException {
try {
final AuthEvaluationContextImpl evaluationContext = (AuthEvaluationContextImpl)context;
if ( Decision.ALLOW != evaluateResourceAuthorization(
evaluationContext,
authorizationMatch,
!evaluationContext.isSystemUser( ),
resourceAccountNumber,
resourceName,
contracts ) ) {
throw new AuthException( AuthException.ACCESS_DENIED );
}
// Allowed
} catch ( AuthException e ) {
throw e;
} catch ( Exception e ) {
LOG.debug( e, e );
throw new AuthException( "An error occurred while trying to evaluate policy for resource access", e );
}
}
@Override
public void evaluateAuthorization( @Nonnull final AuthEvaluationContext context,
final boolean requestAccountDefaultAllow,
@Nullable final PolicyVersion resourcePolicy,
@Nullable final String resourcePolicyAccountNumber,
@Nullable final String resourceAccountNumber,
@Nonnull final String resourceName,
@Nonnull final Map<Contract.Type, Contract> contracts ) throws AuthException {
try {
final AuthEvaluationContextImpl evaluationContext = (AuthEvaluationContextImpl)context;
final ContractKeyEvaluator contractEval = new ContractKeyEvaluator( contracts );
final CachedKeyEvaluator keyEval = new CachedKeyEvaluator( context.getEvaluatedKeys( ) );
final String action = evaluationContext.getAction( );
// System admin can do everything
if ( !evaluationContext.isSystemAdmin() ) {
final boolean sameAccount = context.getRequestUser( ).getAccountNumber( ).equals( resourceAccountNumber );
final boolean sameResourceAccount = resourceAccountNumber != null && resourceAccountNumber.equals( resourcePolicyAccountNumber );
// Check resource authorizations, ignore authorizations for own account
final Decision resourceDecision = resourcePolicy == null ?
Decision.DEFAULT :
processAuthorizations( AuthEvaluationContextImpl.authorizations( resourcePolicy, true ), AuthorizationMatch.All, action, resourceAccountNumber, evaluationContext.getResourceType( ), resourceName, evaluationContext.getPrincipals( ), isAccountPrincipal( resourceAccountNumber ), keyEval, contractEval );
// Denied by explicit or default deny
if ( ( resourceDecision == Decision.DENY ) ||
( !requestAccountDefaultAllow && !sameAccount && sameResourceAccount && resourceDecision != Decision.ALLOW ) ) {
LOG.debug( "Request is rejected by resource authorization check, due to decision " + resourceDecision );
throw new AuthException( AuthException.ACCESS_DENIED );
} else {
final Decision decision = evaluateResourceAuthorization( evaluationContext, AuthorizationMatch.All, !requestAccountDefaultAllow && (resourcePolicy == null || !sameResourceAccount), resourceAccountNumber, resourceName, contracts );
if ( Decision.DENY == decision ||
( !sameAccount && decision != Decision.ALLOW ) ||
( (!(sameResourceAccount || requestAccountDefaultAllow) || resourceDecision != Decision.ALLOW) && decision != Decision.ALLOW ) ) {
throw new AuthException( AuthException.ACCESS_DENIED );
}
}
}
// Allowed
} catch ( AuthException e ) {
//throw by the policy engine implementation
LOG.debug( e, e );
throw e;
} catch ( Exception e ) {
LOG.debug( e, e );
throw new AuthException( "An error occurred while trying to evaluate policy for resource access", e );
}
}
/*
* Quota evaluation algorithm is very simple: going through all quotas that can be applied to the request (by user
* and resource), at all levels (account, group and user), if any of the quota is exceeded, reject the request.
*
* (non-Javadoc)
* @see com.eucalyptus.auth.api.PolicyEngine#evaluateQuota(java.lang.Integer, java.lang.Class, java.lang.String)
*/
@Override
public void evaluateQuota( @Nonnull final AuthEvaluationContext context,
@Nonnull String resourceName,
@Nonnull final Long quantity ) throws AuthException {
try {
final AuthEvaluationContextImpl evaluationContext = (AuthEvaluationContextImpl)context;
String resourceType = context.getResourceType();
resourceName = PolicySpec.canonicalizeResourceName( resourceType, resourceName );
String action = context.getAction().toLowerCase();
// Quotas can be disabled for system users
if ( !evaluationContext.isSystemUser( ) || enableSystemQuotas.get( ) ) {
List<Pair<PolicyVersion,Authorization>> quotas = evaluationContext.lookupQuotas( );
processQuotas(
quotas,
evaluationContext.getRequestAccountNumber( ),
evaluationContext.getRequestUser( ).getUserId( ),
action,
resourceType,
resourceName,
quantity );
}
} catch ( AuthException e ) {
//throw by the policy engine implementation
throw e;
} catch ( Exception e ) {
throw new AuthException( "An error occurred while trying to evaluate policy for resource allocation.", e );
}
}
@Override
public AuthEvaluationContext createEvaluationContext( final String resourceType,
final String action,
final User requestUser,
final Map<String,String> evaluatedKeys,
final Iterable<PolicyVersion> policies ) {
return new AuthEvaluationContextImpl( resourceType, action, requestUser, evaluatedKeys, policies );
}
@Override
public AuthEvaluationContext createEvaluationContext( final String resourceType,
final String action,
final User requestUser,
final Map<String,String> evaluatedKeys,
final Iterable<PolicyVersion> policies,
final Set<TypedPrincipal> principals ) {
return new AuthEvaluationContextImpl( resourceType, action, requestUser, evaluatedKeys, policies, principals );
}
private Decision evaluateResourceAuthorization( @Nonnull final AuthEvaluationContext context,
@Nonnull final AuthorizationMatch authorizationMatch,
final boolean accountOnly,
@Nullable final String resourceAccountNumber,
@Nonnull String resourceName,
@Nonnull final Map<Contract.Type, Contract> contracts ) throws AuthException {
final AuthEvaluationContextImpl evaluationContext = (AuthEvaluationContextImpl)context;
final ContractKeyEvaluator contractEval = new ContractKeyEvaluator( contracts );
final CachedKeyEvaluator keyEval = new CachedKeyEvaluator( context.getEvaluatedKeys( ) );
final String action = evaluationContext.getAction( );
final String resourceType = evaluationContext.getResourceType( );
resourceName = PolicySpec.canonicalizeResourceName( resourceType, resourceName );
// System admin can do everything
if ( evaluationContext.isSystemAdmin( ) ) {
return Decision.ALLOW;
}
final String accountNumber = evaluationContext.getRequestAccountNumber( );
if ( accountOnly && resourceAccountNumber != null && !resourceAccountNumber.equals( accountNumber ) ) {
LOG.debug( "Request is rejected due to resource account mismatch with identity account" );
return Decision.DEFAULT;
}
final Decision decision = processAuthorizations(
evaluationContext.lookupAuthorizations( ),
authorizationMatch,
action,
resourceAccountNumber,
resourceType,
resourceName,
evaluationContext.getPrincipals( ),
Predicates.alwaysFalse( ),
keyEval,
contractEval );
if ( decision == Decision.DENY || decision == Decision.DEFAULT ) {
LOG.debug( "Request is rejected by authorization check, due to decision " + decision );
}
return decision;
}
/**
* Process a list of authorizations against the current request. Collecting contracts from matching authorizations.
*
* @param authorizations The list of authorizations to process
* @param action The request action
* @param resource The requested resource
* @param keyEval The key cache for condition evaluation (optimization purpose)
* @param contractEval The contract evaluator and collector.
* @return The final decision: DEFAULT - no matching authorization, DENY - explicit deny, ALLOW = explicit allow
* @throws AuthException
*/
private Decision processAuthorizations( @Nonnull final List<Authorization> authorizations,
@Nonnull AuthorizationMatch authorizationMatch,
@Nonnull final String action,
@Nullable final String resourceAccountNumber,
@Nullable final String resourceType,
@Nullable final String resource,
@Nullable final Set<TypedPrincipal> principals,
@Nullable final Predicate<TypedPrincipal> denyOnlyPrincipal,
@Nonnull final CachedKeyEvaluator keyEval,
@Nonnull final ContractKeyEvaluator contractEval ) throws AuthException {
Decision result = Decision.DEFAULT;
final String region = PolicyEngineImpl.this.region.get( );
for ( Authorization auth : authorizations ) {
boolean denyOnly = false;
if ( auth.getEffect( ) == EffectType.Limit ) continue;
if ( !matchActions( auth, action ) ) {
continue;
}
if ( !matchPrincipal( auth.getPrincipal(), filter( principals, Predicates.not( denyOnlyPrincipal ) ) ) ) {
if ( !matchPrincipal( auth.getPrincipal(), filter( principals, denyOnlyPrincipal ) ) ) {
continue;
} else {
denyOnly = true;
}
}
if ( authorizationMatch == AuthorizationMatch.Unconditional && auth.getEffect( ) == EffectType.Allow ) {
return Decision.ALLOW; // Cannot deny reliably with unconditional matching
}
if ( !matchResources( auth, region, resourceAccountNumber, resourceType, resource ) ) {
continue;
}
if ( !evaluateConditions( auth.getPolicyVariables(), auth.getConditions( ), action, keyEval, contractEval ) ) {
continue;
}
if ( auth.getEffect( ) == EffectType.Deny ) {
// Explicit deny
return Decision.DENY;
} else if ( !denyOnly ) {
result = Decision.ALLOW;
}
}
return result;
}
private boolean matchActions( Authorization auth, String action ) throws AuthException {
return evaluateElement( matchOne( auth.getActions( ), action, PATTERN_MATCHER ), auth.isNotAction( ) );
}
private boolean matchPrincipal( @Nullable Principal principal, @Nullable Set<TypedPrincipal> principals ) throws AuthException {
if ( principal == null ) {
return true;
} else if ( principals != null ) {
boolean anyMatch = false;
for( final TypedPrincipal typedPrincipal : principals ) {
if ( typedPrincipal.getType( ) == principal.getType( ) ) {
if ( evaluateElement(
matchOne(
typedPrincipal.getType( ).convertForUserMatching( principal.getValues( ) ),
typedPrincipal.getName( ),
PATTERN_MATCHER ),
false ) ) {
anyMatch = true;
}
}
}
return (anyMatch && !principal.isNotPrincipal( )) || (!anyMatch && principal.isNotPrincipal( ));
}
return principal.isNotPrincipal( );
}
private boolean matchResources( Authorization auth, String resourceType, String resource ) throws AuthException {
return matchResources( auth, null, null, resourceType, resource );
}
private boolean matchResources( @Nonnull Authorization auth,
@Nullable String region,
@Nullable String resourceAccountNumber,
@Nullable String resourceType,
@Nullable String resource ) throws AuthException {
if ( resource == null ) {
return true;
} else if ( auth.getRegion() != null && region != null && !auth.getRegion().equals( region ) ) {
return auth.isNotResource( );
} else if ( auth.getAccount() != null && resourceAccountNumber != null && !resolveAccount(auth.getAccount()).equals( resourceAccountNumber ) ) {
return auth.isNotResource( );
} else if ( auth.getType( ) != null && !matchOne( Collections.singleton( auth.getType( ) ), resourceType, PATTERN_MATCHER ) ) {
return auth.isNotResource( );
} else if ( PolicySpec.EC2_RESOURCE_ADDRESS.equals( auth.getType( ) ) ) {
return evaluateElement( matchOne( auth.getResources( ), resource, ADDRESS_MATCHER ), auth.isNotResource( ) );
} else if ( String.format("%s:%s", PolicySpec.VENDOR_IAM, PolicySpec.IAM_RESOURCE_SERVER_CERTIFICATE).equals ( auth.getType( ))){
return evaluateElement( matchOne( auth.getResources( ), resource, SERVER_CERTIFICATE_MATCHER ), auth.isNotResource( ) );
}else {
return evaluateElement( matchOneOrEmpty( auth.getPolicyVariables( ), auth.getResources( ), resource, PATTERN_MATCHER ), auth.isNotResource( ) );
}
}
private static boolean matchOne( Set<String> patterns, String instance, Matcher matcher ) throws AuthException {
return matchOne( Collections.emptySet( ), patterns, instance, matcher );
}
private static boolean matchOne( Set<String> variables, Set<String> patterns, String instance, Matcher matcher ) throws AuthException {
for ( String pattern : patterns ) {
if ( matcher.match( variableExplode( variables, pattern ) , instance ) ) {
return true;
}
}
return false;
}
private static boolean matchOneOrEmpty( Set<String> variables, Set<String> patterns, String instance, Matcher matcher ) throws AuthException {
return patterns.isEmpty( ) ||
matchOne( variables, patterns, instance, matcher );
}
private static String variableExplode( Set<String> variables, String text ) throws AuthException {
if ( variables.isEmpty( ) ) return text;
String result = text;
for ( final String variable : variables ) {
final String variableValue = PolicyVariables.getPolicyVariable( variable ).evaluate( );
//TODO: variable values cannot currently contain ? or *, if they could we would need
//TODO: to escape the values when they were used in regex matches
result = result.replace( variable, variableValue );
}
return result;
}
private <T> Set<T> filter( final Iterable<T> iterable, final Predicate<? super T> matching ) {
if ( iterable == null ) {
return null;
}
return ImmutableSet.copyOf( Iterables.filter( iterable, matching ) );
}
private String resolveAccount( final String accountNumberOrAlias ) {
return accountResolver.apply( accountNumberOrAlias );
}
private boolean evaluateElement( boolean patternMatched, boolean isNot ) {
return ( ( patternMatched && !isNot ) || ( !patternMatched && isNot ) );
}
/**
* Evaluate conditions for an authorization.
*/
private boolean evaluateConditions(
final Set<String> policyVariables,
final List<? extends Condition> conditions,
final String action,
final CachedKeyEvaluator keyEval,
final ContractKeyEvaluator contractEval
) throws AuthException {
for ( Condition cond : conditions ) {
ConditionOp op = Conditions.getOpInstance( cond.getType( ) );
Key key = Keys.getKeyByName( cond.getKey( ) );
final boolean applies = key.canApply( action );
if ( key instanceof ContractKey ) {
if ( applies ) contractEval.addContract( ( ContractKey ) key, cond.getValues( ) );
continue;
}
boolean condValue = false;
final Set<String> expandedValues = Sets.newLinkedHashSet( );
for ( String value : cond.getValues( ) ) {
expandedValues.add( variableExplode( policyVariables, value ) );
}
condValue = op.check( applies ? keyEval.getValues( key ) : Collections.singleton( null ), expandedValues );
if ( !condValue ) {
return false;
}
}
return true;
}
/**
* Process each of the quota authorizations. If any of them is exceeded, deny access.
*
* @param quotas The quota authorizations
* @param action The request action.
* @param resourceType The resource type for allocation
* @param resourceName The resource associated with the allocation
* @param quantity The quantity to allocate.
* @throws AuthException for any error.
*/
private void processQuotas( final List<Pair<PolicyVersion,Authorization>> quotas,
final String accountId,
final String userId,
final String action,
final String resourceType,
final String resourceName,
final Long quantity ) throws AuthException {
NumericGreaterThan ngt = new NumericGreaterThan( );
for ( Pair<PolicyVersion,Authorization> quota : quotas ) {
final PolicyVersion policy = quota.getLeft();
final Authorization auth = quota.getRight( );
if ( !matchActions( auth, action ) ) {
LOG.debug( "Action " + action + " not matching" );
continue;
}
if ( !matchResources( auth, resourceType, resourceName ) ) {
LOG.debug( "Resource " + resourceName + " not matching" );
continue;
}
PolicyScope scope = policy.getPolicyScope();
String principalId = getAuthorizationPrincipalId( scope, accountId, userId );
for ( Condition cond : auth.getConditions( ) ) {
Key key = Keys.getKeyByName( cond.getKey( ) );
if ( !( key instanceof QuotaKey ) ) {
LOG.debug( "Key " + cond.getKey( ) + " is not a quota" );
continue;
}
QuotaKey quotaKey = ( QuotaKey ) key;
if ( !quotaKey.canApply( action , resourceType ) ) {
LOG.debug( "Key " + cond.getKey( ) + " can not apply for action=" + action + ", resourceType=" + resourceType );
continue;
}
String usageValue = quotaKey.value( scope, principalId, resourceName, quantity );
if ( QuotaKey.NOT_SUPPORTED.equals( usageValue ) ) {
LOG.debug( "Key " + cond.getKey( ) + " is not supported for scope=" + scope );
continue;
}
String quotaValue = Iterables.getFirst( cond.getValues(), null );
if ( ngt.check( usageValue, quotaValue ) ) {
LOG.error( "Quota " + key.getClass( ).getName( ) + " is exceeded: quota=" + quotaValue + ", usage=" + usageValue );
throw new AuthException( AuthException.QUOTA_EXCEEDED );
}
}
}
}
/**
* Get the principal ID for an authorization based on scope.
*
* @param scope The scope
* @return The principal ID (account, group or user)
* @throws AuthException for any error
*/
private String getAuthorizationPrincipalId( PolicyScope scope, String accountId, String userId ) throws AuthException {
switch ( scope ) {
case Account:
return accountId;
case User:
case Group:
case Role:
return userId;
}
throw new RuntimeException( "Should not reach here: unrecognized scope." );
}
@SuppressWarnings( "Guava" )
private Predicate<TypedPrincipal> isAccountPrincipal( final String accountNumber ) throws AuthException {
if ( accountNumber == null ) {
return Predicates.alwaysFalse( );
} else {
final TypedPrincipal match = TypedPrincipal.of( Principal.PrincipalType.AWS, Accounts.getAccountArn( accountNumber ) );
return Predicates.equalTo( match );
}
}
static class AuthEvaluationContextImpl implements AuthEvaluationContext {
@Nullable
private final String resourceType;
private final String action;
private final User requestUser;
@Nullable
private final Set<TypedPrincipal> principals;
private Boolean systemAdmin;
private Boolean systemUser;
private Map<String,String> evaluatedKeys;
private List<Authorization> authorizations;
private List<Pair<PolicyVersion,Authorization>> quotaAuthorizations;
private final List<PolicyVersion> policies;
AuthEvaluationContextImpl( @Nullable final String resourceType,
final String action,
final User requestUser,
final Map<String, String> evaluatedKeys,
final Iterable<PolicyVersion> policies ) {
this( resourceType, action, requestUser, evaluatedKeys, policies, null );
}
AuthEvaluationContextImpl( @Nullable final String resourceType,
final String action,
final User requestUser,
final Map<String, String> evaluatedKeys,
final Iterable<PolicyVersion> policies,
@Nullable final Set<TypedPrincipal> principals ) {
this.resourceType = resourceType;
this.action = action.toLowerCase();
this.evaluatedKeys = ImmutableMap.copyOf( evaluatedKeys.entrySet( ).stream( )
.filter( entry -> entry.getValue( ) != null )
.collect( Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue ) ) );
this.policies = ImmutableList.copyOf( policies );
this.requestUser = requestUser;
this.principals = principals==null ? null : ImmutableSet.copyOf( principals );
}
@Nullable
@Override
public String getResourceType() {
return resourceType;
}
@Override
public String getAction() {
return action;
}
@Override
public User getRequestUser() {
return requestUser;
}
public Map<String, String> getEvaluatedKeys( ) {
return evaluatedKeys;
}
@Override
@Nullable
public Set<TypedPrincipal> getPrincipals() {
return principals;
}
@Override
public String describe( String resourceAccountNumber, String resourceName ) {
return String.valueOf(resourceType) + ":" + resourceName + (resourceAccountNumber==null ? "" : " of " + resourceAccountNumber) + " for " + describePrincipal( );
}
@Override
public String describe( final String resourceName, final Long quantity ) {
return String.valueOf(resourceType) + ":" + resourceName + " by " + quantity + " for " + describePrincipal( );
}
private String describePrincipal( ) {
Principal.PrincipalType principalType = null;
String principalName = null;
if ( principals != null && !principals.isEmpty( ) ) {
final TypedPrincipal primaryPrincipal = Iterables.get( principals, 0 );
principalType = primaryPrincipal.getType( );
principalName = primaryPrincipal.getName( );
}
return principalType != null ?
principalType + ":" + principalName + " / " + requestUser :
String.valueOf( requestUser );
}
String getRequestAccountNumber() throws AuthException {
return requestUser.getAccountNumber( );
}
boolean isSystemAdmin() {
if ( systemAdmin == null ) {
systemAdmin = requestUser.isSystemAdmin();
}
return systemAdmin;
}
boolean isSystemUser() {
if ( systemUser == null ) {
systemUser = requestUser.isSystemUser();
}
return systemUser;
}
public List<Authorization> lookupAuthorizations( ) throws AuthException {
if ( authorizations == null ) {
final List<Pair<PolicyVersion,Authorization>> authorizations = authorizations( policies, false );
this.authorizations = ImmutableList.copyOf( Iterables.filter(
Iterables.transform( authorizations, Pair.<PolicyVersion,Authorization>right( ) ),
resourceType == null ?
AuthorizationPredicates.ALLOW_EFFECT :
Predicates.not( AuthorizationPredicates.LIMIT_EFFECT )
) );
}
return authorizations;
}
static List<Authorization> authorizations( final PolicyVersion policy, final boolean resourcePolicy ) throws AuthException {
try {
return authorizationCache.get( policy.getPolicyHash( ), new Callable<ImmutableList<Authorization>>() {
@Override
public ImmutableList<Authorization> call() throws Exception {
return ImmutableList.copyOf( ( resourcePolicy ? PolicyParser.getLaxResourceInstance( ) : PolicyParser.getLaxInstance( ) ).parse( policy.getPolicy( ) ).getAuthorizations( ) );
}
} );
} catch ( final ExecutionException e ) {
throw new AuthException( "Invalid policy", e.getCause( ) );
}
}
static List<Pair<PolicyVersion,Authorization>> authorizations( final List<PolicyVersion> policies, final boolean resourcePolicy ) throws AuthException {
final List<Pair<PolicyVersion,Authorization>> authorizations = Lists.newArrayList( );
for ( final PolicyVersion policy : policies ) {
Iterables.addAll( authorizations, Iterables.transform( authorizations( policy, resourcePolicy ), Pair.<PolicyVersion,Authorization>pair( ).apply( policy ) ) );
}
return authorizations;
}
public List<Pair<PolicyVersion,Authorization>> lookupQuotas( ) throws AuthException {
if ( quotaAuthorizations == null ) {
this.quotaAuthorizations = ImmutableList.copyOf( Iterables.filter(
authorizations( policies, false ),
Predicates.compose( AuthorizationPredicates.LIMIT_EFFECT, Pair.<PolicyVersion,Authorization>right( ) )
) );
}
return quotaAuthorizations;
}
}
private enum AuthorizationPredicates implements Predicate<Authorization> {
ALLOW_EFFECT {
@Override
public boolean apply( @Nullable final Authorization authorization ) {
return authorization != null && authorization.getEffect( ) == EffectType.Allow;
}
},
ALL_RESOURCE {
@Override
public boolean apply( @Nullable final Authorization authorization ) {
return authorization != null && authorization.getResources( ).contains( PolicySpec.ALL_RESOURCE );
}
},
LIMIT_EFFECT {
@Override
public boolean apply( @Nullable final Authorization authorization ) {
return authorization != null && authorization.getEffect( ) == EffectType.Limit;
}
},
}
private enum EucalyptusAccountNumberSupplier implements Supplier<String> {
INSTANCE;
@Override
public String get() {
try {
return Accounts.lookupAccountIdByAlias( AccountIdentifiers.SYSTEM_ACCOUNT );
} catch ( AuthException e ) {
throw Exceptions.toUndeclared( e );
}
}
}
private enum DefaultAccountResolver implements Function<String,String> {
INSTANCE;
private static final Supplier<String> eucalyptusAccountNumberSupplier =
Suppliers.memoize( EucalyptusAccountNumberSupplier.INSTANCE );
@Override
public String apply( final String accountNumberOrAlias ) {
if ( AccountIdentifiers.SYSTEM_ACCOUNT.equals( accountNumberOrAlias ) ) {
return eucalyptusAccountNumberSupplier.get( );
} else {
return accountNumberOrAlias;
}
}
}
}