com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess Maven / Gradle / Ivy
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.tenant;
import com.amazonaws.arn.Arn;
import com.yahoo.text.Text;
import java.util.List;
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_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());
}
private ArchiveAccess(Optional awsRole, Optional gcpMember) {
this.awsRole = awsRole;
this.gcpMember = gcpMember;
awsRole.ifPresent(role -> validateAWSIAMRole(role));
gcpMember.ifPresent(member -> validateGCPMember(member));
}
public ArchiveAccess withAWSRole(String role) {
return new ArchiveAccess(Optional.of(role), gcpMember());
}
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 static final Pattern ACCOUNT_ID_PATTERN = Pattern.compile("\\d{12}");
private static void validateAWSIAMRole(String role) {
if (role.length() > 100) {
throw new IllegalArgumentException("Invalid archive access role too long, must be 100 or less characters");
}
try {
var arn = Arn.fromString(role);
if (!arn.getPartition().equals("aws")) throw new IllegalArgumentException("Partition must be 'aws'");
if (!arn.getService().equals("iam")) throw new IllegalArgumentException("Service must be 'iam'");
var resourceType = arn.getResource().getResourceType();
if (resourceType == null) throw new IllegalArgumentException("Missing resource type - must be 'role' or 'user'");
if (!List.of("user", "role").contains(resourceType))
throw new IllegalArgumentException("Invalid resource type - must be either a 'role' or 'user'");
var accountId = arn.getAccountId();
if (!ACCOUNT_ID_PATTERN.matcher(accountId).matches())
throw new IllegalArgumentException("Account id must be a 12-digit number");
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(Text.format("Invalid archive access IAM role '%s': %s", role, e.getMessage()));
}
}
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);
}
}