summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java2
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantAddress.java98
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantBilling.java63
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantContact.java69
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantContacts.java147
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfo.java133
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfoAddress.java97
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfoBillingContact.java78
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java149
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java138
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java55
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java30
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-info-after-created.json4
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzAssertion.java9
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java3
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/AssertionEntity.java13
17 files changed, 721 insertions, 371 deletions
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 9894de86116..9a4e04ebb3a 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
@@ -53,7 +53,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(), Optional.empty());
}
/** The user that created the tenant */
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantAddress.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantAddress.java
new file mode 100644
index 00000000000..57c4757bde0
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantAddress.java
@@ -0,0 +1,98 @@
+// 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 java.util.Objects;
+
+/**
+ * A generic address container that tries to make as few assumptions about addresses as possible.
+ * Most addresses have some of these fields, but with different names (e.g. postal code vs zip code).
+ *
+ * When consuming data from this class, do not make any assumptions about which fields have content.
+ * An address might be still valid with surprisingly little information.
+ *
+ * All fields are non-null, but might be empty strings.
+ *
+ * @author ogronnesby
+ */
+public class TenantAddress {
+ private final String address;
+ private final String code;
+ private final String city;
+ private final String region;
+ private final String country;
+
+ TenantAddress(String address, String code, String city, String region, String country) {
+ this.address = Objects.requireNonNull(address, "'address' was null");
+ this.code = Objects.requireNonNull(code, "'code' was null");
+ this.city = Objects.requireNonNull(city, "'city' was null");
+ this.region = Objects.requireNonNull(region, "'region' was null");
+ this.country = Objects.requireNonNull(country, "'country' was null");
+ }
+
+ public static TenantAddress empty() {
+ return new TenantAddress("", "", "", "", "");
+ }
+
+ /** Multi-line fields that has the contents of the street address (or similar) */
+ public String address() { return address; }
+
+ /** The ZIP or postal code part of the address */
+ public String code() { return code; }
+
+ /** The city of the address */
+ public String city() { return city; }
+
+ /** The region part of the address - e.g. a state, county, or province */
+ public String region() { return region; }
+
+ /** The country part of the address. Its name, not a code */
+ public String country() { return country; }
+
+ public boolean isEmpty() {
+ return this.equals(empty());
+ }
+
+ public TenantAddress withAddress(String address) {
+ return new TenantAddress(address, code, city, region, country);
+ }
+
+ public TenantAddress withCode(String code) {
+ return new TenantAddress(address, code, city, region, country);
+ }
+
+ public TenantAddress withCity(String city) {
+ return new TenantAddress(address, code, city, region, country);
+ }
+
+ public TenantAddress withRegion(String region) {
+ return new TenantAddress(address, code, city, region, country);
+ }
+
+ public TenantAddress withCountry(String country) {
+ return new TenantAddress(address, code, city, region, country);
+ }
+
+ @Override
+ public String toString() {
+ return "TenantAddress{" +
+ "address='" + address + '\'' +
+ ", code='" + code + '\'' +
+ ", city='" + city + '\'' +
+ ", region='" + region + '\'' +
+ ", country='" + country + '\'' +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TenantAddress that = (TenantAddress) o;
+ return Objects.equals(address, that.address) && Objects.equals(code, that.code) && Objects.equals(city, that.city) && Objects.equals(region, that.region) && Objects.equals(country, that.country);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(address, code, city, region, country);
+ }
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantBilling.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantBilling.java
new file mode 100644
index 00000000000..61381f308ef
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantBilling.java
@@ -0,0 +1,63 @@
+// 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 java.util.Objects;
+
+/**
+ * @author smorgrav
+ */
+public class TenantBilling {
+
+ private final TenantContact contact;
+ private final TenantAddress address;
+
+ public TenantBilling(TenantContact contact, TenantAddress address) {
+ this.contact = Objects.requireNonNull(contact);
+ this.address = Objects.requireNonNull(address);
+ }
+
+ public static TenantBilling empty() {
+ return new TenantBilling(TenantContact.empty(), TenantAddress.empty());
+ }
+
+ public TenantContact contact() {
+ return contact;
+ }
+
+ public TenantAddress address() {
+ return address;
+ }
+
+ public TenantBilling withContact(TenantContact updatedContact) {
+ return new TenantBilling(updatedContact, this.address);
+ }
+
+ public TenantBilling withAddress(TenantAddress updatedAddress) {
+ return new TenantBilling(this.contact, updatedAddress);
+ }
+
+ 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;
+ TenantBilling that = (TenantBilling) o;
+ return Objects.equals(contact, that.contact) && Objects.equals(address, that.address);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(contact, address);
+ }
+
+ @Override
+ public String toString() {
+ return "TenantInfoBillingContact{" +
+ "contact=" + contact +
+ ", address=" + address +
+ '}';
+ }
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantContact.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantContact.java
new file mode 100644
index 00000000000..3aa5600ed87
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantContact.java
@@ -0,0 +1,69 @@
+// 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 java.util.Objects;
+
+/**
+ * @author ogronnesby
+ */
+public class TenantContact {
+ private final String name;
+ private final String email;
+ private final String phone;
+
+ private TenantContact(String name, String email, String phone) {
+ this.name = Objects.requireNonNull(name);
+ this.email = Objects.requireNonNull(email);
+ this.phone = Objects.requireNonNull(phone);
+ }
+
+ public static TenantContact from(String name, String email, String phone) {
+ return new TenantContact(name, email, phone);
+ }
+
+ public static TenantContact from(String name, String email) {
+ return TenantContact.from(name, email, "");
+ }
+
+ public static TenantContact empty() {
+ return new TenantContact("", "", "");
+ }
+
+ public String name() { return name; }
+ public String email() { return email; }
+ public String phone() { return phone; }
+
+ public TenantContact withName(String name) {
+ return new TenantContact(name, email, phone);
+ }
+
+ public TenantContact withEmail(String email) {
+ return new TenantContact(name, email, phone);
+ }
+
+ public TenantContact withPhone(String phone) {
+ return new TenantContact(name, email, phone);
+ }
+
+ @Override
+ public String toString() {
+ return "TenantContact{" +
+ "name='" + name + '\'' +
+ ", email='" + email + '\'' +
+ ", phone='" + phone + '\'' +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TenantContact that = (TenantContact) o;
+ return Objects.equals(name, that.name) && Objects.equals(email, that.email) && Objects.equals(phone, that.phone);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, email, phone);
+ }
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantContacts.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantContacts.java
new file mode 100644
index 00000000000..14635a3bb30
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantContacts.java
@@ -0,0 +1,147 @@
+// 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 java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Tenant contacts are targets of the notification system. Sometimes they
+ * are a person with an email address, other times they are a Slack channel,
+ * IRC plugin, etc.
+ *
+ * @author ogronnesby
+ */
+public class TenantContacts {
+ private final List<? extends Contact> contacts;
+
+ public TenantContacts(List<? extends Contact> contacts) {
+ this.contacts = List.copyOf(contacts);
+ }
+
+ public static TenantContacts empty() {
+ return new TenantContacts(List.of());
+ }
+
+ public List<? extends Contact> all() {
+ return contacts;
+ }
+
+ public boolean isEmpty() {
+ return contacts.isEmpty();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TenantContacts that = (TenantContacts) o;
+ return contacts.equals(that.contacts);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(contacts);
+ }
+
+ @Override
+ public String toString() {
+ return "TenantContacts{" +
+ "contacts=" + contacts +
+ '}';
+ }
+
+ public abstract static class Contact {
+ private final List<Audience> audiences;
+
+ public Contact(List<Audience> audiences) {
+ this.audiences = List.copyOf(audiences);
+ if (audiences.isEmpty()) throw new IllegalArgumentException("at least one notification activity must be enabled");
+ }
+
+ public List<Audience> audiences() { return audiences; }
+
+ public abstract Type type();
+
+ public abstract boolean equals(Object o);
+ public abstract int hashCode();
+ public abstract String toString();
+ }
+
+ public static class EmailContact extends Contact {
+ private final String email;
+
+ public EmailContact(List<Audience> audiences, String email) {
+ super(audiences);
+ this.email = email;
+ }
+
+ public String email() { return email; }
+
+ @Override
+ public Type type() {
+ return Type.EMAIL;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ EmailContact that = (EmailContact) o;
+ return email.equals(that.email);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(email);
+ }
+
+ @Override
+ public String toString() {
+ return "EmailContact{" +
+ "email='" + email + '\'' +
+ '}';
+ }
+ }
+
+ public enum Type {
+ EMAIL("email");
+
+ private final String value;
+
+ Type(String value) {
+ this.value = value;
+ }
+
+ public String value() {
+ return this.value;
+ }
+
+ public static Optional<Type> from(String value) {
+ return Arrays.stream(Type.values()).filter(x -> x.value().equals(value)).findAny();
+ }
+ }
+
+ public enum Audience {
+ // tenant admin type updates about billing etc.
+ TENANT("tenant"),
+
+ // system notifications like deployment failures etc.
+ NOTIFICATIONS("notifications");
+
+ private final String value;
+
+ Audience(String value) {
+ this.value = value;
+ }
+
+ public String value() {
+ return value;
+ }
+
+ public static Optional<Audience> from(String value) {
+ return Arrays.stream(Audience.values()).filter((x -> x.value().equals(value))).findAny();
+ }
+ }
+}
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
index 87d4f03b090..0ca7863fbce 100644
--- 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
@@ -13,51 +13,34 @@ import java.util.Objects;
* @author smorgrav
*/
public class TenantInfo {
- // Editable as 'Tenant Information - Company Name'
- // Viewable in the 'Account - Profile' section as 'Company Name'
- private final String name;
- // Editable as 'Tenant Information - Email'
- // Not displayed outside of 'Edit profile'
+ private final String name;
private final String email;
-
- // Editable as 'Tenant Information - Website'
- // Viewable in the 'Account - Profile' section at bottom of 'Contact Information'
private final String website;
- // Editable as 'Contact Information - Contact Name'
- // Viewable in the 'Account - Profile' section in 'Contact Information'
- private final String contactName;
-
- // Editable as 'Contact Information - Contact Email'
- // Viewable in the 'Account - Profile' section in 'Contact Information'
- private final String contactEmail;
-
- // Not editable in the account setting
- // Not viewable.
- // TODO: Remove
- private final String invoiceEmail;
-
- // See class for more info
- private final TenantInfoAddress address;
-
- // See class for more info
- private final TenantInfoBillingContact billingContact;
+ private final TenantContact contact;
+ private final TenantAddress address;
+ private final TenantBilling billingContact;
+ private final TenantContacts contacts;
TenantInfo(String name, String email, String website, String contactName, String contactEmail,
- String invoiceEmail, TenantInfoAddress address, TenantInfoBillingContact billingContact) {
+ TenantAddress address, TenantBilling billingContact, TenantContacts contacts) {
+ this(name, email, website, TenantContact.from(contactName, contactEmail), address, billingContact, contacts);
+ }
+
+ TenantInfo(String name, String email, String website, TenantContact contact, TenantAddress address, TenantBilling billing, TenantContacts contacts) {
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.contact = Objects.requireNonNull(contact);
this.address = Objects.requireNonNull(address);
- this.billingContact = Objects.requireNonNull(billingContact);
+ this.billingContact = Objects.requireNonNull(billing);
+ this.contacts = Objects.requireNonNull(contacts);
}
- public static final TenantInfo EMPTY = new TenantInfo("","","", "", "", "",
- TenantInfoAddress.EMPTY, TenantInfoBillingContact.EMPTY);
+ public static TenantInfo empty() {
+ return new TenantInfo("", "", "", "", "", TenantAddress.empty(), TenantBilling.empty(), TenantContacts.empty());
+ }
public String name() {
return name;
@@ -71,60 +54,46 @@ public class TenantInfo {
return website;
}
- public String contactName() {
- return contactName;
- }
-
- public String contactEmail() {
- return contactEmail;
- }
-
- public String invoiceEmail() {
- return invoiceEmail;
- }
+ public TenantContact contact() { return contact; }
- public TenantInfoAddress address() {
- return address;
- }
+ public TenantAddress address() { return address; }
- public TenantInfoBillingContact billingContact() {
+ public TenantBilling billingContact() {
return billingContact;
}
- public TenantInfo withName(String newName) {
- return new TenantInfo(newName, email, website, contactName, contactEmail, invoiceEmail, address, billingContact);
- }
+ public TenantContacts contacts() { return contacts; }
- public TenantInfo withEmail(String newEmail) {
- return new TenantInfo(name, newEmail, website, contactName, contactEmail, invoiceEmail, address, billingContact);
+ public boolean isEmpty() {
+ return this.equals(empty());
}
- public TenantInfo withWebsite(String newWebsite) {
- return new TenantInfo(name, email, newWebsite, contactName, contactEmail, invoiceEmail, address, billingContact);
+ public TenantInfo withName(String name) {
+ return new TenantInfo(name, email, website, contact, address, billingContact, contacts);
}
- public TenantInfo withContactName(String newContactName) {
- return new TenantInfo(name, email, website, newContactName, contactEmail, invoiceEmail, address, billingContact);
+ public TenantInfo withEmail(String email) {
+ return new TenantInfo(name, email, website, contact, address, billingContact, contacts);
}
- public TenantInfo withContactEmail(String newContactEmail) {
- return new TenantInfo(name, email, website, contactName, newContactEmail, invoiceEmail, address, billingContact);
+ public TenantInfo withWebsite(String website) {
+ return new TenantInfo(name, email, website, contact, address, billingContact, contacts);
}
- public TenantInfo withInvoiceEmail(String newInvoiceEmail) {
- return new TenantInfo(name, email, website, contactName, contactEmail, newInvoiceEmail, address, billingContact);
+ public TenantInfo withContact(TenantContact contact) {
+ return new TenantInfo(name, email, website, contact, address, billingContact, contacts);
}
- public TenantInfo withAddress(TenantInfoAddress newAddress) {
- return new TenantInfo(name, email, website, contactName, contactEmail, invoiceEmail, newAddress, billingContact);
+ public TenantInfo withAddress(TenantAddress address) {
+ return new TenantInfo(name, email, website, contact, address, billingContact, contacts);
}
- public TenantInfo withBillingContact(TenantInfoBillingContact newBillingContact) {
- return new TenantInfo(name, email, website, contactName, contactEmail, invoiceEmail, address, newBillingContact);
+ public TenantInfo withBilling(TenantBilling billingContact) {
+ return new TenantInfo(name, email, website, contact, address, billingContact, contacts);
}
- public boolean isEmpty() {
- return this.equals(EMPTY);
+ public TenantInfo withContacts(TenantContacts contacts) {
+ return new TenantInfo(name, email, website, contact, address, billingContact, contacts);
}
@Override
@@ -132,18 +101,30 @@ public class TenantInfo {
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);
+ return Objects.equals(name, that.name) &&
+ Objects.equals(email, that.email) &&
+ Objects.equals(website, that.website) &&
+ Objects.equals(contact, that.contact) &&
+ Objects.equals(address, that.address) &&
+ Objects.equals(billingContact, that.billingContact) &&
+ Objects.equals(contacts, that.contacts);
}
@Override
public int hashCode() {
- return Objects.hash(name, email, website, contactName, contactEmail, invoiceEmail, address, billingContact);
+ return Objects.hash(name, email, website, contact, address, billingContact, contacts);
+ }
+
+ @Override
+ public String toString() {
+ return "TenantInfo{" +
+ "name='" + name + '\'' +
+ ", email='" + email + '\'' +
+ ", website='" + website + '\'' +
+ ", contact=" + contact +
+ ", address=" + address +
+ ", billingContact=" + billingContact +
+ ", contacts=" + contacts +
+ '}';
}
}
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
deleted file mode 100644
index 740adde4519..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfoAddress.java
+++ /dev/null
@@ -1,97 +0,0 @@
-// 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 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 {
-
- // All fields are editable in 'Edit Profile - Company Address'.
- // The fields are not exposed outside the 'Edit Profile' form.
- 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
deleted file mode 100644
index c875a19d57b..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfoBillingContact.java
+++ /dev/null
@@ -1,78 +0,0 @@
-// 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 java.util.Objects;
-
-/**
- * @author smorgrav
- */
-public class TenantInfoBillingContact {
-
- // All fields are editable in 'Billing - Edit billing contact'
- // Only 'name' and 'email' are exposed outside the 'Edit billing contact' form.
- // All these fields are required by the billing process.
- 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-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java
index 1b6a0a6a122..00e38abcba7 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java
@@ -9,22 +9,24 @@ import com.yahoo.slime.ArrayTraverser;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Inspector;
import com.yahoo.slime.Slime;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
import com.yahoo.slime.SlimeUtils;
+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.BillingInfo;
+import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.BillingInfo;
import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import com.yahoo.vespa.hosted.controller.tenant.DeletedTenant;
import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
+import com.yahoo.vespa.hosted.controller.tenant.TenantAddress;
+import com.yahoo.vespa.hosted.controller.tenant.TenantBilling;
+import com.yahoo.vespa.hosted.controller.tenant.TenantContact;
+import com.yahoo.vespa.hosted.controller.tenant.TenantContacts;
import com.yahoo.vespa.hosted.controller.tenant.TenantInfo;
-import com.yahoo.vespa.hosted.controller.tenant.TenantInfoAddress;
-import com.yahoo.vespa.hosted.controller.tenant.TenantInfoBillingContact;
import java.net.URI;
import java.security.Principal;
@@ -35,6 +37,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.stream.Collectors;
/**
* Slime serialization of {@link Tenant} sub-types.
@@ -193,50 +196,47 @@ public class TenantSerializer {
}
TenantInfo tenantInfoFromSlime(Inspector infoObject) {
- if (!infoObject.valid()) return TenantInfo.EMPTY;
+ if (!infoObject.valid()) return TenantInfo.empty();
- return TenantInfo.EMPTY
+ return TenantInfo.empty()
.withName(infoObject.field("name").asString())
.withEmail(infoObject.field("email").asString())
.withWebsite(infoObject.field("website").asString())
- .withContactName(infoObject.field("contactName").asString())
- .withContactEmail(infoObject.field("contactEmail").asString())
- .withInvoiceEmail(infoObject.field("invoiceEmail").asString())
+ .withContact(TenantContact.from(
+ infoObject.field("contactName").asString(),
+ infoObject.field("contactEmail").asString()))
.withAddress(tenantInfoAddressFromSlime(infoObject.field("address")))
- .withBillingContact(tenantInfoBillingContactFromSlime(infoObject.field("billingContact")));
+ .withBilling(tenantInfoBillingContactFromSlime(infoObject.field("billingContact")))
+ .withContacts(tenantContactsFrom(infoObject.field("contacts")));
}
- private TenantInfoAddress tenantInfoAddressFromSlime(Inspector addressObject) {
- return TenantInfoAddress.EMPTY
- .withAddressLines(addressObject.field("addressLines").asString())
- .withPostalCodeOrZip(addressObject.field("postalCodeOrZip").asString())
+ private TenantAddress tenantInfoAddressFromSlime(Inspector addressObject) {
+ return TenantAddress.empty()
+ .withAddress(addressObject.field("addressLines").asString())
+ .withCode(addressObject.field("postalCodeOrZip").asString())
.withCity(addressObject.field("city").asString())
- .withStateRegionProvince(addressObject.field("stateRegionProvince").asString())
+ .withRegion(addressObject.field("stateRegionProvince").asString())
.withCountry(addressObject.field("country").asString());
}
- private TenantInfoBillingContact tenantInfoBillingContactFromSlime(Inspector billingObject) {
- return TenantInfoBillingContact.EMPTY
- .withName(billingObject.field("name").asString())
- .withEmail(billingObject.field("email").asString())
- .withPhone(billingObject.field("phone").asString())
+ private TenantBilling tenantInfoBillingContactFromSlime(Inspector billingObject) {
+ return TenantBilling.empty()
+ .withContact(TenantContact.from(
+ billingObject.field("name").asString(),
+ billingObject.field("email").asString(),
+ billingObject.field("phone").asString()))
.withAddress(tenantInfoAddressFromSlime(billingObject.field("address")));
}
private List<TenantSecretStore> secretStoresFromSlime(Inspector secretStoresObject) {
- List<TenantSecretStore> secretStores = new ArrayList<>();
- if (!secretStoresObject.valid()) return secretStores;
-
- secretStoresObject.traverse((ArrayTraverser) (index, inspector) -> {
- secretStores.add(
- new TenantSecretStore(
- inspector.field(nameField).asString(),
- inspector.field(awsIdField).asString(),
- inspector.field(roleField).asString()
- )
- );
- });
- return secretStores;
+ if (!secretStoresObject.valid()) return List.of();
+
+ return SlimeUtils.entriesStream(secretStoresObject)
+ .map(inspector -> new TenantSecretStore(
+ inspector.field(nameField).asString(),
+ inspector.field(awsIdField).asString(),
+ inspector.field(roleField).asString()))
+ .collect(Collectors.toUnmodifiableList());
}
private LastLoginInfo lastLoginInfoFromSlime(Inspector lastLoginInfoObject) {
@@ -252,31 +252,31 @@ public class TenantSerializer {
infoCursor.setString("name", info.name());
infoCursor.setString("email", info.email());
infoCursor.setString("website", info.website());
- infoCursor.setString("invoiceEmail", info.invoiceEmail());
- infoCursor.setString("contactName", info.contactName());
- infoCursor.setString("contactEmail", info.contactEmail());
+ infoCursor.setString("contactName", info.contact().name());
+ infoCursor.setString("contactEmail", info.contact().email());
toSlime(info.address(), infoCursor);
toSlime(info.billingContact(), infoCursor);
+ toSlime(info.contacts(), infoCursor);
}
- private void toSlime(TenantInfoAddress address, Cursor parentCursor) {
+ private void toSlime(TenantAddress address, Cursor parentCursor) {
if (address.isEmpty()) return;
Cursor addressCursor = parentCursor.setObject("address");
- addressCursor.setString("addressLines", address.addressLines());
- addressCursor.setString("postalCodeOrZip", address.postalCodeOrZip());
+ addressCursor.setString("addressLines", address.address());
+ addressCursor.setString("postalCodeOrZip", address.code());
addressCursor.setString("city", address.city());
- addressCursor.setString("stateRegionProvince", address.stateRegionProvince());
+ addressCursor.setString("stateRegionProvince", address.region());
addressCursor.setString("country", address.country());
}
- private void toSlime(TenantInfoBillingContact billingContact, Cursor parentCursor) {
+ private void toSlime(TenantBilling billingContact, Cursor parentCursor) {
if (billingContact.isEmpty()) return;
Cursor addressCursor = parentCursor.setObject("billingContact");
- addressCursor.setString("name", billingContact.name());
- addressCursor.setString("email", billingContact.email());
- addressCursor.setString("phone", billingContact.phone());
+ addressCursor.setString("name", billingContact.contact().name());
+ addressCursor.setString("email", billingContact.contact().email());
+ addressCursor.setString("phone", billingContact.contact().phone());
toSlime(billingContact.address(), addressCursor);
}
@@ -290,7 +290,19 @@ public class TenantSerializer {
secretStoreCursor.setString(awsIdField, tenantSecretStore.getAwsId());
secretStoreCursor.setString(roleField, tenantSecretStore.getRole());
});
+ }
+
+ private void toSlime(TenantContacts contacts, Cursor parent) {
+ if (contacts.isEmpty()) return;
+ var cursor = parent.setArray("contacts");
+ contacts.all().forEach(contact -> writeContact(contact, cursor.addObject()));
+ }
+ private TenantContacts tenantContactsFrom(Inspector object) {
+ List<TenantContacts.Contact> contacts = SlimeUtils.entriesStream(object)
+ .map(this::readContact)
+ .collect(Collectors.toUnmodifiableList());
+ return new TenantContacts(contacts);
}
private Optional<Contact> contactFrom(Inspector object) {
@@ -336,6 +348,36 @@ public class TenantSerializer {
return personLists;
}
+ private void writeContact(TenantContacts.Contact contact, Cursor cursor) {
+ cursor.setString("type", contact.type().value());
+ Cursor audiencesArray = cursor.setArray("audiences");
+ contact.audiences().forEach(audience -> audiencesArray.addString(toAudience(audience)));
+ var data = cursor.setObject("data");
+ switch (contact.type()) {
+ case EMAIL:
+ var email = (TenantContacts.EmailContact) contact;
+ data.setString("email", email.email());
+ return;
+ default:
+ throw new IllegalArgumentException("Serialization for contact type not implemented: " + contact.type());
+ }
+ }
+
+ private TenantContacts.Contact readContact(Inspector inspector) {
+ var type = TenantContacts.Type.from(inspector.field("type").asString())
+ .orElseThrow(() -> new RuntimeException("Unknown type: " + inspector.field("type").asString()));
+ var audiences = SlimeUtils.entriesStream(inspector.field("audiences"))
+ .map(audience -> TenantSerializer.fromAudience(audience.asString()))
+ .collect(Collectors.toUnmodifiableList());
+ switch (type) {
+ case EMAIL:
+ return new TenantContacts.EmailContact(audiences, inspector.field("data").field("email").asString());
+ default:
+ throw new IllegalArgumentException("Serialization for contact type not implemented: " + type);
+ }
+
+ }
+
private static Tenant.Type typeOf(String value) {
switch (value) {
case "athenz": return Tenant.Type.athenz;
@@ -371,4 +413,21 @@ public class TenantSerializer {
default: throw new IllegalArgumentException("Unexpected user level '" + userLevel + "'.");
}
}
+
+ private static TenantContacts.Audience fromAudience(String value) {
+ switch (value) {
+ case "tenant": return TenantContacts.Audience.TENANT;
+ case "notifications": return TenantContacts.Audience.NOTIFICATIONS;
+ default: throw new IllegalArgumentException("Unknown contact audience '" + value + "'.");
+ }
+ }
+
+ private static String toAudience(TenantContacts.Audience audience) {
+ switch (audience) {
+ case TENANT: return "tenant";
+ case NOTIFICATIONS: return "notifications";
+ default: throw new IllegalArgumentException("Unexpected contact audience '" + audience + "'.");
+ }
+ }
+
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
index c8e16634464..6490ade655a 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
@@ -111,9 +111,11 @@ import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import com.yahoo.vespa.hosted.controller.tenant.DeletedTenant;
import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
+import com.yahoo.vespa.hosted.controller.tenant.TenantAddress;
+import com.yahoo.vespa.hosted.controller.tenant.TenantBilling;
+import com.yahoo.vespa.hosted.controller.tenant.TenantContact;
+import com.yahoo.vespa.hosted.controller.tenant.TenantContacts;
import com.yahoo.vespa.hosted.controller.tenant.TenantInfo;
-import com.yahoo.vespa.hosted.controller.tenant.TenantInfoAddress;
-import com.yahoo.vespa.hosted.controller.tenant.TenantInfoBillingContact;
import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
import com.yahoo.vespa.serviceview.bindings.ApplicationView;
@@ -504,37 +506,71 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
infoCursor.setString("name", info.name());
infoCursor.setString("email", info.email());
infoCursor.setString("website", info.website());
- infoCursor.setString("invoiceEmail", info.invoiceEmail());
- infoCursor.setString("contactName", info.contactName());
- infoCursor.setString("contactEmail", info.contactEmail());
+ infoCursor.setString("contactName", info.contact().name());
+ infoCursor.setString("contactEmail", info.contact().email());
toSlime(info.address(), infoCursor);
toSlime(info.billingContact(), infoCursor);
+ toSlime(info.contacts(), infoCursor);
}
return new SlimeJsonResponse(slime);
}
- private void toSlime(TenantInfoAddress address, Cursor parentCursor) {
+ private void toSlime(TenantAddress address, Cursor parentCursor) {
if (address.isEmpty()) return;
Cursor addressCursor = parentCursor.setObject("address");
- addressCursor.setString("addressLines", address.addressLines());
- addressCursor.setString("postalCodeOrZip", address.postalCodeOrZip());
+ addressCursor.setString("addressLines", address.address());
+ addressCursor.setString("postalCodeOrZip", address.code());
addressCursor.setString("city", address.city());
- addressCursor.setString("stateRegionProvince", address.stateRegionProvince());
+ addressCursor.setString("stateRegionProvince", address.region());
addressCursor.setString("country", address.country());
}
- private void toSlime(TenantInfoBillingContact billingContact, Cursor parentCursor) {
+ private void toSlime(TenantBilling billingContact, Cursor parentCursor) {
if (billingContact.isEmpty()) return;
Cursor addressCursor = parentCursor.setObject("billingContact");
- addressCursor.setString("name", billingContact.name());
- addressCursor.setString("email", billingContact.email());
- addressCursor.setString("phone", billingContact.phone());
+ addressCursor.setString("name", billingContact.contact().name());
+ addressCursor.setString("email", billingContact.contact().email());
+ addressCursor.setString("phone", billingContact.contact().phone());
toSlime(billingContact.address(), addressCursor);
}
+ private void toSlime(TenantContacts contacts, Cursor parentCursor) {
+ Cursor contactsCursor = parentCursor.setArray("contacts");
+ contacts.all().forEach(contact -> {
+ Cursor contactCursor = contactsCursor.addObject();
+ Cursor audiencesArray = contactCursor.setArray("audiences");
+ contact.audiences().forEach(audience -> audiencesArray.addString(toAudience(audience)));
+ switch (contact.type()) {
+ case EMAIL:
+ var email = (TenantContacts.EmailContact) contact;
+ contactCursor.setString("email", email.email());
+ return;
+ default:
+ throw new IllegalArgumentException("Serialization for contact type not implemented: " + contact.type());
+ }
+ });
+ }
+
+ private static TenantContacts.Audience fromAudience(String value) {
+ switch (value) {
+ case "tenant": return TenantContacts.Audience.TENANT;
+ case "notifications": return TenantContacts.Audience.NOTIFICATIONS;
+ default: throw new IllegalArgumentException("Unknown contact audience '" + value + "'.");
+ }
+ }
+
+ private static String toAudience(TenantContacts.Audience audience) {
+ switch (audience) {
+ case TENANT: return "tenant";
+ case NOTIFICATIONS: return "notifications";
+ default: throw new IllegalArgumentException("Unexpected contact audience '" + audience + "'.");
+ }
+ }
+
+
private HttpResponse updateTenantInfo(String tenantName, HttpRequest request) {
return controller.tenants().get(TenantName.from(tenantName))
.filter(tenant -> tenant.type() == Tenant.Type.cloud)
@@ -551,24 +587,28 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
// Merge info from request with the existing info
Inspector insp = toSlime(request.getData()).get();
- TenantInfo mergedInfo = TenantInfo.EMPTY
+
+ TenantContact mergedContact = TenantContact.empty()
+ .withName(getString(insp.field("contactName"), oldInfo.contact().name()))
+ .withEmail(getString(insp.field("contactEmail"), oldInfo.contact().email()));
+
+ TenantInfo mergedInfo = TenantInfo.empty()
.withName(getString(insp.field("name"), oldInfo.name()))
- .withEmail(getString(insp.field("email"), oldInfo.email()))
- .withWebsite(getString(insp.field("website"), oldInfo.website()))
- .withInvoiceEmail(getString(insp.field("invoiceEmail"), oldInfo.invoiceEmail()))
- .withContactName(getString(insp.field("contactName"), oldInfo.contactName()))
- .withContactEmail(getString(insp.field("contactEmail"), oldInfo.contactEmail()))
+ .withEmail(getString(insp.field("email"), oldInfo.email()))
+ .withWebsite(getString(insp.field("website"), oldInfo.website()))
+ .withContact(mergedContact)
.withAddress(updateTenantInfoAddress(insp.field("address"), oldInfo.address()))
- .withBillingContact(updateTenantInfoBillingContact(insp.field("billingContact"), oldInfo.billingContact()));
+ .withBilling(updateTenantInfoBillingContact(insp.field("billingContact"), oldInfo.billingContact()))
+ .withContacts(updateTenantInfoContacts(insp.field("contacts"), oldInfo.contacts()));
// Assert that we have a valid tenant info
- if (mergedInfo.contactName().isBlank()) {
+ if (mergedInfo.contact().name().isBlank()) {
throw new IllegalArgumentException("'contactName' cannot be empty");
}
- if (mergedInfo.contactEmail().isBlank()) {
+ if (mergedInfo.contact().email().isBlank()) {
throw new IllegalArgumentException("'contactEmail' cannot be empty");
}
- if (! mergedInfo.contactEmail().contains("@")) {
+ if (! mergedInfo.contact().email().contains("@")) {
// email address validation is notoriously hard - we should probably just try to send a
// verification email to this address. checking for @ is a simple best-effort.
throw new IllegalArgumentException("'contactEmail' needs to be an email address");
@@ -590,20 +630,20 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
return new MessageResponse("Tenant info updated");
}
- private TenantInfoAddress updateTenantInfoAddress(Inspector insp, TenantInfoAddress oldAddress) {
+ private TenantAddress updateTenantInfoAddress(Inspector insp, TenantAddress oldAddress) {
if (!insp.valid()) return oldAddress;
- TenantInfoAddress address = TenantInfoAddress.EMPTY
+ TenantAddress address = TenantAddress.empty()
.withCountry(getString(insp.field("country"), oldAddress.country()))
- .withStateRegionProvince(getString(insp.field("stateRegionProvince"), oldAddress.stateRegionProvince()))
+ .withRegion(getString(insp.field("stateRegionProvince"), oldAddress.region()))
.withCity(getString(insp.field("city"), oldAddress.city()))
- .withPostalCodeOrZip(getString(insp.field("postalCodeOrZip"), oldAddress.postalCodeOrZip()))
- .withAddressLines(getString(insp.field("addressLines"), oldAddress.addressLines()));
+ .withCode(getString(insp.field("postalCodeOrZip"), oldAddress.code()))
+ .withAddress(getString(insp.field("addressLines"), oldAddress.address()));
- List<String> fields = List.of(address.addressLines(),
- address.postalCodeOrZip(),
+ List<String> fields = List.of(address.address(),
+ address.code(),
address.country(),
address.city(),
- address.stateRegionProvince());
+ address.region());
if (fields.stream().allMatch(String::isBlank) || fields.stream().noneMatch(String::isBlank))
return address;
@@ -611,7 +651,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
throw new IllegalArgumentException("All address fields must be set");
}
- private TenantInfoBillingContact updateTenantInfoBillingContact(Inspector insp, TenantInfoBillingContact oldContact) {
+ private TenantContact updateTenantInfoContact(Inspector insp, TenantContact oldContact) {
if (!insp.valid()) return oldContact;
String email = getString(insp.field("email"), oldContact.email());
@@ -622,13 +662,37 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
throw new IllegalArgumentException("'email' needs to be an email address");
}
- return TenantInfoBillingContact.EMPTY
+ return TenantContact.empty()
.withName(getString(insp.field("name"), oldContact.name()))
- .withEmail(email)
- .withPhone(getString(insp.field("phone"), oldContact.phone()))
+ .withEmail(getString(insp.field("email"), oldContact.email()))
+ .withPhone(getString(insp.field("phone"), oldContact.phone()));
+ }
+
+ private TenantBilling updateTenantInfoBillingContact(Inspector insp, TenantBilling oldContact) {
+ if (!insp.valid()) return oldContact;
+
+ return TenantBilling.empty()
+ .withContact(updateTenantInfoContact(insp, oldContact.contact()))
.withAddress(updateTenantInfoAddress(insp.field("address"), oldContact.address()));
}
+ private TenantContacts updateTenantInfoContacts(Inspector insp, TenantContacts oldContacts) {
+ if (!insp.valid()) return oldContacts;
+
+ List<TenantContacts.Contact> contacts = SlimeUtils.entriesStream(insp).map(inspector -> {
+ String email = inspector.field("email").asString().trim();
+ List<TenantContacts.Audience> audiences = SlimeUtils.entriesStream(inspector.field("audiences"))
+ .map(audience -> fromAudience(audience.asString()))
+ .collect(Collectors.toUnmodifiableList());
+ if (!email.contains("@")) {
+ throw new IllegalArgumentException("'email' needs to be an email address");
+ }
+ return new TenantContacts.EmailContact(audiences, email);
+ }).collect(toUnmodifiableList());
+
+ return new TenantContacts(contacts);
+ }
+
private HttpResponse notifications(HttpRequest request, Optional<String> tenant, boolean includeTenantFieldInResponse) {
boolean productionOnly = showOnlyProductionInstances(request);
boolean excludeMessages = "true".equals(request.getProperty("excludeMessages"));
@@ -648,6 +712,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
.forEach(notification -> toSlime(notificationsArray.addObject(), notification, includeTenantFieldInResponse, excludeMessages));
return new SlimeJsonResponse(slime);
}
+
private static <T> boolean propertyEquals(HttpRequest request, String property, Function<String, T> mapper, Optional<T> value) {
return Optional.ofNullable(request.getProperty(property))
.map(propertyValue -> value.isPresent() && mapper.apply(propertyValue).equals(value.get()))
@@ -1852,8 +1917,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
User user = getAttribute(request, User.ATTRIBUTE_NAME, User.class);
TenantInfo info = controller.tenants().require(tenant, CloudTenant.class)
.info()
- .withContactName(user.name())
- .withContactEmail(user.email());
+ .withContact(TenantContact.from(user.name(), user.email()));
// Store changes
controller.tenants().lockOrThrow(tenant, LockedTenant.Cloud.class, lockedTenant -> {
lockedTenant = lockedTenant.withInfo(info);
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java
index 0b986667911..e0d14f19f21 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java
@@ -16,9 +16,11 @@ import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import com.yahoo.vespa.hosted.controller.tenant.DeletedTenant;
import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
+import com.yahoo.vespa.hosted.controller.tenant.TenantAddress;
+import com.yahoo.vespa.hosted.controller.tenant.TenantBilling;
+import com.yahoo.vespa.hosted.controller.tenant.TenantContact;
+import com.yahoo.vespa.hosted.controller.tenant.TenantContacts;
import com.yahoo.vespa.hosted.controller.tenant.TenantInfo;
-import com.yahoo.vespa.hosted.controller.tenant.TenantInfoAddress;
-import com.yahoo.vespa.hosted.controller.tenant.TenantInfoBillingContact;
import org.junit.Test;
import java.net.URI;
@@ -97,7 +99,7 @@ public class TenantSerializerTest {
Optional.of(new SimplePrincipal("foobar-user")),
ImmutableBiMap.of(publicKey, new SimplePrincipal("joe"),
otherPublicKey, new SimplePrincipal("jane")),
- TenantInfo.EMPTY,
+ TenantInfo.empty(),
List.of(),
Optional.empty()
);
@@ -116,7 +118,7 @@ public class TenantSerializerTest {
Optional.of(new SimplePrincipal("foobar-user")),
ImmutableBiMap.of(publicKey, new SimplePrincipal("joe"),
otherPublicKey, new SimplePrincipal("jane")),
- TenantInfo.EMPTY.withName("Ofni Tnanet"),
+ TenantInfo.empty().withName("Ofni Tnanet"),
List.of(
new TenantSecretStore("ss1", "123", "role1"),
new TenantSecretStore("ss2", "124", "role2")
@@ -131,39 +133,35 @@ public class TenantSerializerTest {
@Test
public void cloud_tenant_with_tenant_info_partial() {
- TenantInfo partialInfo = TenantInfo.EMPTY
- .withAddress(TenantInfoAddress.EMPTY.withCity("Hønefoss"));
+ TenantInfo partialInfo = TenantInfo.empty()
+ .withAddress(TenantAddress.empty().withCity("Hønefoss"));
Slime slime = new Slime();
Cursor parentObject = slime.setObject();
serializer.toSlime(partialInfo, parentObject);
- assertEquals("{\"info\":{\"name\":\"\",\"email\":\"\",\"website\":\"\",\"invoiceEmail\":\"\",\"contactName\":\"\",\"contactEmail\":\"\",\"address\":{\"addressLines\":\"\",\"postalCodeOrZip\":\"\",\"city\":\"Hønefoss\",\"stateRegionProvince\":\"\",\"country\":\"\"}}}", slime.toString());
+ assertEquals("{\"info\":{\"name\":\"\",\"email\":\"\",\"website\":\"\",\"contactName\":\"\",\"contactEmail\":\"\",\"address\":{\"addressLines\":\"\",\"postalCodeOrZip\":\"\",\"city\":\"Hønefoss\",\"stateRegionProvince\":\"\",\"country\":\"\"}}}", slime.toString());
}
@Test
public void cloud_tenant_with_tenant_info_full() {
- TenantInfo fullInfo = TenantInfo.EMPTY
+ TenantInfo fullInfo = TenantInfo.empty()
.withName("My Company")
.withEmail("email@mycomp.any")
.withWebsite("http://mycomp.any")
- .withContactEmail("ceo@mycomp.any")
- .withContactName("My Name")
- .withInvoiceEmail("invoice@mycomp.any")
- .withAddress(TenantInfoAddress.EMPTY
+ .withContact(TenantContact.from("My Name", "ceo@mycomp.any"))
+ .withAddress(TenantAddress.empty()
.withCity("Hønefoss")
- .withAddressLines("Riperbakken 2")
+ .withAddress("Riperbakken 2")
.withCountry("Norway")
- .withPostalCodeOrZip("3510")
- .withStateRegionProvince("Viken"))
- .withBillingContact(TenantInfoBillingContact.EMPTY
- .withEmail("thomas@sodor.com")
- .withName("Thomas The Tank Engine")
- .withPhone("NA")
- .withAddress(TenantInfoAddress.EMPTY
+ .withCode("3510")
+ .withRegion("Viken"))
+ .withBilling(TenantBilling.empty()
+ .withContact(TenantContact.from("Thomas The Tank Engine", "thomas@sodor.com", "NA"))
+ .withAddress(TenantAddress.empty()
.withCity("Suddery")
.withCountry("Sodor")
- .withAddressLines("Central Station")
- .withStateRegionProvince("Irish Sea")));
+ .withAddress("Central Station")
+ .withRegion("Irish Sea")));
Slime slime = new Slime();
Cursor parentCursor = slime.setObject();
@@ -174,6 +172,19 @@ public class TenantSerializerTest {
}
@Test
+ public void cloud_tenant_with_tenant_info_contacts() {
+ TenantInfo tenantInfo = TenantInfo.empty()
+ .withContacts(new TenantContacts(List.of(
+ new TenantContacts.EmailContact(List.of(TenantContacts.Audience.TENANT), "email1@email.com"),
+ new TenantContacts.EmailContact(List.of(TenantContacts.Audience.TENANT, TenantContacts.Audience.NOTIFICATIONS), "email2@email.com"))));
+ Slime slime = new Slime();
+ Cursor parentCursor = slime.setObject();
+ serializer.toSlime(tenantInfo, parentCursor);
+ TenantInfo roundTripInfo = serializer.tenantInfoFromSlime(parentCursor.field("info"));
+ assertEquals(tenantInfo, roundTripInfo);
+ }
+
+ @Test
public void deleted_tenant() {
DeletedTenant tenant = new DeletedTenant(
TenantName.from("tenant1"), Instant.ofEpochMilli(1234L), Instant.ofEpochMilli(2345L));
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java
index fa1483fd90f..c02e898b038 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java
@@ -6,8 +6,6 @@ import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ApplicationName;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.TenantName;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
import com.yahoo.vespa.flags.InMemoryFlagSource;
import com.yahoo.vespa.flags.PermanentFlags;
import com.yahoo.vespa.hosted.controller.ControllerTester;
@@ -32,7 +30,6 @@ import org.junit.Test;
import javax.ws.rs.ForbiddenException;
import java.io.File;
-import java.io.IOException;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
@@ -89,19 +86,26 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest {
tester.assertResponse(infoRequest, "{}", 200);
String partialInfo = "{\"contactName\":\"newName\", \"contactEmail\": \"foo@example.com\", \"billingContact\":{\"name\":\"billingName\"}}";
-
var postPartial =
request("/application/v4/tenant/scoober/info", PUT)
.data(partialInfo)
.roles(Set.of(Role.administrator(tenantName)));
tester.assertResponse(postPartial, "{\"message\":\"Tenant info updated\"}", 200);
+ String partialContacts = "{\"contacts\": [{\"audiences\": [\"tenant\"],\"email\": \"contact1@example.com\"}]}";
+ var postPartialContacts =
+ request("/application/v4/tenant/scoober/info", PUT)
+ .data(partialContacts)
+ .roles(Set.of(Role.administrator(tenantName)));
+ tester.assertResponse(postPartialContacts, "{\"message\":\"Tenant info updated\"}", 200);
+
// Read back the updated info
- tester.assertResponse(infoRequest, "{\"name\":\"\",\"email\":\"\",\"website\":\"\",\"invoiceEmail\":\"\",\"contactName\":\"newName\",\"contactEmail\":\"foo@example.com\",\"billingContact\":{\"name\":\"billingName\",\"email\":\"\",\"phone\":\"\"}}", 200);
+ tester.assertResponse(infoRequest, "{\"name\":\"\",\"email\":\"\",\"website\":\"\",\"contactName\":\"newName\",\"contactEmail\":\"foo@example.com\",\"billingContact\":{\"name\":\"billingName\",\"email\":\"\",\"phone\":\"\"},\"contacts\":[{\"audiences\":[\"tenant\"],\"email\":\"contact1@example.com\"}]}", 200);
String fullAddress = "{\"addressLines\":\"addressLines\",\"postalCodeOrZip\":\"postalCodeOrZip\",\"city\":\"city\",\"stateRegionProvince\":\"stateRegionProvince\",\"country\":\"country\"}";
String fullBillingContact = "{\"name\":\"name\",\"email\":\"foo@example\",\"phone\":\"phone\",\"address\":" + fullAddress + "}";
- String fullInfo = "{\"name\":\"name\",\"email\":\"foo@example\",\"website\":\"https://yahoo.com\",\"invoiceEmail\":\"invoiceEmail\",\"contactName\":\"contactName\",\"contactEmail\":\"contact@example.com\",\"address\":" + fullAddress + ",\"billingContact\":" + fullBillingContact + "}";
+ String fullContacts = "[{\"audiences\":[\"tenant\"],\"email\":\"contact1@example.com\"},{\"audiences\":[\"notifications\"],\"email\":\"contact2@example.com\"},{\"audiences\":[\"tenant\",\"notifications\"],\"email\":\"contact3@example.com\"}]";
+ String fullInfo = "{\"name\":\"name\",\"email\":\"foo@example\",\"website\":\"https://yahoo.com\",\"contactName\":\"contactName\",\"contactEmail\":\"contact@example.com\",\"address\":" + fullAddress + ",\"billingContact\":" + fullBillingContact + ",\"contacts\":" + fullContacts + "}";
// Now set all fields
var postFull =
@@ -168,6 +172,20 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest {
.roles(Set.of(Role.administrator(tenantName)));
tester.assertResponse(addressInfoResponse, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"All address fields must be set\"}", 400);
+ // at least one notification activity must be enabled
+ var contactsWithoutAudience = "{\"contacts\": [{\"email\": \"contact1@example.com\"}]}";
+ var contactsWithoutAudienceResponse = request("/application/v4/tenant/scoober/info", PUT)
+ .data(contactsWithoutAudience)
+ .roles(Set.of(Role.administrator(tenantName)));
+ tester.assertResponse(contactsWithoutAudienceResponse, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"at least one notification activity must be enabled\"}", 400);
+
+ // email needs to be present, not blank, and contain an @
+ var contactsWithInvalidEmail = "{\"contacts\": [{\"audiences\": [\"tenant\"],\"email\": \"contact1\"}]}";
+ var contactsWithInvalidEmailResponse = request("/application/v4/tenant/scoober/info", PUT)
+ .data(contactsWithInvalidEmail)
+ .roles(Set.of(Role.administrator(tenantName)));
+ tester.assertResponse(contactsWithInvalidEmailResponse, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"'email' needs to be an email address\"}", 400);
+
// updating a tenant that already has the fields set works
var basicInfo = "{\"contactName\": \"Scoober Rentals Inc.\", \"contactEmail\": \"foo@example.com\"}";
var basicInfoResponse = request("/application/v4/tenant/scoober/info", PUT)
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java
index 7f588367819..15c7dbf73ab 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java
@@ -74,7 +74,7 @@ public class SignatureFilterTest {
LastLoginInfo.EMPTY,
Optional.empty(),
ImmutableBiMap.of(),
- TenantInfo.EMPTY,
+ TenantInfo.empty(),
List.of(),
Optional.empty()));
tester.curator().writeApplication(new Application(appId, tester.clock().instant()));
@@ -120,7 +120,7 @@ public class SignatureFilterTest {
LastLoginInfo.EMPTY,
Optional.empty(),
ImmutableBiMap.of(publicKey, () -> "user"),
- TenantInfo.EMPTY,
+ TenantInfo.empty(),
List.of(),
Optional.empty()));
verifySecurityContext(requestOf(signer.signed(request.copy(), Method.POST, () -> new ByteArrayInputStream(hiBytes)), hiBytes),
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-info-after-created.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-info-after-created.json
index 942b5c1db45..56104d626dd 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-info-after-created.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-info-after-created.json
@@ -2,7 +2,7 @@
"name": "",
"email": "",
"website":"",
- "invoiceEmail":"",
"contactName": "administrator",
- "contactEmail": "administrator@tenant"
+ "contactEmail": "administrator@tenant",
+ "contacts": []
} \ No newline at end of file
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzAssertion.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzAssertion.java
index 49cc31fe8c2..a343ea6e8f0 100644
--- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzAssertion.java
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzAssertion.java
@@ -10,7 +10,14 @@ import java.util.OptionalLong;
*/
public class AthenzAssertion {
- public enum Effect { ALLOW, DENY }
+ public enum Effect {
+ ALLOW, DENY;
+
+ public static Effect valueOrNull(String value) {
+ try { return valueOf(value); }
+ catch (RuntimeException e) { return null; }
+ }
+ }
private final Long id;
private final Effect effect;
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java
index eef833c91a7..a6d18f3167c 100644
--- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java
@@ -229,7 +229,7 @@ public class DefaultZmsClient extends ClientBase implements ZmsClient {
athenzDomain.getName(), athenzPolicy));
HttpUriRequest request = RequestBuilder.put()
.setUri(uri)
- .setEntity(toJsonStringEntity(new AssertionEntity(athenzRole.toResourceNameString(), resourceName.toResourceNameString(), action)))
+ .setEntity(toJsonStringEntity(new AssertionEntity(athenzRole.toResourceNameString(), resourceName.toResourceNameString(), action, "ALLOW")))
.build();
execute(request, response -> readEntity(response, Void.class));
}
@@ -281,6 +281,7 @@ public class DefaultZmsClient extends ClientBase implements ZmsClient {
AthenzResourceName.fromString(a.getResource()),
a.getAction())
.id(a.getId())
+ .effect(AthenzAssertion.Effect.valueOrNull(a.getEffect()))
.build())
.collect(toList());
return Optional.of(new AthenzPolicy(entity.getName(), assertions));
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/AssertionEntity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/AssertionEntity.java
index 4ef83760b5a..f0fe383a55b 100644
--- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/AssertionEntity.java
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/AssertionEntity.java
@@ -17,20 +17,23 @@ public class AssertionEntity {
private final String resource;
private final String action;
private final Long id;
+ private final String effect;
- public AssertionEntity(String role, String resource, String action) {
- this(role, resource, action, null);
+ public AssertionEntity(String role, String resource, String action, String effect) {
+ this(role, resource, action, null, effect);
}
public AssertionEntity(@JsonProperty("role") String role,
@JsonProperty("resource") String resource,
@JsonProperty("action") String action,
- @JsonProperty("id") Long id) {
+ @JsonProperty("id") Long id,
+ @JsonProperty("effect") String effect) {
this.role = role;
this.resource = resource;
this.action = action;
this.id = id;
+ this.effect = effect;
}
public String getRole() {
@@ -45,6 +48,10 @@ public class AssertionEntity {
return action;
}
+ public String getEffect() {
+ return effect;
+ }
+
@JsonIgnore
public long getId() {
return id;