Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,20 @@ public IamPolicy getPolicy(String policyArn) {
"Policy " + policyArn + " does not exist.", 404));
}

/**
* Resolves a policy by ARN without throwing, mirroring {@link #getPolicy} so that
* AWS-managed policies (arn:aws:iam::aws:policy/...) are served from the global catalog
* rather than the account-partitioned store. Attached-policy read paths must use this:
* a managed policy attached to a principal owned by a non-default account is absent from
* that account's {@link #policies} partition and would otherwise be silently dropped.
*/
private Optional<IamPolicy> resolvePolicy(String arn) {
if (arn != null && arn.startsWith(AwsManagedPolicies.ARN_PREFIX)) {
return Optional.ofNullable(awsManagedPolicies.get(arn));
}
return policies.get(arn);
}

private void rejectIfAwsManaged(String policyArn) {
if (policyArn != null && policyArn.startsWith(AwsManagedPolicies.ARN_PREFIX)) {
throw new AwsException("AccessDenied",
Expand Down Expand Up @@ -628,7 +642,7 @@ public void detachUserPolicy(String userName, String policyArn) {

public List<IamPolicy> listAttachedUserPolicies(String userName, String pathPrefix) {
return getUser(userName).getAttachedPolicyArns().stream()
.flatMap(arn -> policies.get(arn).stream())
.flatMap(arn -> resolvePolicy(arn).stream())
.filter(p -> pathPrefix == null || p.getPath().startsWith(pathPrefix))
.toList();
}
Expand Down Expand Up @@ -663,7 +677,7 @@ public void detachGroupPolicy(String groupName, String policyArn) {

public List<IamPolicy> listAttachedGroupPolicies(String groupName, String pathPrefix) {
return getGroup(groupName).getAttachedPolicyArns().stream()
.flatMap(arn -> policies.get(arn).stream())
.flatMap(arn -> resolvePolicy(arn).stream())
.filter(p -> pathPrefix == null || p.getPath().startsWith(pathPrefix))
.toList();
}
Expand Down Expand Up @@ -698,7 +712,7 @@ public void detachRolePolicy(String roleName, String policyArn) {

public List<IamPolicy> listAttachedRolePolicies(String roleName, String pathPrefix) {
return getRole(roleName).getAttachedPolicyArns().stream()
.flatMap(arn -> policies.get(arn).stream())
.flatMap(arn -> resolvePolicy(arn).stream())
.filter(p -> pathPrefix == null || p.getPath().startsWith(pathPrefix))
.toList();
}
Expand Down Expand Up @@ -1125,7 +1139,7 @@ public CallerContext resolvePrincipalContext(String principalArn) {
private String resolveUserBoundaryDocument(String userName) {
return users.get(userName)
.map(IamUser::getPermissionsBoundaryArn)
.flatMap(arn -> policies.get(arn))
.flatMap(this::resolvePolicy)
.map(IamPolicy::getDefaultDocument)
.orElse(null);
}
Expand All @@ -1137,7 +1151,7 @@ private String resolveRoleBoundaryDocument(String roleArn) {
String roleName = roleArn.contains("/") ? roleArn.substring(roleArn.lastIndexOf('/') + 1) : roleArn;
return roles.get(roleName)
.map(IamRole::getPermissionsBoundaryArn)
.flatMap(arn -> policies.get(arn))
.flatMap(this::resolvePolicy)
.map(IamPolicy::getDefaultDocument)
.orElse(null);
}
Expand Down Expand Up @@ -1196,7 +1210,7 @@ private List<String> collectUserPolicies(String userName) {

// User attached managed policies
for (String arn : user.getAttachedPolicyArns()) {
Optional<IamPolicy> p = policies.get(arn);
Optional<IamPolicy> p = resolvePolicy(arn);
if (p.isPresent() && p.get().getDefaultDocument() != null) {
docs.add(p.get().getDefaultDocument());
}
Expand All @@ -1209,7 +1223,7 @@ private List<String> collectUserPolicies(String userName) {
IamGroup group = groupOpt.get();
docs.addAll(group.getInlinePolicies().values());
for (String arn : group.getAttachedPolicyArns()) {
Optional<IamPolicy> p = policies.get(arn);
Optional<IamPolicy> p = resolvePolicy(arn);
if (p.isPresent() && p.get().getDefaultDocument() != null) {
docs.add(p.get().getDefaultDocument());
}
Expand All @@ -1236,7 +1250,7 @@ private List<String> collectRolePolicies(String roleArn) {

// Role attached managed policies
for (String arn : role.getAttachedPolicyArns()) {
Optional<IamPolicy> p = policies.get(arn);
Optional<IamPolicy> p = resolvePolicy(arn);
if (p.isPresent() && p.get().getDefaultDocument() != null) {
docs.add(p.get().getDefaultDocument());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@
import io.github.hectorvent.floci.core.common.RequestContext;
import io.github.hectorvent.floci.core.storage.AccountAwareStorageBackend;
import io.github.hectorvent.floci.core.storage.InMemoryStorage;
import io.github.hectorvent.floci.services.iam.model.CallerContext;
import io.github.hectorvent.floci.services.iam.model.IamGroup;
import io.github.hectorvent.floci.services.iam.model.IamPolicy;
import io.github.hectorvent.floci.services.iam.model.IamRole;
import io.github.hectorvent.floci.services.iam.model.IamUser;
import jakarta.enterprise.inject.Instance;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
Expand Down Expand Up @@ -153,4 +158,137 @@ void listPoliciesScopesLocalToCallerAndDoesNotDuplicateMirroredManaged() {
assertEquals(AwsManagedPolicies.POLICIES.size(), defAll.size());
assertEquals(1L, defAll.stream().filter(p -> mirroredManagedArn.equals(p.getArn())).count());
}

@Test
void attachedManagedPolicyResolvesForUserInNonDefaultAccount() {
// Request runs as account 111...; managed policies are only mirrored into the default
// account at seed time. A managed policy attached to a user owned by 111... was silently
// dropped by the attached-policy read paths, which resolved straight from the
// account-partitioned store instead of the global catalog.
Instance<RequestContext> ctx = requestContextFor(REQUEST_ACCT);
InMemoryStorage<String, IamUser> rawUsers = new InMemoryStorage<>();
InMemoryStorage<String, IamPolicy> rawPolicies = new InMemoryStorage<>();
AccountAwareStorageBackend<IamUser> users = new AccountAwareStorageBackend<>(rawUsers, ctx, DEFAULT_ACCT);
AccountAwareStorageBackend<IamPolicy> policies =
new AccountAwareStorageBackend<>(rawPolicies, ctx, DEFAULT_ACCT);

String managedArn = AwsManagedPolicies.ARN_PREFIX + "/service-role/AWSLambdaBasicExecutionRole";
// A customer policy attached to the same user — must stay account-scoped (control).
String customerArn = "arn:aws:iam::" + REQUEST_ACCT + ":policy/app-policy";
policies.putForAccount(REQUEST_ACCT, customerArn,
new IamPolicy("ANPAAPP000000001", "app-policy", "/", customerArn,
"app", AwsManagedPolicies.PERMISSIVE_DOCUMENT));

IamUser user = new IamUser("AIDAUSER00000001", "app-user", "/",
"arn:aws:iam::" + REQUEST_ACCT + ":user/app-user");
user.getAttachedPolicyArns().add(managedArn);
user.getAttachedPolicyArns().add(customerArn);
users.putForAccount(REQUEST_ACCT, "app-user", user);

IamService service = new IamService(
users, new InMemoryStorage<>(), new InMemoryStorage<>(),
policies,
new InMemoryStorage<>(), new InMemoryStorage<>(), new InMemoryStorage<>(),
new RegionResolver("us-east-1", DEFAULT_ACCT));

// ListAttachedUserPolicies returns both the managed (catalog) and customer (scoped) policies.
List<IamPolicy> attached = service.listAttachedUserPolicies("app-user", null);
assertEquals(2, attached.size());
assertTrue(attached.stream().anyMatch(p -> managedArn.equals(p.getArn())));
assertTrue(attached.stream().anyMatch(p -> customerArn.equals(p.getArn())));

// SimulatePrincipalPolicy (resolvePrincipalContext -> collectUserPolicies) now picks up
// the attached managed policy's document for a non-default-account principal.
CallerContext caller = service.resolvePrincipalContext(
"arn:aws:iam::" + REQUEST_ACCT + ":user/app-user");
assertTrue(caller.identityPolicies().contains(AwsManagedPolicies.PERMISSIVE_DOCUMENT));

// Control: the customer policy is genuinely account-scoped — invisible from another account.
Instance<RequestContext> otherCtx = requestContextFor("222222222222");
IamService otherService = new IamService(
new AccountAwareStorageBackend<>(rawUsers, otherCtx, DEFAULT_ACCT),
new InMemoryStorage<>(), new InMemoryStorage<>(),
new AccountAwareStorageBackend<>(rawPolicies, otherCtx, DEFAULT_ACCT),
new InMemoryStorage<>(), new InMemoryStorage<>(), new InMemoryStorage<>(),
new RegionResolver("us-east-1", DEFAULT_ACCT));
// The user does not exist under account 222..., so the customer policy never leaks; assert
// the catalog policy still resolves directly regardless of account.
assertFalse(otherService.listPolicies("Local", null).stream()
.anyMatch(p -> customerArn.equals(p.getArn())));
assertNotNull(otherService.getPolicy(managedArn));
}

@Test
void attachedManagedPolicyResolvesForGroupInNonDefaultAccount() {
// Symmetric with the user/role cases: a managed policy attached to a group owned by a
// non-default account must resolve from the global catalog (the resolvePolicy fix applied
// to listAttachedGroupPolicies), while a customer policy attached to the same group stays
// account-scoped.
Instance<RequestContext> ctx = requestContextFor(REQUEST_ACCT);
InMemoryStorage<String, IamGroup> rawGroups = new InMemoryStorage<>();
InMemoryStorage<String, IamPolicy> rawPolicies = new InMemoryStorage<>();
AccountAwareStorageBackend<IamGroup> groups = new AccountAwareStorageBackend<>(rawGroups, ctx, DEFAULT_ACCT);
AccountAwareStorageBackend<IamPolicy> policies =
new AccountAwareStorageBackend<>(rawPolicies, ctx, DEFAULT_ACCT);

String managedArn = AwsManagedPolicies.ARN_PREFIX + "/service-role/AWSLambdaBasicExecutionRole";
String customerArn = "arn:aws:iam::" + REQUEST_ACCT + ":policy/group-policy";
policies.putForAccount(REQUEST_ACCT, customerArn,
new IamPolicy("ANPAGRP000000001", "group-policy", "/", customerArn,
"grp", AwsManagedPolicies.PERMISSIVE_DOCUMENT));

IamGroup group = new IamGroup("AGPAGROUP0000001", "app-group", "/",
"arn:aws:iam::" + REQUEST_ACCT + ":group/app-group");
group.getAttachedPolicyArns().add(managedArn);
group.getAttachedPolicyArns().add(customerArn);
groups.putForAccount(REQUEST_ACCT, "app-group", group);

IamService service = new IamService(
new InMemoryStorage<>(), groups, new InMemoryStorage<>(),
policies,
new InMemoryStorage<>(), new InMemoryStorage<>(), new InMemoryStorage<>(),
new RegionResolver("us-east-1", DEFAULT_ACCT));

// ListAttachedGroupPolicies returns both the managed (catalog) and customer (scoped) policies.
List<IamPolicy> attached = service.listAttachedGroupPolicies("app-group", null);
assertEquals(2, attached.size());
assertTrue(attached.stream().anyMatch(p -> managedArn.equals(p.getArn())));
assertTrue(attached.stream().anyMatch(p -> customerArn.equals(p.getArn())));
}

@Test
void attachedManagedPolicyResolvesForRoleInNonDefaultAccountIncludingBoundary() {
Instance<RequestContext> ctx = requestContextFor(REQUEST_ACCT);
InMemoryStorage<String, IamRole> rawRoles = new InMemoryStorage<>();
AccountAwareStorageBackend<IamRole> roles = new AccountAwareStorageBackend<>(rawRoles, ctx, DEFAULT_ACCT);
AccountAwareStorageBackend<IamPolicy> policies =
new AccountAwareStorageBackend<>(new InMemoryStorage<>(), ctx, DEFAULT_ACCT);

String managedArn = AwsManagedPolicies.ARN_PREFIX + "/service-role/AWSLambdaBasicExecutionRole";
String boundaryArn = AwsManagedPolicies.ARN_PREFIX + "/PowerUserAccess";

IamRole role = new IamRole("AROLE00000000001", "task-role", "/",
"arn:aws:iam::" + REQUEST_ACCT + ":role/task-role", "{}");
role.getAttachedPolicyArns().add(managedArn);
role.setPermissionsBoundaryArn(boundaryArn);
roles.putForAccount(REQUEST_ACCT, "task-role", role);

IamService service = new IamService(
new InMemoryStorage<>(), new InMemoryStorage<>(), roles,
policies,
new InMemoryStorage<>(), new InMemoryStorage<>(), new InMemoryStorage<>(),
new RegionResolver("us-east-1", DEFAULT_ACCT));

// ListAttachedRolePolicies resolves the managed policy from the catalog for account 111...
List<IamPolicy> attached = service.listAttachedRolePolicies("task-role", null);
assertEquals(1, attached.size());
assertEquals(managedArn, attached.get(0).getArn());

// resolvePrincipalContext (collectRolePolicies + resolveRoleBoundaryDocument) resolves both
// the attached managed policy and the managed permissions boundary for a non-default account.
CallerContext caller = service.resolvePrincipalContext(
"arn:aws:iam::" + REQUEST_ACCT + ":role/task-role");
assertTrue(caller.identityPolicies().contains(AwsManagedPolicies.PERMISSIVE_DOCUMENT));
assertEquals(AwsManagedPolicies.PERMISSIVE_DOCUMENT, caller.boundaryPolicyDocument());
}
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.