aboutsummaryrefslogtreecommitdiffstats
path: root/controller-api
diff options
context:
space:
mode:
authorEirik Nygaard <eirik.nygaard@yahooinc.com>2022-05-24 13:53:41 +0200
committerEirik Nygaard <eirik.nygaard@yahooinc.com>2022-05-25 09:44:12 +0200
commit3f3507a56dfafe8e3eea8500ce36584642c71434 (patch)
tree863aa98dd28e0c55b22b85f5535c88542768639b /controller-api
parent9fc7979cfd0fc1659e9a91ffe7fc030b12f88cd5 (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.java89
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java29
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;