From 9c102a86a66e438c00878a1ef3d91f1fbc2282fe Mon Sep 17 00:00:00 2001 From: Eirik Nygaard Date: Wed, 18 May 2022 11:49:38 +0200 Subject: Move zone and system specific settings to ArchiveService implementation --- .../controller/api/integration/archive/ArchiveService.java | 5 +++++ .../api/integration/archive/MockArchiveService.java | 11 +++++++++++ 2 files changed, 16 insertions(+) (limited to 'controller-api') diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveService.java index 69eda662692..389d815249d 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveService.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveService.java @@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.api.integration.archive; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; +import java.net.URI; import java.util.Map; import java.util.Set; @@ -20,4 +21,8 @@ public interface ArchiveService { void updateBucketPolicy(ZoneId zoneId, ArchiveBucket bucket, Map authorizeIamRoleByTenantName); void updateKeyPolicy(ZoneId zoneId, String keyArn, Set tenantAuthorizedIamRoles); + + boolean canAddTenantToBucket(ZoneId zoneId, ArchiveBucket bucket); + + URI bucketURI(ZoneId zoneId, String bucketName, TenantName tenantName); } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/MockArchiveService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/MockArchiveService.java index ce7b56ad1f6..1db003f8067 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/MockArchiveService.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/MockArchiveService.java @@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.api.integration.archive; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; +import java.net.URI; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -32,4 +33,14 @@ public class MockArchiveService implements ArchiveService { public void updateKeyPolicy(ZoneId zoneId, String keyArn, Set tenantAuthorizedIamRoles) { authorizedIamRolesForKey.put(keyArn, tenantAuthorizedIamRoles); } + + @Override + public boolean canAddTenantToBucket(ZoneId zoneId, ArchiveBucket bucket) { + return bucket.tenants().size() < 5; + } + + @Override + public URI bucketURI(ZoneId zoneId, String bucketName, TenantName tenantName) { + return URI.create(String.format("s3://%s/%s/", bucketName, tenantName.value())); + } } -- cgit v1.2.3 From 7a593da6545aa6a1e8022e774e904f866bca4e49 Mon Sep 17 00:00:00 2001 From: Harald Musum Date: Mon, 23 May 2022 11:24:35 +0200 Subject: Fix typo --- .../hosted/controller/api/systemflags/v1/SystemFlagsDataArchive.java | 2 +- .../controller/api/systemflags/v1/SystemFlagsDataArchiveTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'controller-api') diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchive.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchive.java index a739a8e2b01..60950341a42 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchive.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchive.java @@ -177,7 +177,7 @@ public class SystemFlagsDataArchive { if (!filenamesForSystem.isEmpty() && !filenamesForSystem.contains(filename)) { if (systemDefinition != null && filename.startsWith(systemDefinition.system().value() + '.')) { throw new IllegalArgumentException(String.format( - "Environment or zone in filename '%s' is does not exist", filename)); + "Environment or zone in filename '%s' does not exist", filename)); } return; // Ignore files irrelevant for system } diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchiveTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchiveTest.java index e4c46cfed1a..ce2746f92e0 100644 --- a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchiveTest.java +++ b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchiveTest.java @@ -135,7 +135,7 @@ public class SystemFlagsDataArchiveTest { Path directory = Paths.get("src/test/resources/system-flags-with-unknown-file-name/"); expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage( - "Environment or zone in filename 'main.prod.unknown-region.json' is does not exist"); + "Environment or zone in filename 'main.prod.unknown-region.json' does not exist"); SystemFlagsDataArchive.fromDirectoryAndSystem(directory, createZoneRegistryMock()); } -- cgit v1.2.3 From 3f3507a56dfafe8e3eea8500ce36584642c71434 Mon Sep 17 00:00:00 2001 From: Eirik Nygaard Date: Tue, 24 May 2022 13:53:41 +0200 Subject: Setup structure needed to give a GCP member archive bucket access --- .../hosted/controller/tenant/ArchiveAccess.java | 89 ++++++++++++++++++++++ .../hosted/controller/tenant/CloudTenant.java | 29 +++---- .../vespa/hosted/controller/LockedTenant.java | 28 +++---- .../controller/persistence/TenantSerializer.java | 31 +++++++- .../restapi/application/ApplicationApiHandler.java | 5 +- .../maintenance/ArchiveAccessMaintainerTest.java | 3 +- .../notification/NotificationsDbTest.java | 3 +- .../persistence/TenantSerializerTest.java | 54 ++++++++++++- .../restapi/filter/SignatureFilterTest.java | 5 +- 9 files changed, 208 insertions(+), 39 deletions(-) create mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/ArchiveAccess.java (limited to 'controller-api') diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/ArchiveAccess.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/ArchiveAccess.java new file mode 100644 index 00000000000..6336fddb8be --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/ArchiveAccess.java @@ -0,0 +1,89 @@ +package com.yahoo.vespa.hosted.controller.tenant; + +import com.yahoo.text.Text; + +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class ArchiveAccess { + + private static final Pattern VALID_AWS_ARCHIVE_ACCESS_ROLE_PATTERN = Pattern.compile("arn:aws:iam::\\d{12}:.+"); + private static final Pattern VALID_GCP_ARCHIVE_ACCESS_MEMBER_PATTERN = Pattern.compile("(?[a-zA-Z]+):.+"); + + private static final Set gcpMemberPrefixes = Set.of("user", "serviceAccount", "group", "domain"); + + // AWS IAM Role + private final Optional awsRole; + // GCP Member + private final Optional gcpMember; + + public ArchiveAccess() { + this(Optional.empty(), Optional.empty()); + } + + public ArchiveAccess(Optional awsRole, Optional gcpMember) { + this.awsRole = awsRole; + this.gcpMember = gcpMember; + + awsRole.ifPresent(role -> validateAWSIAMRole(role)); + gcpMember.ifPresent(member -> validateGCPMember(member)); + } + + public static ArchiveAccess fromAWSRole(String role) { + return new ArchiveAccess(Optional.of(role), Optional.empty()); + } + + public static ArchiveAccess fromGCPMember(String member) { + return new ArchiveAccess(Optional.empty(), Optional.of(member)); + } + + private void validateAWSIAMRole(String role) { + if (!VALID_AWS_ARCHIVE_ACCESS_ROLE_PATTERN.matcher(role).matches()) { + throw new IllegalArgumentException(Text.format("Invalid archive access role '%s': Must match expected pattern: '%s'", + awsRole.get(), VALID_AWS_ARCHIVE_ACCESS_ROLE_PATTERN.pattern())); + } + if (role.length() > 100) { + throw new IllegalArgumentException("Invalid archive access role too long, must be 100 or less characters"); + } + } + + private void validateGCPMember(String member) { + var matcher = VALID_GCP_ARCHIVE_ACCESS_MEMBER_PATTERN.matcher(member); + if (!matcher.matches()) { + throw new IllegalArgumentException(Text.format("Invalid GCP archive access member '%s': Must match expected pattern: '%s'", + gcpMember.get(), VALID_GCP_ARCHIVE_ACCESS_MEMBER_PATTERN.pattern())); + } + var prefix = matcher.group("prefix"); + if (!gcpMemberPrefixes.contains(prefix)) { + throw new IllegalArgumentException(Text.format("Invalid GCP member prefix '%s', must be one of '%s'", + prefix, gcpMemberPrefixes.stream().collect(Collectors.joining(", ")))); + } + if (!"domain".equals(prefix) && !member.contains("@")) { + throw new IllegalArgumentException(Text.format("Invalid GCP member '%s', prefix '%s' must be followed by an email id", member, prefix)); + } + } + + public Optional awsRole() { + return awsRole; + } + + public Optional gcpMember() { + return gcpMember; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ArchiveAccess that = (ArchiveAccess) o; + return awsRole.equals(that.awsRole) && gcpMember.equals(that.gcpMember); + } + + @Override + public int hashCode() { + return Objects.hash(awsRole, gcpMember); + } +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java index 9a4e04ebb3a..953468a28a7 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java @@ -4,7 +4,6 @@ package com.yahoo.vespa.hosted.controller.tenant; import com.google.common.collect.BiMap; import com.google.common.collect.ImmutableBiMap; import com.yahoo.config.provision.TenantName; -import com.yahoo.text.Text; import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore; import java.security.Principal; @@ -13,7 +12,6 @@ import java.time.Instant; import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.regex.Pattern; /** * A paying tenant in a Vespa cloud service. @@ -22,29 +20,22 @@ import java.util.regex.Pattern; */ public class CloudTenant extends Tenant { - private static final Pattern VALID_ARCHIVE_ACCESS_ROLE_PATTERN = Pattern.compile("arn:aws:iam::\\d{12}:.+"); - private final Optional creator; private final BiMap developerKeys; private final TenantInfo info; private final List tenantSecretStores; - private final Optional archiveAccessRole; + private final ArchiveAccess archiveAccess; /** Public for the serialization layer — do not use! */ public CloudTenant(TenantName name, Instant createdAt, LastLoginInfo lastLoginInfo, Optional creator, BiMap developerKeys, TenantInfo info, - List tenantSecretStores, Optional archiveAccessRole) { + List tenantSecretStores, ArchiveAccess archiveAccess) { super(name, createdAt, lastLoginInfo, Optional.empty()); this.creator = creator; this.developerKeys = developerKeys; this.info = Objects.requireNonNull(info); this.tenantSecretStores = tenantSecretStores; - this.archiveAccessRole = archiveAccessRole; - if (!archiveAccessRole.map(role -> VALID_ARCHIVE_ACCESS_ROLE_PATTERN.matcher(role).matches()).orElse(true)) - throw new IllegalArgumentException(Text.format("Invalid archive access role '%s': Must match expected pattern: '%s'", - archiveAccessRole.get(), VALID_ARCHIVE_ACCESS_ROLE_PATTERN.pattern())); - if (archiveAccessRole.map(role -> role.length() > 100).orElse(false)) - throw new IllegalArgumentException("Invalid archive access role too long, must be 100 or less characters"); + this.archiveAccess = Objects.requireNonNull(archiveAccess); } /** Creates a tenant with the given name, provided it passes validation. */ @@ -53,7 +44,7 @@ public class CloudTenant extends Tenant { createdAt, LastLoginInfo.EMPTY, Optional.ofNullable(creator), - ImmutableBiMap.of(), TenantInfo.empty(), List.of(), Optional.empty()); + ImmutableBiMap.of(), TenantInfo.empty(), List.of(), new ArchiveAccess()); } /** The user that created the tenant */ @@ -68,7 +59,7 @@ public class CloudTenant extends Tenant { /** An iam role which is allowed to access the S3 (log, dump) archive) */ public Optional archiveAccessRole() { - return archiveAccessRole; + return archiveAccess.awsRole(); } /** Returns the set of developer keys and their corresponding developers for this tenant. */ @@ -79,6 +70,16 @@ public class CloudTenant extends Tenant { return tenantSecretStores; } + /** + * Returns archive access archive bucket access string + * + * For AWS is this the IAM role + * For GCP it is a Google Workspace group + */ + public ArchiveAccess archiveAccess() { + return archiveAccess; + } + @Override public Type type() { return Type.cloud; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java index 7a0e60aacb4..4f58e87035b 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java @@ -8,11 +8,11 @@ import com.yahoo.config.provision.TenantName; import com.yahoo.security.KeyUtils; import com.yahoo.transaction.Mutex; import com.yahoo.vespa.athenz.api.AthenzDomain; -import com.yahoo.vespa.curator.Lock; import com.yahoo.vespa.hosted.controller.api.identifiers.Property; import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact; import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore; +import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess; import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import com.yahoo.vespa.hosted.controller.tenant.DeletedTenant; @@ -128,26 +128,26 @@ public abstract class LockedTenant { private final BiMap developerKeys; private final TenantInfo info; private final List tenantSecretStores; - private final Optional archiveAccessRole; + private final ArchiveAccess archiveAccess; private Cloud(TenantName name, Instant createdAt, LastLoginInfo lastLoginInfo, Optional creator, BiMap developerKeys, TenantInfo info, - List tenantSecretStores, Optional archiveAccessRole) { + List tenantSecretStores, ArchiveAccess archiveAccess) { super(name, createdAt, lastLoginInfo); this.developerKeys = ImmutableBiMap.copyOf(developerKeys); this.creator = creator; this.info = info; this.tenantSecretStores = tenantSecretStores; - this.archiveAccessRole = archiveAccessRole; + this.archiveAccess = archiveAccess; } private Cloud(CloudTenant tenant) { - this(tenant.name(), tenant.createdAt(), tenant.lastLoginInfo(), tenant.creator(), tenant.developerKeys(), tenant.info(), tenant.tenantSecretStores(), tenant.archiveAccessRole()); + this(tenant.name(), tenant.createdAt(), tenant.lastLoginInfo(), tenant.creator(), tenant.developerKeys(), tenant.info(), tenant.tenantSecretStores(), tenant.archiveAccess()); } @Override public CloudTenant get() { - return new CloudTenant(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccessRole); + return new CloudTenant(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess); } public Cloud withDeveloperKey(PublicKey key, Principal principal) { @@ -155,38 +155,38 @@ public abstract class LockedTenant { if (keys.containsKey(key)) throw new IllegalArgumentException("Key " + KeyUtils.toPem(key) + " is already owned by " + keys.get(key)); keys.put(key, principal); - return new Cloud(name, createdAt, lastLoginInfo, creator, keys, info, tenantSecretStores, archiveAccessRole); + return new Cloud(name, createdAt, lastLoginInfo, creator, keys, info, tenantSecretStores, archiveAccess); } public Cloud withoutDeveloperKey(PublicKey key) { BiMap keys = HashBiMap.create(developerKeys); keys.remove(key); - return new Cloud(name, createdAt, lastLoginInfo, creator, keys, info, tenantSecretStores, archiveAccessRole); + return new Cloud(name, createdAt, lastLoginInfo, creator, keys, info, tenantSecretStores, archiveAccess); } public Cloud withInfo(TenantInfo newInfo) { - return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, newInfo, tenantSecretStores, archiveAccessRole); + return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, newInfo, tenantSecretStores, archiveAccess); } @Override public LockedTenant with(LastLoginInfo lastLoginInfo) { - return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccessRole); + return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess); } public Cloud withSecretStore(TenantSecretStore tenantSecretStore) { ArrayList secretStores = new ArrayList<>(tenantSecretStores); secretStores.add(tenantSecretStore); - return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, secretStores, archiveAccessRole); + return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, secretStores, archiveAccess); } public Cloud withoutSecretStore(TenantSecretStore tenantSecretStore) { ArrayList secretStores = new ArrayList<>(tenantSecretStores); secretStores.remove(tenantSecretStore); - return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, secretStores, archiveAccessRole); + return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, secretStores, archiveAccess); } - public Cloud withArchiveAccessRole(Optional role) { - return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, role); + public Cloud withArchiveAccess(ArchiveAccess archiveAccess) { + return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess); } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java index 00e38abcba7..f00e179cfcf 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java @@ -17,6 +17,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.organization.BillingInf import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact; import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore; import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal; +import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess; import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import com.yahoo.vespa.hosted.controller.tenant.DeletedTenant; @@ -77,6 +78,10 @@ public class TenantSerializer { private static final String lastLoginInfoField = "lastLoginInfo"; private static final String secretStoresField = "secretStores"; private static final String archiveAccessRoleField = "archiveAccessRole"; + private static final String archiveAccessField = "archiveAccess"; + private static final String awsArchiveAccessRoleField = "awsArchiveAccessRole"; + private static final String gcpArchiveAccessMemberField = "gcpArchiveAccessMember"; + private static final String awsIdField = "awsId"; private static final String roleField = "role"; @@ -117,7 +122,13 @@ public class TenantSerializer { toSlime(legacyBillingInfo, root.setObject(billingInfoField)); toSlime(tenant.info(), root); toSlime(tenant.tenantSecretStores(), root); - tenant.archiveAccessRole().ifPresent(role -> root.setString(archiveAccessRoleField, role)); + toSlime(tenant.archiveAccess(), root); + } + + private void toSlime(ArchiveAccess archiveAccess, Cursor root) { + Cursor object = root.setObject(archiveAccessField); + archiveAccess.awsRole().ifPresent(role -> object.setString(awsArchiveAccessRoleField, role)); + archiveAccess.gcpMember().ifPresent(member -> object.setString(gcpArchiveAccessMemberField, member)); } private void toSlime(DeletedTenant tenant, Cursor root) { @@ -175,8 +186,8 @@ public class TenantSerializer { BiMap developerKeys = developerKeysFromSlime(tenantObject.field(pemDeveloperKeysField)); TenantInfo info = tenantInfoFromSlime(tenantObject.field(tenantInfoField)); List tenantSecretStores = secretStoresFromSlime(tenantObject.field(secretStoresField)); - Optional archiveAccessRole = SlimeUtils.optionalString(tenantObject.field(archiveAccessRoleField)); - return new CloudTenant(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccessRole); + ArchiveAccess archiveAccess = archiveAccessFromSlime(tenantObject); + return new CloudTenant(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess); } private DeletedTenant deletedTenantFrom(Inspector tenantObject) { @@ -195,6 +206,20 @@ public class TenantSerializer { return keys.build(); } + ArchiveAccess archiveAccessFromSlime(Inspector tenantObject) { + // TODO(enygaard, 2022-05-24): Remove when all tenants have been rewritten to use ArchiveAccess object + Optional archiveAccessRole = SlimeUtils.optionalString(tenantObject.field(archiveAccessRoleField)); + if (archiveAccessRole.isPresent()) { + return new ArchiveAccess(archiveAccessRole, Optional.empty()); + } + Inspector object = tenantObject.field(archiveAccessField); + if (!object.valid()) { + return new ArchiveAccess(); + } + Optional awsArchiveAccessRole = SlimeUtils.optionalString(object.field(awsArchiveAccessRoleField)); + Optional gcpArchiveAccessMember = SlimeUtils.optionalString(object.field(gcpArchiveAccessMemberField)); + return new ArchiveAccess(awsArchiveAccessRole, gcpArchiveAccessMember); + } TenantInfo tenantInfoFromSlime(Inspector infoObject) { if (!infoObject.valid()) return TenantInfo.empty(); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java index 91b76ac8d05..0c564a51f37 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java @@ -108,6 +108,7 @@ import com.yahoo.vespa.hosted.controller.routing.rotation.RotationStatus; import com.yahoo.vespa.hosted.controller.security.AccessControlRequests; import com.yahoo.vespa.hosted.controller.security.Credentials; import com.yahoo.vespa.hosted.controller.support.access.SupportAccess; +import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess; import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import com.yahoo.vespa.hosted.controller.tenant.DeletedTenant; @@ -1041,7 +1042,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { } controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, lockedTenant -> { - lockedTenant = lockedTenant.withArchiveAccessRole(Optional.of(role)); + lockedTenant = lockedTenant.withArchiveAccess(new ArchiveAccess(Optional.of(role), Optional.empty())); controller.tenants().store(lockedTenant); }); @@ -1053,7 +1054,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant"); controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, lockedTenant -> { - lockedTenant = lockedTenant.withArchiveAccessRole(Optional.empty()); + lockedTenant = lockedTenant.withArchiveAccess(new ArchiveAccess()); controller.tenants().store(lockedTenant); }); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainerTest.java index b97743f4d44..12418656c2f 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainerTest.java @@ -9,6 +9,7 @@ import com.yahoo.vespa.hosted.controller.ControllerTester; import com.yahoo.vespa.hosted.controller.LockedTenant; import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBucket; import com.yahoo.vespa.hosted.controller.api.integration.archive.MockArchiveService; +import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess; import com.yahoo.vespa.hosted.controller.tenant.Tenant; import org.junit.Test; @@ -59,7 +60,7 @@ public class ArchiveAccessMaintainerTest { private TenantName createTenantWithAccessRole(ControllerTester tester, String tenantName, String role) { var tenant = tester.createTenant(tenantName, Tenant.Type.cloud); tester.controller().tenants().lockOrThrow(tenant, LockedTenant.Cloud.class, lockedTenant -> { - lockedTenant = lockedTenant.withArchiveAccessRole(Optional.of(role)); + lockedTenant = lockedTenant.withArchiveAccess(new ArchiveAccess(Optional.of(role), Optional.empty())); tester.controller().tenants().store(lockedTenant); }); return tenant; diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDbTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDbTest.java index 75dbebe96ff..5666f8bafd8 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDbTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDbTest.java @@ -20,6 +20,7 @@ import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext; import com.yahoo.vespa.hosted.controller.integration.ZoneRegistryMock; import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb; +import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess; import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo; import com.yahoo.vespa.hosted.controller.tenant.TenantContacts; @@ -60,7 +61,7 @@ public class NotificationsDbTest { List.of(TenantContacts.Audience.NOTIFICATIONS), email)))), List.of(), - Optional.empty()); + new ArchiveAccess()); private static final List notifications = List.of( notification(1001, Type.deployment, Level.error, NotificationSource.from(tenant), "tenant msg"), notification(1101, Type.applicationPackage, Level.warning, NotificationSource.from(TenantAndApplicationId.from(tenant.value(), "app1")), "app msg"), diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java index e0d14f19f21..431cfbbcbad 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java @@ -6,12 +6,14 @@ import com.yahoo.config.provision.TenantName; import com.yahoo.security.KeyUtils; import com.yahoo.slime.Cursor; import com.yahoo.slime.Slime; +import com.yahoo.slime.SlimeUtils; import com.yahoo.vespa.athenz.api.AthenzDomain; import com.yahoo.vespa.hosted.controller.api.identifiers.Property; import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact; import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore; import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal; +import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess; import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import com.yahoo.vespa.hosted.controller.tenant.DeletedTenant; @@ -101,7 +103,7 @@ public class TenantSerializerTest { otherPublicKey, new SimplePrincipal("jane")), TenantInfo.empty(), List.of(), - Optional.empty() + new ArchiveAccess() ); CloudTenant serialized = (CloudTenant) serializer.tenantFrom(serializer.toSlime(tenant)); assertEquals(tenant.name(), serialized.name()); @@ -123,13 +125,61 @@ public class TenantSerializerTest { new TenantSecretStore("ss1", "123", "role1"), new TenantSecretStore("ss2", "124", "role2") ), - Optional.of("arn:aws:iam::123456789012:role/my-role") + new ArchiveAccess(Optional.of("arn:aws:iam::123456789012:role/my-role"), Optional.empty()) ); CloudTenant serialized = (CloudTenant) serializer.tenantFrom(serializer.toSlime(tenant)); assertEquals(tenant.info(), serialized.info()); assertEquals(tenant.tenantSecretStores(), serialized.tenantSecretStores()); } + @Test + public void cloud_tenant_with_old_archive_access_serialization() { + var json = "{\n" + + " \"name\": \"elderly-lady\",\n" + + " \"type\": \"cloud\",\n" + + " \"createdAt\": 1234,\n" + + " \"lastLoginInfo\": {\n" + + " \"user\": 123,\n" + + " \"developer\": 456\n" + + " },\n" + + " \"creator\": \"foobar-user\",\n" + + " \"pemDeveloperKeys\": [\n" + + " {\n" + + " \"key\": \"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\\n-----END PUBLIC KEY-----\\n\",\n" + + " \"user\": \"joe\"\n" + + " },\n" + + " {\n" + + " \"key\": \"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\\npDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\\n-----END PUBLIC KEY-----\\n\",\n" + + " \"user\": \"jane\"\n" + + " }\n" + + " ],\n" + + " \"billingInfo\": {\n" + + " \"customerId\": \"customer\",\n" + + " \"productCode\": \"Vespa\"\n" + + " },\n" + + " \"archiveAccessRole\": \"arn:aws:iam::123456789012:role/my-role\"\n" + + "}"; + var tenant = (CloudTenant) serializer.tenantFrom(SlimeUtils.jsonToSlime(json)); + assertEquals("arn:aws:iam::123456789012:role/my-role", tenant.archiveAccess().awsRole().get()); + assertFalse(tenant.archiveAccess().gcpMember().isPresent()); + } + + @Test + public void cloud_tenant_with_archive_access() { + CloudTenant tenant = new CloudTenant(TenantName.from("elderly-lady"), + Instant.ofEpochMilli(1234L), + lastLoginInfo(123L, 456L, null), + Optional.of(new SimplePrincipal("foobar-user")), + ImmutableBiMap.of(publicKey, new SimplePrincipal("joe"), + otherPublicKey, new SimplePrincipal("jane")), + TenantInfo.empty(), + List.of(), + new ArchiveAccess(Optional.of("arn:aws:iam::123456789012:role/my-role"), Optional.of("user:foo@example.com")) + ); + CloudTenant serialized = (CloudTenant) serializer.tenantFrom(serializer.toSlime(tenant)); + assertEquals(serialized.archiveAccess().awsRole().get(), "arn:aws:iam::123456789012:role/my-role"); + assertEquals(serialized.archiveAccess().gcpMember().get(), "user:foo@example.com"); + } @Test public void cloud_tenant_with_tenant_info_partial() { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java index 15c7dbf73ab..9024d7c8e7e 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java @@ -16,6 +16,7 @@ import com.yahoo.vespa.hosted.controller.api.role.SecurityContext; import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal; import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.restapi.ApplicationRequestToDiscFilterRequestWrapper; +import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess; import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo; import com.yahoo.vespa.hosted.controller.tenant.TenantInfo; @@ -76,7 +77,7 @@ public class SignatureFilterTest { ImmutableBiMap.of(), TenantInfo.empty(), List.of(), - Optional.empty())); + new ArchiveAccess())); tester.curator().writeApplication(new Application(appId, tester.clock().instant())); } @@ -122,7 +123,7 @@ public class SignatureFilterTest { ImmutableBiMap.of(publicKey, () -> "user"), TenantInfo.empty(), List.of(), - Optional.empty())); + new ArchiveAccess())); verifySecurityContext(requestOf(signer.signed(request.copy(), Method.POST, () -> new ByteArrayInputStream(hiBytes)), hiBytes), new SecurityContext(new SimplePrincipal("user"), Set.of(Role.reader(id.tenant()), -- cgit v1.2.3 From 340bdc4f860e934f1a3eb11084661c13900bdb28 Mon Sep 17 00:00:00 2001 From: Eirik Nygaard Date: Tue, 24 May 2022 15:45:46 +0200 Subject: Use ArchiveAccess instead of directly accessing AWS IAM role --- .../api/integration/archive/ArchiveService.java | 5 ++- .../integration/archive/MockArchiveService.java | 18 +++++----- .../hosted/controller/tenant/CloudTenant.java | 9 ++--- .../maintenance/ArchiveAccessMaintainer.java | 38 +++++++--------------- .../restapi/application/ApplicationApiHandler.java | 2 +- .../maintenance/ArchiveAccessMaintainerTest.java | 10 +++--- 6 files changed, 31 insertions(+), 51 deletions(-) (limited to 'controller-api') diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveService.java index 389d815249d..46e7fb48553 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveService.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveService.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.api.integration.archive; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; +import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess; import java.net.URI; import java.util.Map; @@ -18,9 +19,7 @@ public interface ArchiveService { ArchiveBucket createArchiveBucketFor(ZoneId zoneId); - void updateBucketPolicy(ZoneId zoneId, ArchiveBucket bucket, Map authorizeIamRoleByTenantName); - - void updateKeyPolicy(ZoneId zoneId, String keyArn, Set tenantAuthorizedIamRoles); + void updatePolicies(ZoneId zoneId, Set buckets, Map authorizeAccessByTenantName); boolean canAddTenantToBucket(ZoneId zoneId, ArchiveBucket bucket); diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/MockArchiveService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/MockArchiveService.java index 1db003f8067..a2847439ce7 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/MockArchiveService.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/MockArchiveService.java @@ -3,9 +3,11 @@ package com.yahoo.vespa.hosted.controller.api.integration.archive; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; +import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess; import java.net.URI; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.TreeMap; @@ -16,8 +18,10 @@ import java.util.TreeMap; */ public class MockArchiveService implements ArchiveService { - public Map> authorizedIamRolesForBucket = new HashMap<>(); - public Map> authorizedIamRolesForKey = new TreeMap<>(); + + public Set archiveBuckets = new HashSet<>(); + public Map authorizeAccessByTenantName = new HashMap<>(); + @Override public ArchiveBucket createArchiveBucketFor(ZoneId zoneId) { @@ -25,13 +29,9 @@ public class MockArchiveService implements ArchiveService { } @Override - public void updateBucketPolicy(ZoneId zoneId, ArchiveBucket bucket, Map authorizeIamRoleByTenantName) { - authorizedIamRolesForBucket.put(bucket, authorizeIamRoleByTenantName); - } - - @Override - public void updateKeyPolicy(ZoneId zoneId, String keyArn, Set tenantAuthorizedIamRoles) { - authorizedIamRolesForKey.put(keyArn, tenantAuthorizedIamRoles); + public void updatePolicies(ZoneId zoneId, Set buckets, Map authorizeAccessByTenantName) { + this.archiveBuckets = new HashSet<>(buckets); + this.authorizeAccessByTenantName = new HashMap<>(authorizeAccessByTenantName); } @Override diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java index 953468a28a7..54924b9c456 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java @@ -57,11 +57,6 @@ public class CloudTenant extends Tenant { return info; } - /** An iam role which is allowed to access the S3 (log, dump) archive) */ - public Optional archiveAccessRole() { - return archiveAccess.awsRole(); - } - /** Returns the set of developer keys and their corresponding developers for this tenant. */ public BiMap developerKeys() { return developerKeys; } @@ -71,10 +66,10 @@ public class CloudTenant extends Tenant { } /** - * Returns archive access archive bucket access string + * Role or member that is allowed to access archive bucket (log, dump) * * For AWS is this the IAM role - * For GCP it is a Google Workspace group + * For GCP it is a GCP member */ public ArchiveAccess archiveAccess() { return archiveAccess; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainer.java index 9691b45de7d..788360996ff 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainer.java @@ -10,6 +10,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBucket; import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveService; import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry; import com.yahoo.vespa.hosted.controller.archive.CuratorArchiveBucketDb; +import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess; import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import com.yahoo.vespa.hosted.controller.tenant.Tenant; @@ -53,41 +54,26 @@ public class ArchiveAccessMaintainer extends ControllerMaintainer { "cloud", z.getCloudName().value())))); zoneRegistry.zonesIncludingSystem().controllerUpgraded().zones().forEach(z -> { - ZoneId zoneId = z.getVirtualId(); - try { - var tenantArchiveAccessRoles = cloudTenantArchiveExternalAccessRoles(); - archiveBucketDb.buckets(zoneId).forEach(archiveBucket -> - archiveService.updateBucketPolicy(zoneId, archiveBucket, - Maps.filterEntries(tenantArchiveAccessRoles, - entry -> archiveBucket.tenants().contains(entry.getKey()))) - ); - Map> bucketsPerKey = archiveBucketDb.buckets(zoneId).stream() - .collect(groupingBy(ArchiveBucket::keyArn)); - bucketsPerKey.forEach((keyArn, buckets) -> { - Set authorizedIamRolesForKey = buckets.stream() - .flatMap(b -> b.tenants().stream()) - .filter(tenantArchiveAccessRoles::containsKey) - .map(tenantArchiveAccessRoles::get) - .collect(Collectors.toSet()); - archiveService.updateKeyPolicy(zoneId, keyArn, authorizedIamRolesForKey); - }); - } catch (Exception e) { - throw new RuntimeException("Failed to maintain archive access in " + zoneId.value(), e); - } - } - ); + ZoneId zoneId = z.getVirtualId(); + try { + var tenantArchiveAccessRoles = cloudTenantArchiveExternalAccessRoles(); + var buckets = archiveBucketDb.buckets(zoneId); + archiveService.updatePolicies(zoneId, buckets, tenantArchiveAccessRoles); + } catch (Exception e) { + throw new RuntimeException("Failed to maintain archive access in " + zoneId.value(), e); + } + }); return 1.0; } - private Map cloudTenantArchiveExternalAccessRoles() { + private Map cloudTenantArchiveExternalAccessRoles() { List tenants = controller().tenants().asList(); return tenants.stream() .filter(t -> t instanceof CloudTenant) .map(t -> (CloudTenant) t) - .filter(t -> t.archiveAccessRole().isPresent()) .collect(Collectors.toUnmodifiableMap( - Tenant::name, cloudTenant -> cloudTenant.archiveAccessRole().orElseThrow())); + Tenant::name, cloudTenant -> cloudTenant.archiveAccess())); } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java index 0c564a51f37..18cae83a131 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java @@ -2356,7 +2356,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { log.warning(String.format("Failed to get quota for tenant %s: %s", tenant.name(), Exceptions.toMessageString(e))); } - cloudTenant.archiveAccessRole().ifPresent(role -> object.setString("archiveAccessRole", role)); + cloudTenant.archiveAccess().awsRole().ifPresent(role -> object.setString("archiveAccessRole", role)); break; } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainerTest.java index 12418656c2f..b2451161f34 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainerTest.java @@ -34,19 +34,19 @@ public class ArchiveAccessMaintainerTest { String tenant1role = "arn:aws:iam::123456789012:role/my-role"; String tenant2role = "arn:aws:iam::210987654321:role/my-role"; var tenant1 = createTenantWithAccessRole(tester, "tenant1", tenant1role); - createTenantWithAccessRole(tester, "tenant2", tenant2role); + var tenant2 = createTenantWithAccessRole(tester, "tenant2", tenant2role); ZoneId testZone = ZoneId.from("prod.aws-us-east-1c"); tester.controller().archiveBucketDb().archiveUriFor(testZone, tenant1, true); var testBucket = new ArchiveBucket("bucketName", "keyArn").withTenant(tenant1); MockArchiveService archiveService = (MockArchiveService) tester.controller().serviceRegistry().archiveService(); - assertNull(archiveService.authorizedIamRolesForBucket.get(testBucket)); - assertNull(archiveService.authorizedIamRolesForKey.get(testBucket.keyArn())); + + assertEquals(0, archiveService.authorizeAccessByTenantName.size()); MockMetric metric = new MockMetric(); new ArchiveAccessMaintainer(tester.controller(), metric, Duration.ofMinutes(10)).maintain(); - assertEquals(Map.of(tenant1, tenant1role), archiveService.authorizedIamRolesForBucket.get(testBucket)); - assertEquals(Set.of(tenant1role), archiveService.authorizedIamRolesForKey.get(testBucket.keyArn())); + assertEquals(new ArchiveAccess(Optional.of(tenant1role), Optional.empty()), archiveService.authorizeAccessByTenantName.get(tenant1)); + assertEquals(new ArchiveAccess(Optional.of(tenant2role), Optional.empty()), archiveService.authorizeAccessByTenantName.get(tenant2)); var expected = Map.of("archive.bucketCount", tester.controller().zoneRegistry().zonesIncludingSystem().all().ids().stream() -- cgit v1.2.3 From 03b9ea8cc73793c427d2358e4f7b96cd8e3800cf Mon Sep 17 00:00:00 2001 From: Eirik Nygaard Date: Wed, 25 May 2022 14:20:53 +0200 Subject: Expose GCP archive member setting in API --- .../hosted/controller/api/role/PathGroup.java | 5 +- .../hosted/controller/tenant/ArchiveAccess.java | 26 +++++++-- .../controller/persistence/TenantSerializer.java | 6 +- .../restapi/application/ApplicationApiHandler.java | 64 +++++++++++++++++++--- .../maintenance/ArchiveAccessMaintainerTest.java | 6 +- .../persistence/TenantSerializerTest.java | 4 +- .../application/ApplicationApiCloudTest.java | 35 +++++++++++- .../restapi/user/responses/tenant-with-keys.json | 1 + .../user/responses/tenant-with-secrets.json | 1 + .../responses/tenant-without-applications.json | 1 + 10 files changed, 125 insertions(+), 24 deletions(-) (limited to 'controller-api') diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java index b5f03756777..f87da90f8ef 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java @@ -62,7 +62,10 @@ enum PathGroup { "/application/v4/tenant/{tenant}/key/"), tenantArchiveAccess(Matcher.tenant, - "/application/v4/tenant/{tenant}/archive-access"), + "/application/v4/tenant/{tenant}/archive-access", + "/application/v4/tenant/{tenant}/archive-access/aws", + "/application/v4/tenant/{tenant}/archive-access/gcp"), + billingToken(Matcher.tenant, "/billing/v1/tenant/{tenant}/token"), diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/ArchiveAccess.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/ArchiveAccess.java index 6336fddb8be..fba361f9223 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/ArchiveAccess.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/ArchiveAccess.java @@ -24,7 +24,7 @@ public class ArchiveAccess { this(Optional.empty(), Optional.empty()); } - public ArchiveAccess(Optional awsRole, Optional gcpMember) { + private ArchiveAccess(Optional awsRole, Optional gcpMember) { this.awsRole = awsRole; this.gcpMember = gcpMember; @@ -32,12 +32,28 @@ public class ArchiveAccess { gcpMember.ifPresent(member -> validateGCPMember(member)); } - public static ArchiveAccess fromAWSRole(String role) { - return new ArchiveAccess(Optional.of(role), Optional.empty()); + public ArchiveAccess withAWSRole(String role) { + return new ArchiveAccess(Optional.of(role), gcpMember()); } - public static ArchiveAccess fromGCPMember(String member) { - return new ArchiveAccess(Optional.empty(), Optional.of(member)); + public ArchiveAccess withGCPMember(String member) { + return new ArchiveAccess(awsRole(), Optional.of(member)); + } + + public ArchiveAccess withAWSRole(Optional role) { + return new ArchiveAccess(role, gcpMember()); + } + + public ArchiveAccess withGCPMember(Optional member) { + return new ArchiveAccess(awsRole(), member); + } + + public ArchiveAccess removeAWSRole() { + return new ArchiveAccess(Optional.empty(), gcpMember()); + } + + public ArchiveAccess removeGCPMember() { + return new ArchiveAccess(awsRole(), Optional.empty()); } private void validateAWSIAMRole(String role) { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java index f00e179cfcf..e7cf0c34511 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java @@ -210,7 +210,7 @@ public class TenantSerializer { // TODO(enygaard, 2022-05-24): Remove when all tenants have been rewritten to use ArchiveAccess object Optional archiveAccessRole = SlimeUtils.optionalString(tenantObject.field(archiveAccessRoleField)); if (archiveAccessRole.isPresent()) { - return new ArchiveAccess(archiveAccessRole, Optional.empty()); + return new ArchiveAccess().withAWSRole(archiveAccessRole.get()); } Inspector object = tenantObject.field(archiveAccessField); if (!object.valid()) { @@ -218,7 +218,9 @@ public class TenantSerializer { } Optional awsArchiveAccessRole = SlimeUtils.optionalString(object.field(awsArchiveAccessRoleField)); Optional gcpArchiveAccessMember = SlimeUtils.optionalString(object.field(gcpArchiveAccessMemberField)); - return new ArchiveAccess(awsArchiveAccessRole, gcpArchiveAccessMember); + return new ArchiveAccess() + .withAWSRole(awsArchiveAccessRole) + .withGCPMember(gcpArchiveAccessMember); } TenantInfo tenantInfoFromSlime(Inspector infoObject) { if (!infoObject.valid()) return TenantInfo.empty(); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java index 18391815499..b60c402895b 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java @@ -297,7 +297,9 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}/access/approve/operator")) return approveAccessRequest(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/access/managed/operator")) return addManagedAccess(path.get("tenant")); if (path.matches("/application/v4/tenant/{tenant}/info")) return updateTenantInfo(path.get("tenant"), request); - if (path.matches("/application/v4/tenant/{tenant}/archive-access")) return allowArchiveAccess(path.get("tenant"), request); + if (path.matches("/application/v4/tenant/{tenant}/archive-access")) return allowAwsArchiveAccess(path.get("tenant"), request); // TODO(enygaard, 2022-05-25) Remove when no longer used by console + if (path.matches("/application/v4/tenant/{tenant}/archive-access/aws")) return allowAwsArchiveAccess(path.get("tenant"), request); + if (path.matches("/application/v4/tenant/{tenant}/archive-access/gcp")) return allowGcpArchiveAccess(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/secret-store/{name}")) return addSecretStore(path.get("tenant"), path.get("name"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/global-rotation/override")) return setGlobalRotationOverride(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), false, request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/global-rotation/override")) return setGlobalRotationOverride(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), false, request); @@ -345,7 +347,9 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}")) return deleteTenant(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/access/managed/operator")) return removeManagedAccess(path.get("tenant")); if (path.matches("/application/v4/tenant/{tenant}/key")) return removeDeveloperKey(path.get("tenant"), request); - if (path.matches("/application/v4/tenant/{tenant}/archive-access")) return removeArchiveAccess(path.get("tenant")); + if (path.matches("/application/v4/tenant/{tenant}/archive-access")) return removeAwsArchiveAccess(path.get("tenant")); // TODO(enygaard, 2022-05-25) Remove when no longer used by console + if (path.matches("/application/v4/tenant/{tenant}/archive-access/aws")) return removeAwsArchiveAccess(path.get("tenant")); + if (path.matches("/application/v4/tenant/{tenant}/archive-access/gcp")) return removeGcpArchiveAccess(path.get("tenant")); if (path.matches("/application/v4/tenant/{tenant}/secret-store/{name}")) return deleteSecretStore(path.get("tenant"), path.get("name"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return deleteApplication(path.get("tenant"), path.get("application"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deployment")) return removeAllProdDeployments(path.get("tenant"), path.get("application")); @@ -1028,7 +1032,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { return new SlimeJsonResponse(slime); } - private HttpResponse allowArchiveAccess(String tenantName, HttpRequest request) { + private HttpResponse allowAwsArchiveAccess(String tenantName, HttpRequest request) { if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud) throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant"); @@ -1036,27 +1040,62 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { var role = mandatory("role", data).asString(); if (role.isBlank()) { - return ErrorResponse.badRequest("Archive access role can't be whitespace only"); + return ErrorResponse.badRequest("AWS archive access role can't be whitespace only"); } controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, lockedTenant -> { - lockedTenant = lockedTenant.withArchiveAccess(new ArchiveAccess(Optional.of(role), Optional.empty())); + var access = lockedTenant.get().archiveAccess(); + lockedTenant = lockedTenant.withArchiveAccess(access.withAWSRole(role)); controller.tenants().store(lockedTenant); }); - return new MessageResponse("Archive access role set to '" + role + "' for tenant " + tenantName + "."); + return new MessageResponse("AWS archive access role set to '" + role + "' for tenant " + tenantName + "."); } - private HttpResponse removeArchiveAccess(String tenantName) { + private HttpResponse removeAwsArchiveAccess(String tenantName) { if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud) throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant"); controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, lockedTenant -> { - lockedTenant = lockedTenant.withArchiveAccess(new ArchiveAccess()); + var access = lockedTenant.get().archiveAccess(); + lockedTenant = lockedTenant.withArchiveAccess(access.removeAWSRole()); controller.tenants().store(lockedTenant); }); - return new MessageResponse("Archive access role removed for tenant " + tenantName + "."); + return new MessageResponse("AWS archive access role removed for tenant " + tenantName + "."); + } + + private HttpResponse allowGcpArchiveAccess(String tenantName, HttpRequest request) { + if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud) + throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant"); + + var data = toSlime(request.getData()).get(); + var member = mandatory("member", data).asString(); + + if (member.isBlank()) { + return ErrorResponse.badRequest("GCP archive access role can't be whitespace only"); + } + + controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, lockedTenant -> { + var access = lockedTenant.get().archiveAccess(); + lockedTenant = lockedTenant.withArchiveAccess(access.withGCPMember(member)); + controller.tenants().store(lockedTenant); + }); + + return new MessageResponse("GCP archive access member set to '" + member + "' for tenant " + tenantName + "."); + } + + private HttpResponse removeGcpArchiveAccess(String tenantName) { + if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud) + throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant"); + + controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, lockedTenant -> { + var access = lockedTenant.get().archiveAccess(); + lockedTenant = lockedTenant.withArchiveAccess(access.removeGCPMember()); + controller.tenants().store(lockedTenant); + }); + + return new MessageResponse("GCP archive access member removed for tenant " + tenantName + "."); } private HttpResponse patchApplication(String tenantName, String applicationName, HttpRequest request) { @@ -2354,7 +2393,9 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { log.warning(String.format("Failed to get quota for tenant %s: %s", tenant.name(), Exceptions.toMessageString(e))); } + // TODO(enygaard, 2022-05-25) Remove when console is using new archive access structure cloudTenant.archiveAccess().awsRole().ifPresent(role -> object.setString("archiveAccessRole", role)); + toSlime(cloudTenant.archiveAccess(), object.setObject("archiveAccess")); break; } @@ -2385,6 +2426,11 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { tenantMetaDataToSlime(tenant, applications, object.setObject("metaData")); } + private void toSlime(ArchiveAccess archiveAccess, Cursor object) { + archiveAccess.awsRole().ifPresent(role -> object.setString("awsRole", role)); + archiveAccess.gcpMember().ifPresent(member -> object.setString("gcpMember", member)); + } + private void toSlime(Quota quota, QuotaUsage usage, Cursor object) { quota.budget().ifPresentOrElse( budget -> object.setDouble("budget", budget.doubleValue()), diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainerTest.java index b2451161f34..3535417c586 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainerTest.java @@ -45,8 +45,8 @@ public class ArchiveAccessMaintainerTest { assertEquals(0, archiveService.authorizeAccessByTenantName.size()); MockMetric metric = new MockMetric(); new ArchiveAccessMaintainer(tester.controller(), metric, Duration.ofMinutes(10)).maintain(); - assertEquals(new ArchiveAccess(Optional.of(tenant1role), Optional.empty()), archiveService.authorizeAccessByTenantName.get(tenant1)); - assertEquals(new ArchiveAccess(Optional.of(tenant2role), Optional.empty()), archiveService.authorizeAccessByTenantName.get(tenant2)); + assertEquals(new ArchiveAccess().withAWSRole(tenant1role), archiveService.authorizeAccessByTenantName.get(tenant1)); + assertEquals(new ArchiveAccess().withAWSRole(tenant2role), archiveService.authorizeAccessByTenantName.get(tenant2)); var expected = Map.of("archive.bucketCount", tester.controller().zoneRegistry().zonesIncludingSystem().all().ids().stream() @@ -60,7 +60,7 @@ public class ArchiveAccessMaintainerTest { private TenantName createTenantWithAccessRole(ControllerTester tester, String tenantName, String role) { var tenant = tester.createTenant(tenantName, Tenant.Type.cloud); tester.controller().tenants().lockOrThrow(tenant, LockedTenant.Cloud.class, lockedTenant -> { - lockedTenant = lockedTenant.withArchiveAccess(new ArchiveAccess(Optional.of(role), Optional.empty())); + lockedTenant = lockedTenant.withArchiveAccess(new ArchiveAccess().withAWSRole(role)); tester.controller().tenants().store(lockedTenant); }); return tenant; diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java index 431cfbbcbad..a9e633a78d6 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java @@ -125,7 +125,7 @@ public class TenantSerializerTest { new TenantSecretStore("ss1", "123", "role1"), new TenantSecretStore("ss2", "124", "role2") ), - new ArchiveAccess(Optional.of("arn:aws:iam::123456789012:role/my-role"), Optional.empty()) + new ArchiveAccess().withAWSRole("arn:aws:iam::123456789012:role/my-role") ); CloudTenant serialized = (CloudTenant) serializer.tenantFrom(serializer.toSlime(tenant)); assertEquals(tenant.info(), serialized.info()); @@ -174,7 +174,7 @@ public class TenantSerializerTest { otherPublicKey, new SimplePrincipal("jane")), TenantInfo.empty(), List.of(), - new ArchiveAccess(Optional.of("arn:aws:iam::123456789012:role/my-role"), Optional.of("user:foo@example.com")) + new ArchiveAccess().withAWSRole("arn:aws:iam::123456789012:role/my-role").withGCPMember("user:foo@example.com") ); CloudTenant serialized = (CloudTenant) serializer.tenantFrom(serializer.toSlime(tenant)); assertEquals(serialized.archiveAccess().awsRole().get(), "arn:aws:iam::123456789012:role/my-role"); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java index 5627a1dd2d3..1df87ffa7ad 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java @@ -315,9 +315,40 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest { .data("{\"role\":\"dummy\"}").roles(Role.administrator(tenantName)), "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Invalid archive access role 'dummy': Must match expected pattern: 'arn:aws:iam::\\\\d{12}:.+'\"}", 400); + tester.assertResponse(request("/application/v4/tenant/scoober/archive-access/aws", PUT) + .data("{\"role\":\"arn:aws:iam::123456789012:role/my-role\"}").roles(Role.administrator(tenantName)), + "{\"message\":\"AWS archive access role set to 'arn:aws:iam::123456789012:role/my-role' for tenant scoober.\"}", 200); + tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)), + (response) -> assertTrue(response.getBodyAsString().contains("\"archiveAccessRole\":\"arn:aws:iam::123456789012:role/my-role\"")), + 200); + tester.assertResponse(request("/application/v4/tenant/scoober/archive-access/aws", DELETE).roles(Role.administrator(tenantName)), + "{\"message\":\"AWS archive access role removed for tenant scoober.\"}", 200); + tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)), + (response) -> assertFalse(response.getBodyAsString().contains("\"archiveAccessRole\":\"arn:aws:iam::123456789012:role/my-role\"")), + 200); + + tester.assertResponse(request("/application/v4/tenant/scoober/archive-access/gcp", PUT) + .data("{\"member\":\"user:test@example.com\"}").roles(Role.administrator(tenantName)), + "{\"message\":\"GCP archive access member set to 'user:test@example.com' for tenant scoober.\"}", 200); + tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)), + (response) -> assertTrue(response.getBodyAsString().contains("\"gcpMember\":\"user:test@example.com\"")), + 200); + tester.assertResponse(request("/application/v4/tenant/scoober/archive-access/gcp", DELETE).roles(Role.administrator(tenantName)), + "{\"message\":\"GCP archive access member removed for tenant scoober.\"}", 200); + tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)), + (response) -> assertFalse(response.getBodyAsString().contains("\"gcpMember\":\"user:test@example.com\"")), + 200); + + tester.assertResponse(request("/application/v4/tenant/scoober/archive-access", PUT) + .data("{\"role\":\"arn:aws:iam::123456789012:role/my-role\"}").roles(Role.administrator(tenantName)), + "{\"message\":\"AWS archive access role set to 'arn:aws:iam::123456789012:role/my-role' for tenant scoober.\"}", 200); + tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)), + (response) -> assertTrue(response.getBodyAsString().contains("\"archiveAccessRole\":\"arn:aws:iam::123456789012:role/my-role\"")), + 200); + tester.assertResponse(request("/application/v4/tenant/scoober/archive-access", PUT) .data("{\"role\":\"arn:aws:iam::123456789012:role/my-role\"}").roles(Role.administrator(tenantName)), - "{\"message\":\"Archive access role set to 'arn:aws:iam::123456789012:role/my-role' for tenant scoober.\"}", 200); + "{\"message\":\"AWS archive access role set to 'arn:aws:iam::123456789012:role/my-role' for tenant scoober.\"}", 200); tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)), (response) -> assertTrue(response.getBodyAsString().contains("\"archiveAccessRole\":\"arn:aws:iam::123456789012:role/my-role\"")), 200); @@ -327,7 +358,7 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest { new File("deployment-cloud.json")); tester.assertResponse(request("/application/v4/tenant/scoober/archive-access", DELETE).roles(Role.administrator(tenantName)), - "{\"message\":\"Archive access role removed for tenant scoober.\"}", 200); + "{\"message\":\"AWS archive access role removed for tenant scoober.\"}", 200); tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)), (response) -> assertFalse(response.getBodyAsString().contains("archiveAccessRole")), 200); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json index 3237e99783d..f980f9231f3 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json @@ -24,6 +24,7 @@ "budgetUsed": 0.0, "clusterSize": 5 }, + "archiveAccess": { }, "applications": [ { "tenant": "my-tenant", diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-secrets.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-secrets.json index 0cc8ba2cd9e..1152033791b 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-secrets.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-secrets.json @@ -32,6 +32,7 @@ "budgetUsed": 0.0, "clusterSize": 5 }, + "archiveAccess": { }, "applications": [ { "tenant": "my-tenant", diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json index 3153c6e218a..631346181a1 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json @@ -15,6 +15,7 @@ "budgetUsed": 0.0, "clusterSize": 5 }, + "archiveAccess": { }, "applications": [ ], "metaData": { "createdAtMillis": 1600000000000 -- cgit v1.2.3