diff options
Diffstat (limited to 'controller-server/src')
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": [ |