diff options
author | Bjørn Christian Seime <bjorncs@verizonmedia.com> | 2021-09-24 14:27:56 +0200 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@verizonmedia.com> | 2021-09-24 14:27:56 +0200 |
commit | da99f37106766020c73ea6efd18008354aceea7d (patch) | |
tree | 1035e758252752eaa9ea1ed8bbf11a919274b0fd /controller-api | |
parent | 1a598e45369d1fdcfd46756bd782306adf5a74d0 (diff) |
Move 'c.y.v.hosted.controller' to controller-api
Diffstat (limited to 'controller-api')
9 files changed, 646 insertions, 0 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java new file mode 100644 index 00000000000..7fa46031c98 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java @@ -0,0 +1,73 @@ +// Copyright 2018 Yahoo Holdings. 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.yahoo.config.provision.TenantName; +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 java.time.Instant; +import java.util.Objects; +import java.util.Optional; + +/** + * Represents an Athenz tenant in hosted Vespa. + * + * @author mpolden + */ +public class AthenzTenant extends Tenant { + + private final AthenzDomain domain; + private final Property property; + private final Optional<PropertyId> propertyId; + + /** + * This should only be used by serialization. + * Use {@link #create(TenantName, AthenzDomain, Property, Optional, Instant)}. + * */ + public AthenzTenant(TenantName name, AthenzDomain domain, Property property, Optional<PropertyId> propertyId, + Optional<Contact> contact, Instant createdAt, LastLoginInfo lastLoginInfo) { + super(name, createdAt, lastLoginInfo, contact); + this.domain = Objects.requireNonNull(domain, "domain must be non-null"); + this.property = Objects.requireNonNull(property, "property must be non-null"); + this.propertyId = Objects.requireNonNull(propertyId, "propertyId must be non-null"); + } + + /** Property name of this tenant */ + public Property property() { + return property; + } + + /** Property ID of the tenant, if any */ + public Optional<PropertyId> propertyId() { + return propertyId; + } + + /** Athenz domain of this tenant */ + public AthenzDomain domain() { + return domain; + } + + /** Returns true if tenant is in given domain */ + public boolean in(AthenzDomain domain) { + return this.domain.equals(domain); + } + + @Override + public String toString() { + return "athenz tenant '" + name() + "'"; + } + + /** Create a new Athenz tenant */ + public static AthenzTenant create(TenantName name, AthenzDomain domain, Property property, + Optional<PropertyId> propertyId, Instant createdAt) { + return new AthenzTenant(requireName(name), domain, property, propertyId, Optional.empty(), createdAt, LastLoginInfo.EMPTY); + } + + @Override + public Type type() { + return Type.athenz; + } + +} 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 new file mode 100644 index 00000000000..1060b118beb --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java @@ -0,0 +1,87 @@ +// Copyright 2020 Oath Inc. 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.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; +import java.security.PublicKey; +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. + * + * @author jonmv + */ +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; + + /** 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) { + 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"); + } + + /** Creates a tenant with the given name, provided it passes validation. */ + public static CloudTenant create(TenantName tenantName, Instant createdAt, Principal creator) { + return new CloudTenant(requireName(tenantName), + createdAt, + LastLoginInfo.EMPTY, + Optional.ofNullable(creator), + ImmutableBiMap.of(), TenantInfo.EMPTY, List.of(), Optional.empty()); + } + + /** The user that created the tenant */ + public Optional<Principal> creator() { + return creator; + } + + /** Legal name, addresses etc */ + public TenantInfo info() { + return info; + } + + /** An iam role which is allowed to access the S3 (log, dump) archive) */ + public Optional<String> archiveAccessRole() { + return archiveAccessRole; + } + + /** Returns the set of developer keys and their corresponding developers for this tenant. */ + public BiMap<PublicKey, Principal> developerKeys() { return developerKeys; } + + /** List of configured secret stores */ + public List<TenantSecretStore> tenantSecretStores() { + return tenantSecretStores; + } + + @Override + public Type type() { + return Type.cloud; + } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/DeletedTenant.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/DeletedTenant.java new file mode 100644 index 00000000000..cf6d73cb8f8 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/DeletedTenant.java @@ -0,0 +1,39 @@ +// Copyright Yahoo. 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.yahoo.config.provision.TenantName; + +import java.time.Instant; +import java.util.Objects; +import java.util.Optional; + +/** + * Represents a tenant that has been deleted. Exists to prevent creation of a new tenant with the same name. + * + * @author freva + */ +public class DeletedTenant extends Tenant { + + private final Instant deletedAt; + + public DeletedTenant(TenantName name, Instant createdAt, Instant deletedAt) { + super(name, createdAt, LastLoginInfo.EMPTY, Optional.empty()); + this.deletedAt = Objects.requireNonNull(deletedAt, "deletedAt must be non-null"); + } + + /** Instant when the tenant was deleted */ + public Instant deletedAt() { + return deletedAt; + } + + @Override + public String toString() { + return "deleted tenant '" + name() + "'"; + } + + @Override + public Type type() { + return Type.deleted; + } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/LastLoginInfo.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/LastLoginInfo.java new file mode 100644 index 00000000000..15f2f97e7d1 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/LastLoginInfo.java @@ -0,0 +1,55 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.tenant; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * @author freva + */ +public class LastLoginInfo { + + public static final LastLoginInfo EMPTY = new LastLoginInfo(Map.of()); + + private final Map<UserLevel, Instant> lastLoginByUserLevel; + + public LastLoginInfo(Map<UserLevel, Instant> lastLoginByUserLevel) { + this.lastLoginByUserLevel = Map.copyOf(lastLoginByUserLevel); + } + + public Optional<Instant> get(UserLevel userLevel) { + return Optional.ofNullable(lastLoginByUserLevel.get(userLevel)); + } + + /** + * Returns new instance with updated last login time if the given {@code loginAt} timestamp is after the current + * for the given {@code userLevel}, otherwise returns this + */ + public LastLoginInfo withLastLoginIfLater(UserLevel userLevel, Instant loginAt) { + Instant lastLogin = lastLoginByUserLevel.getOrDefault(userLevel, Instant.EPOCH); + if (loginAt.isAfter(lastLogin)) { + Map<UserLevel, Instant> lastLoginByUserLevel = new HashMap<>(this.lastLoginByUserLevel); + lastLoginByUserLevel.put(userLevel, loginAt); + return new LastLoginInfo(lastLoginByUserLevel); + } + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + LastLoginInfo lastLoginInfo = (LastLoginInfo) o; + return lastLoginByUserLevel.equals(lastLoginInfo.lastLoginByUserLevel); + } + + @Override + public int hashCode() { + return lastLoginByUserLevel.hashCode(); + } + + public enum UserLevel { user, developer, administrator }; +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java new file mode 100644 index 00000000000..80982d70107 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java @@ -0,0 +1,88 @@ +// Copyright 2018 Yahoo Holdings. 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.yahoo.config.provision.TenantName; +import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact; + +import java.time.Instant; +import java.util.Objects; +import java.util.Optional; + +/** + * A tenant in hosted Vespa. + * + * @author mpolden + */ +public abstract class Tenant { + + private final TenantName name; + private final Instant createdAt; + private final LastLoginInfo lastLoginInfo; + private final Optional<Contact> contact; + + Tenant(TenantName name, Instant createdAt, LastLoginInfo lastLoginInfo, Optional<Contact> contact) { + this.name = name; + this.createdAt = createdAt; + this.lastLoginInfo = lastLoginInfo; + this.contact = contact; + } + + /** Name of this tenant */ + public TenantName name() { + return name; + } + + /** Instant when the tenant was created */ + public Instant createdAt() { + return createdAt; + } + + /** Returns login information for this tenant */ + public LastLoginInfo lastLoginInfo() { + return lastLoginInfo; + } + + /** Contact information for this tenant */ + public Optional<Contact> contact() { + return contact; + } + + public abstract Type type(); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Tenant tenant = (Tenant) o; + return Objects.equals(name, tenant.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + public static TenantName requireName(TenantName name) { + if ( ! name.value().matches("^(?=.{1,20}$)[a-z](-?[a-z0-9]+)*$")) { + throw new IllegalArgumentException("New tenant or application names must start with a letter, may " + + "contain no more than 20 characters, and may only contain lowercase " + + "letters, digits or dashes, but no double-dashes."); + } + return name; + } + + + public enum Type { + + /** Tenant authenticated through Athenz. */ + athenz, + + /** Tenant authenticated through some cloud identity provider. */ + cloud, + + /** Tenant has been deleted. */ + deleted, + + } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfo.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfo.java new file mode 100644 index 00000000000..81c08e1083b --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfo.java @@ -0,0 +1,127 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.tenant; + +import java.util.Objects; + +/** + * Tenant information beyond technical tenant id and user authorizations. + * + * This info is used to capture generic support information and invoiced billing information. + * + * All fields are non null but strings can be empty + * + * @author smorgrav + */ +public class TenantInfo { + private final String name; + private final String email; + private final String website; + private final String contactName; + private final String contactEmail; + private final String invoiceEmail; + private final TenantInfoAddress address; + private final TenantInfoBillingContact billingContact; + + TenantInfo(String name, String email, String website, String contactName, String contactEmail, + String invoiceEmail, TenantInfoAddress address, TenantInfoBillingContact billingContact) { + this.name = Objects.requireNonNull(name); + this.email = Objects.requireNonNull(email); + this.website = Objects.requireNonNull(website); + this.contactName = Objects.requireNonNull(contactName); + this.contactEmail = Objects.requireNonNull(contactEmail); + this.invoiceEmail = Objects.requireNonNull(invoiceEmail); + this.address = Objects.requireNonNull(address); + this.billingContact = Objects.requireNonNull(billingContact); + } + + public static final TenantInfo EMPTY = new TenantInfo("","","", "", "", "", + TenantInfoAddress.EMPTY, TenantInfoBillingContact.EMPTY); + + public String name() { + return name; + } + + public String email() { + return email; + } + + public String website() { + return website; + } + + public String contactName() { + return contactName; + } + + public String contactEmail() { + return contactEmail; + } + + public String invoiceEmail() { + return invoiceEmail; + } + + public TenantInfoAddress address() { + return address; + } + + public TenantInfoBillingContact billingContact() { + return billingContact; + } + + public TenantInfo withName(String newName) { + return new TenantInfo(newName, email, website, contactName, contactEmail, invoiceEmail, address, billingContact); + } + + public TenantInfo withEmail(String newEmail) { + return new TenantInfo(name, newEmail, website, contactName, contactEmail, invoiceEmail, address, billingContact); + } + + public TenantInfo withWebsite(String newWebsite) { + return new TenantInfo(name, email, newWebsite, contactName, contactEmail, invoiceEmail, address, billingContact); + } + + public TenantInfo withContactName(String newContactName) { + return new TenantInfo(name, email, website, newContactName, contactEmail, invoiceEmail, address, billingContact); + } + + public TenantInfo withContactEmail(String newContactEmail) { + return new TenantInfo(name, email, website, contactName, newContactEmail, invoiceEmail, address, billingContact); + } + + public TenantInfo withInvoiceEmail(String newInvoiceEmail) { + return new TenantInfo(name, email, website, contactName, contactEmail, newInvoiceEmail, address, billingContact); + } + + public TenantInfo withAddress(TenantInfoAddress newAddress) { + return new TenantInfo(name, email, website, contactName, contactEmail, invoiceEmail, newAddress, billingContact); + } + + public TenantInfo withBillingContact(TenantInfoBillingContact newBillingContact) { + return new TenantInfo(name, email, website, contactName, contactEmail, invoiceEmail, address, newBillingContact); + } + + public boolean isEmpty() { + return this.equals(EMPTY); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TenantInfo that = (TenantInfo) o; + return name.equals(that.name) && + email.equals(that.email) && + website.equals(that.website) && + contactName.equals(that.contactName) && + contactEmail.equals(that.contactEmail) && + invoiceEmail.equals(that.invoiceEmail) && + address.equals(that.address) && + billingContact.equals(that.billingContact); + } + + @Override + public int hashCode() { + return Objects.hash(name, email, website, contactName, contactEmail, invoiceEmail, address, billingContact); + } +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfoAddress.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfoAddress.java new file mode 100644 index 00000000000..a12f351abd6 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfoAddress.java @@ -0,0 +1,95 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.tenant; + +import java.util.Objects; + +/** + * Address formats are quite diverse across the world both in therms of what fields are used, named and + * the order of them. + * + * To be generic a little future proof the address fields here are a mix of free text (address lines) and fixed fields. + * The address lines can be street address, P.O box, c/o name, apartment, suite, unit, building floor etc etc. + * + * All fields are mandatory but can be an empty string (ie. not null) + * + * @author smorgrav + */ +public class TenantInfoAddress { + + private final String addressLines; + private final String postalCodeOrZip; + private final String city; + private final String stateRegionProvince; + private final String country; + + TenantInfoAddress(String addressLines, String postalCodeOrZip, String city, String country, String stateRegionProvince) { + this.addressLines = Objects.requireNonNull(addressLines);; + this.city = Objects.requireNonNull(city); + this.postalCodeOrZip = Objects.requireNonNull(postalCodeOrZip); + this.country = Objects.requireNonNull(country); + this.stateRegionProvince = Objects.requireNonNull(stateRegionProvince); + } + + public static final TenantInfoAddress EMPTY = new TenantInfoAddress("","","", "", ""); + + public String addressLines() { + return addressLines; + } + + public String postalCodeOrZip() { + return postalCodeOrZip; + } + + public String city() { + return city; + } + + public String country() { + return country; + } + + public String stateRegionProvince() { + return stateRegionProvince; + } + + public TenantInfoAddress withAddressLines(String newAddressLines) { + return new TenantInfoAddress(newAddressLines, postalCodeOrZip, city, country, stateRegionProvince); + } + + public TenantInfoAddress withPostalCodeOrZip(String newPostalCodeOrZip) { + return new TenantInfoAddress(addressLines, newPostalCodeOrZip, city, country, stateRegionProvince); + } + + public TenantInfoAddress withCity(String newCity) { + return new TenantInfoAddress(addressLines, postalCodeOrZip, newCity, country, stateRegionProvince); + } + + public TenantInfoAddress withCountry(String newCountry) { + return new TenantInfoAddress(addressLines, postalCodeOrZip, city, newCountry, stateRegionProvince); + } + + public TenantInfoAddress withStateRegionProvince(String newStateRegionProvince) { + return new TenantInfoAddress(addressLines, postalCodeOrZip, city, country, newStateRegionProvince); + } + + public boolean isEmpty() { + return this.equals(EMPTY); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TenantInfoAddress that = (TenantInfoAddress) o; + return addressLines.equals(that.addressLines) && + postalCodeOrZip.equals(that.postalCodeOrZip) && + city.equals(that.city) && + stateRegionProvince.equals(that.stateRegionProvince) && + country.equals(that.country); + } + + @Override + public int hashCode() { + return Objects.hash(addressLines, postalCodeOrZip, city, stateRegionProvince, country); + } +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfoBillingContact.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfoBillingContact.java new file mode 100644 index 00000000000..a00dd626f0a --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfoBillingContact.java @@ -0,0 +1,74 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.tenant; + +import java.util.Objects; + +/** + * @author smorgrav + */ +public class TenantInfoBillingContact { + private final String name; + private final String email; + private final String phone; + private final TenantInfoAddress address; + + TenantInfoBillingContact(String name, String email, String phone, TenantInfoAddress address) { + this.name = Objects.requireNonNull(name); + this.email = Objects.requireNonNull(email); + this.phone = Objects.requireNonNull(phone); + this.address = Objects.requireNonNull(address); + } + + public static final TenantInfoBillingContact EMPTY = + new TenantInfoBillingContact("","", "", TenantInfoAddress.EMPTY); + + public String name() { + return name; + } + + public String email() { return email; } + + public String phone() { + return phone; + } + + public TenantInfoAddress address() { + return address; + } + + public TenantInfoBillingContact withName(String newName) { + return new TenantInfoBillingContact(newName, email, phone, address); + } + + public TenantInfoBillingContact withEmail(String newEmail) { + return new TenantInfoBillingContact(name, newEmail, phone, address); + } + + public TenantInfoBillingContact withPhone(String newPhone) { + return new TenantInfoBillingContact(name, email, newPhone, address); + } + + public TenantInfoBillingContact withAddress(TenantInfoAddress newAddress) { + return new TenantInfoBillingContact(name, email, phone, newAddress); + } + + public boolean isEmpty() { + return this.equals(EMPTY); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TenantInfoBillingContact that = (TenantInfoBillingContact) o; + return name.equals(that.name) && + email.equals(that.email) && + phone.equals(that.phone) && + address.equals(that.address); + } + + @Override + public int hashCode() { + return Objects.hash(name, email, phone, address); + } +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/package-info.java new file mode 100644 index 00000000000..9218bfcd850 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/package-info.java @@ -0,0 +1,8 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * @author mpolden + */ +@ExportPackage +package com.yahoo.vespa.hosted.controller.tenant; + +import com.yahoo.osgi.annotation.ExportPackage; |