diff options
author | Eirik Nygaard <eirik.nygaard@yahooinc.com> | 2022-05-24 13:53:41 +0200 |
---|---|---|
committer | Eirik Nygaard <eirik.nygaard@yahooinc.com> | 2022-05-25 09:44:12 +0200 |
commit | 3f3507a56dfafe8e3eea8500ce36584642c71434 (patch) | |
tree | 863aa98dd28e0c55b22b85f5535c88542768639b /controller-api | |
parent | 9fc7979cfd0fc1659e9a91ffe7fc030b12f88cd5 (diff) |
Setup structure needed to give a GCP member archive bucket access
Diffstat (limited to 'controller-api')
-rw-r--r-- | controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/ArchiveAccess.java | 89 | ||||
-rw-r--r-- | controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java | 29 |
2 files changed, 104 insertions, 14 deletions
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("(?<prefix>[a-zA-Z]+):.+"); + + private static final Set<String> gcpMemberPrefixes = Set.of("user", "serviceAccount", "group", "domain"); + + // AWS IAM Role + private final Optional<String> awsRole; + // GCP Member + private final Optional<String> gcpMember; + + public ArchiveAccess() { + this(Optional.empty(), Optional.empty()); + } + + public ArchiveAccess(Optional<String> awsRole, Optional<String> 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<String> awsRole() { + return awsRole; + } + + public Optional<String> 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<Principal> creator; private final BiMap<PublicKey, Principal> developerKeys; private final TenantInfo info; private final List<TenantSecretStore> tenantSecretStores; - private final Optional<String> archiveAccessRole; + private final ArchiveAccess archiveAccess; /** Public for the serialization layer — do not use! */ public CloudTenant(TenantName name, Instant createdAt, LastLoginInfo lastLoginInfo, Optional<Principal> creator, BiMap<PublicKey, Principal> developerKeys, TenantInfo info, - List<TenantSecretStore> tenantSecretStores, Optional<String> archiveAccessRole) { + List<TenantSecretStore> 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<String> 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; |