@cloud-copilot/iam-simulate
Version:
Simulate evaluation of AWS IAM policies
197 lines (195 loc) • 8.13 kB
JavaScript
import { isAssumedRoleArn, isFederatedUserArn, isIamRoleArn, isIamUserArn, isServicePrincipal } from '@cloud-copilot/iam-utils';
/**
* The default authorizer for services.
*/
export class DefaultServiceAuthorizer {
/**
* Authorize a service request after all policy analysis has been completed.
*
* @param request the service authorization request containing all analyses
* @returns the result of the authorization
*/
authorize(request) {
const scpResult = request.scpAnalysis.result;
const rcpResult = request.rcpAnalysis.result;
const sessionResult = request.sessionAnalysis?.result;
const identityStatementResult = request.identityAnalysis.result;
const resourcePolicyResult = request.resourceAnalysis?.result;
const permissionBoundaryResult = request.permissionBoundaryAnalysis?.result;
const endpointPolicyResult = request.endpointAnalysis?.result;
const principalAccount = request.request.principal.accountId();
const resourceAccount = request.request.resource?.accountId();
const sameAccount = principalAccount === resourceAccount;
const baseResult = {
sameAccount,
sessionAnalysis: request.sessionAnalysis,
identityAnalysis: request.identityAnalysis,
scpAnalysis: request.scpAnalysis,
rcpAnalysis: request.rcpAnalysis,
resourceAnalysis: request.resourceAnalysis,
permissionBoundaryAnalysis: request.permissionBoundaryAnalysis,
endpointAnalysis: request.endpointAnalysis
};
if (scpResult !== 'Allowed') {
return {
result: scpResult,
...baseResult
};
}
if (rcpResult !== 'Allowed') {
return {
result: rcpResult,
...baseResult
};
}
if (sessionResult && sessionResult !== 'Allowed') {
return {
result: sessionResult,
...baseResult
};
}
if (endpointPolicyResult === 'ExplicitlyDenied' ||
endpointPolicyResult === 'ImplicitlyDenied') {
return {
result: endpointPolicyResult,
...baseResult
};
}
if (resourcePolicyResult === 'ExplicitlyDenied' ||
resourcePolicyResult === 'DeniedForAccount') {
return {
result: 'ExplicitlyDenied',
...baseResult
};
}
if (identityStatementResult === 'ExplicitlyDenied') {
return {
result: 'ExplicitlyDenied',
...baseResult
};
}
if (permissionBoundaryResult === 'ExplicitlyDenied') {
return {
result: 'ExplicitlyDenied',
...baseResult
};
}
// Service Principals
if (isServicePrincipal(request.request.principal.value())) {
// Service principals are allowed if the resource policy allows them
if (resourcePolicyResult === 'Allowed') {
return {
result: 'Allowed',
...baseResult
};
}
return {
result: 'ImplicitlyDenied',
...baseResult
};
}
//Same Account
if (principalAccount === resourceAccount) {
if (permissionBoundaryResult === 'ImplicitlyDenied') {
/**
* If the permission boundary is an implicit deny
*
* If the request is from an assumed role ARN AND the resource policy allows the assumed role (session) ARN = ALLOW
* If the request is from an IAM user ARN AND the resource policy allows the IAM user ARN = ALLOW
* If the request is from a federated user ARN AND the resource policy allows the federated user ARN = ALLOW
* The request is allowed: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html
*/
if (resourcePolicyResult === 'Allowed') {
const principal = request.request.principal.value();
if (isIamRoleArn(principal) &&
request.simulationParameters.simulationMode === 'Discovery') {
if (request.resourceAnalysis.allowStatements.some((statement) => statement.principalMatch === 'Match' && statement.ignoredRoleSessionName)) {
return {
result: 'Allowed',
...baseResult
};
}
}
if (isAssumedRoleArn(principal) ||
isIamUserArn(principal) ||
isFederatedUserArn(principal)) {
if (request.resourceAnalysis.allowStatements.some((statement) => statement.principalMatch === 'Match')) {
return {
result: 'Allowed',
...baseResult
};
}
}
}
return {
result: 'ImplicitlyDenied',
...baseResult
};
}
/*
TODO: Implicit denies in identity policies
I think if the identity policy has an implicit deny for assumed roles or federated users,
then the resource policy must have the federated or assumed role ARN exactly.
That doesn't seem right though. I know many cases where the resource policy has the role ARN and it works
Need to add some tests for this.
*/
const trustedAccount = this.serviceTrustsPrincipalAccount(sameAccount, request.resourceAnalysis, request.request.resource);
if (resourcePolicyResult === 'Allowed' ||
(trustedAccount && identityStatementResult === 'Allowed')) {
return {
result: 'Allowed',
...baseResult
};
}
return {
result: 'ImplicitlyDenied',
...baseResult
};
}
//Cross Account
if (permissionBoundaryResult === 'ImplicitlyDenied') {
return {
result: 'ImplicitlyDenied',
...baseResult
};
}
if (resourcePolicyResult === 'Allowed' || resourcePolicyResult === 'AllowedForAccount') {
if (identityStatementResult === 'Allowed') {
return {
result: 'Allowed',
...baseResult
};
}
return {
result: 'ImplicitlyDenied',
...baseResult
};
}
return {
result: 'ImplicitlyDenied',
...baseResult
};
/**
* Add checks for:
* * root user - can override resource policies for most resource types
* * service linked roles - ignore SCPs and RCPs
* * session policies
* * vpc endpoint policies
* * organization APIs and delegated admin policy
*/
}
/**
* Determines if the service trusts the principal's Account's IAM policies
*
* @param sameAccount - If the principal and resource are in the same account
* @param resourceAnalysis - The resource policy analysis
* @returns true if the service trusts the principal's account IAM policies
*/
serviceTrustsPrincipalAccount(sameAccount, resourceAnalysis, resource) {
if (sameAccount) {
return true;
}
return resourceAnalysis.allowStatements.some((statement) => statement.principalMatch === 'AccountLevelMatch');
}
}
//# sourceMappingURL=DefaultServiceAuthorizer.js.map