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 +++---- 2 files changed, 104 insertions(+), 14 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; -- cgit v1.2.3