summaryrefslogtreecommitdiffstats
path: root/controller-server/src
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@vespa.ai>2023-11-02 14:56:16 +0100
committerBjørn Christian Seime <bjorncs@vespa.ai>2023-11-02 14:56:16 +0100
commit0488bdf5c2b6712a08618c6a6e49d85997429c74 (patch)
treefec54ba58d3e61fae24e9c48fdbfa07e030b1e1a /controller-server/src
parente64583fa0b618da67189152c10310293221dd8bc (diff)
Extend `/application/v4` with API for approving terms of service
Diffstat (limited to 'controller-server/src')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java20
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java25
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java24
4 files changed, 68 insertions, 3 deletions
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 7801efe504b..961925cf620 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
@@ -36,6 +36,7 @@ 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.TermsOfServiceApproval;
import java.net.URI;
import java.security.Principal;
@@ -101,6 +102,9 @@ public class TenantSerializer {
private static final String taxIdCodeField = "code";
private static final String purchaseOrderField = "purchaseOrder";
private static final String invoiceEmailField = "invoiceEmail";
+ private static final String tosApprovalField = "tosApproval";
+ private static final String tosApprovalAtField = "at";
+ private static final String tosApprovalByField = "by";
private static final String awsIdField = "awsId";
private static final String roleField = "role";
@@ -292,6 +296,7 @@ public class TenantSerializer {
private TenantBilling tenantInfoBillingContactFromSlime(Inspector billingObject) {
var taxIdInspector = billingObject.field(taxIdField);
var taxId = switch (taxIdInspector.type()) {
+ // TODO(bjorncs, 2023-11-02): Remove legacy tax id format
case STRING -> TaxId.legacy(taxIdInspector.asString());
case OBJECT -> {
var taxIdCountry = taxIdInspector.field(taxIdCountryField).asString();
@@ -304,6 +309,13 @@ public class TenantSerializer {
};
var purchaseOrder = new PurchaseOrder(billingObject.field(purchaseOrderField).asString());
var invoiceEmail = new Email(billingObject.field(invoiceEmailField).asString(), false);
+ var tosApprovalInspector = billingObject.field(tosApprovalField);
+ var tosApproval = switch (tosApprovalInspector.type()) {
+ case OBJECT -> new TermsOfServiceApproval(tosApprovalInspector.field(tosApprovalAtField).asString(),
+ tosApprovalInspector.field(tosApprovalByField).asString());
+ case NIX -> TermsOfServiceApproval.empty();
+ default -> throw new IllegalArgumentException(taxIdInspector.type().name());
+ };
return TenantBilling.empty()
.withContact(TenantContact.from(
@@ -313,7 +325,8 @@ public class TenantSerializer {
.withAddress(tenantInfoAddressFromSlime(billingObject.field("address")))
.withTaxId(taxId)
.withPurchaseOrder(purchaseOrder)
- .withInvoiceEmail(invoiceEmail);
+ .withInvoiceEmail(invoiceEmail)
+ .withToSApproval(tosApproval);
}
private List<TenantSecretStore> secretStoresFromSlime(Inspector secretStoresObject) {
@@ -382,6 +395,11 @@ public class TenantSerializer {
billingCursor.setString(purchaseOrderField, billingContact.getPurchaseOrder().value());
billingCursor.setString(invoiceEmailField, billingContact.getInvoiceEmail().getEmailAddress());
toSlime(billingContact.address(), billingCursor);
+ if (!billingContact.getToSApproval().isEmpty()) {
+ var tosApprovalCursor = billingCursor.setObject(tosApprovalField);
+ tosApprovalCursor.setString(tosApprovalAtField, billingContact.getToSApproval().approvedAt().toString());
+ tosApprovalCursor.setString(tosApprovalByField, billingContact.getToSApproval().approvedBy().get().getName());
+ }
}
private void toSlime(List<TenantSecretStore> tenantSecretStores, Cursor parentCursor) {
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 1fd8e7c8f3b..6cf38a1927c 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
@@ -87,6 +87,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretSto
import com.yahoo.vespa.hosted.controller.api.role.Role;
import com.yahoo.vespa.hosted.controller.api.role.RoleDefinition;
import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
+import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
import com.yahoo.vespa.hosted.controller.application.AssignedRotation;
import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.Deployment;
@@ -135,6 +136,7 @@ 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.TermsOfServiceApproval;
import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
import com.yahoo.yolean.Exceptions;
@@ -386,6 +388,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
private HttpResponse handlePOST(Path path, HttpRequest request) {
if (path.matches("/application/v4/tenant/{tenant}")) return createTenant(path.get("tenant"), request);
+ if (path.matches("/application/v4/tenant/{tenant}/terms-of-service")) return approveTermsOfService(path.get("tenant"), request);
if (path.matches("/application/v4/tenant/{tenant}/key")) return addDeveloperKey(path.get("tenant"), request);
if (path.matches("/application/v4/tenant/{tenant}/token/{tokenid}")) return generateToken(path.get("tenant"), path.get("tokenid"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return createApplication(path.get("tenant"), path.get("application"), request);
@@ -702,6 +705,10 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
taxIdCursor.setString("code", billingContact.getTaxId().code().value());
root.setString("purchaseOrder", billingContact.getPurchaseOrder().value());
root.setString("invoiceEmail", billingContact.getInvoiceEmail().getEmailAddress());
+ var tosApprovalCursor = root.setObject("tosApproval");
+ var tosApproval = billingContact.getToSApproval();
+ tosApprovalCursor.setString("at", !tosApproval.isEmpty() ? tosApproval.approvedAt().toString() : "");
+ tosApprovalCursor.setString("by", !tosApproval.isEmpty() ? tosApproval.approvedBy().get().getName() : "");
toSlime(billingContact.address(), root); // will create "address" on the parent
}
@@ -812,6 +819,10 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
billingCursor.setString("purchaseOrder", billingContact.getPurchaseOrder().value());
billingCursor.setString("invoiceEmail", billingContact.getInvoiceEmail().getEmailAddress());
toSlime(billingContact.address(), billingCursor);
+ var tosApprovalCursor = billingCursor.setObject("tosApproval");
+ var tosApproval = billingContact.getToSApproval();
+ tosApprovalCursor.setString("at", !tosApproval.isEmpty() ? tosApproval.approvedAt().toString() : "");
+ tosApprovalCursor.setString("by", !tosApproval.isEmpty() ? tosApproval.approvedBy().get().getName() : "");
}
private void toSlime(TenantContacts contacts, Cursor parentCursor) {
@@ -1239,6 +1250,20 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
return new SlimeJsonResponse(slime);
}
+ private HttpResponse approveTermsOfService(String tenant, HttpRequest req) {
+ if (controller.tenants().require(TenantName.from(tenant)).type() != Tenant.Type.cloud)
+ throw new IllegalArgumentException("Tenant '" + tenant + "' is not a cloud tenant");
+ var approvedBy = SimplePrincipal.of(req.getJDiscRequest().getUserPrincipal());
+ var approvedAt = controller.clock().instant();
+
+ controller.tenants().lockOrThrow(TenantName.from(tenant), LockedTenant.Cloud.class, t -> {
+ var updatedTenant = t.withInfo(t.get().info().withBilling(t.get().info().billingContact().withToSApproval(
+ new TermsOfServiceApproval(approvedAt, approvedBy))));
+ controller.tenants().store(updatedTenant);
+ });
+ return new MessageResponse("Terms of service approved by %s".formatted(approvedBy.getName()));
+ }
+
private HttpResponse addDeveloperKey(String tenantName, HttpRequest request) {
if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud)
throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant");
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 f2fc43933df..493d4df90a9 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
@@ -31,6 +31,7 @@ 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.TermsOfServiceApproval;
import org.junit.jupiter.api.Test;
import java.net.URI;
@@ -240,6 +241,7 @@ public class TenantSerializerTest {
.withPurchaseOrder(new PurchaseOrder("PO42"))
.withTaxId(new TaxId("NO", "no_vat", "123456789MVA"))
.withInvoiceEmail(new Email("billing@mycomp.any", false))
+ .withToSApproval(new TermsOfServiceApproval(Instant.ofEpochMilli(1234L), new SimplePrincipal("ceo@mycomp.any")))
);
Slime slime = new Slime();
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 f8ae7c8ea50..3c57f812a48 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
@@ -112,7 +112,11 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest {
"code": ""
},
"purchaseOrder":"",
- "invoiceEmail":""
+ "invoiceEmail":"",
+ "tosApproval": {
+ "at": "",
+ "by": ""
+ }
}
""";
var request = request("/application/v4/tenant/scoober/info/billing", GET)
@@ -143,6 +147,10 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest {
.roles(Set.of(Role.administrator(tenantName)));
tester.assertResponse(updateRequest, "{\"message\":\"Tenant info updated\"}", 200);
+ var approveToSRequest = request("/application/v4/tenant/scoober/terms-of-service", POST)
+ .data("{}").roles(Set.of(Role.administrator(tenantName)));
+ tester.assertResponse(approveToSRequest, "{\"message\":\"Terms of service approved by user@test\"}", 200);
+
expectedResponse = """
{
"contact": {
@@ -158,6 +166,10 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest {
},
"purchaseOrder":"PO9001",
"invoiceEmail":"billing@mycomp.any",
+ "tosApproval": {
+ "at": "2020-09-13T12:26:40Z",
+ "by": "user@test"
+ },
"address": {
"addressLines":"addressLines",
"postalCodeOrZip":"postalCodeOrZip",
@@ -245,7 +257,11 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest {
"code": ""
},
"purchaseOrder":"",
- "invoiceEmail":""
+ "invoiceEmail":"",
+ "tosApproval": {
+ "at": "",
+ "by": ""
+ }
},
"contacts": [
{"audiences":["tenant"],"email":"contact1@example.com","emailVerified":false}
@@ -287,6 +303,10 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest {
"city":"city",
"stateRegionProvince":"stateRegionProvince",
"country":"country"
+ },
+ "tosApproval": {
+ "at": "",
+ "by": ""
}
},
"contacts": [