aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorValerij Fredriksen <freva@users.noreply.github.com>2019-12-02 15:04:19 +0100
committerGitHub <noreply@github.com>2019-12-02 15:04:19 +0100
commit610dcc8a32847c0b017e55319f2c5977004c198f (patch)
tree5497df6a702d5af3850004f9b10ea5af73bb339a
parent20d1f2f1a96b2ae326127ee44e5bd50eda0bc9f5 (diff)
parent65ac775d68a898f7bc387b2b7350fdbf30db70dc (diff)
Merge pull request #11465 from vespa-engine/freva/create-user-v1-user
Create /user/v1/user
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzDbMock.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java14
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java15
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java10
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java13
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java1
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java44
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java110
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControl.java9
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java8
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java5
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java72
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainerTest.java3
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerCloudTest.java18
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java9
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java25
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/responses/athensDomain-list.json4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilterTest.java4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiOnPremTest.java66
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java57
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-athenz.json57
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json60
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-applications.json11
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java14
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java4
29 files changed, 556 insertions, 93 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzDbMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzDbMock.java
index 7ab1ba36aa6..8e21d8cbf20 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzDbMock.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzDbMock.java
@@ -57,6 +57,11 @@ public class AthenzDbMock {
return this;
}
+ public Domain deleteTenantAdmin(AthenzIdentity identity) {
+ tenantAdmins.remove(identity);
+ return this;
+ }
+
/**
* Simulates establishing Vespa tenancy in Athens.
*/
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java
index a80843ad252..096a1af2824 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java
@@ -74,6 +74,20 @@ public class ZmsClientMock implements ZmsClient {
}
@Override
+ public void addRoleMember(AthenzRole role, AthenzIdentity member) {
+ if ( ! role.roleName().equals("tenancy.vespa.hosting.admin"))
+ throw new IllegalArgumentException("Mock only supports adding tenant admins, not " + role.roleName());
+ getDomainOrThrow(role.domain(), true).tenantAdmin(member);
+ }
+
+ @Override
+ public void deleteRoleMember(AthenzRole role, AthenzIdentity member) {
+ if ( ! role.roleName().equals("tenancy.vespa.hosting.admin"))
+ throw new IllegalArgumentException("Mock only supports deleting tenant admins, not " + role.roleName());
+ getDomainOrThrow(role.domain(), true).deleteTenantAdmin(member);
+ }
+
+ @Override
public boolean getMembership(AthenzRole role, AthenzIdentity identity) {
if (role.roleName().equals("admin")) {
return getDomainOrThrow(role.domain(), false).admins.contains(identity);
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java
index 4601ebde925..95669f7f05d 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java
@@ -32,8 +32,9 @@ enum PathGroup {
"/provision/v2/{*}",
"/zone/v2/{*}"),
- /** Paths used for creating user tenants. */
- user("/application/v4/user"),
+ /** Paths used for creating and reading user resources. */
+ user("/application/v4/user",
+ "/athenz/v1/{*}"),
/** Paths used for creating tenants with proper access control. */
tenant(Matcher.tenant,
@@ -95,6 +96,7 @@ enum PathGroup {
"/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{ignored}/global-rotation/{*}",
"/application/v4/tenant/{tenant}/application/{application}/metering"),
+ // TODO jonmv: remove
/** Path used to restart development nodes. */
developmentRestart(Matcher.tenant,
Matcher.application,
@@ -105,6 +107,7 @@ enum PathGroup {
"/application/v4/tenant/{tenant}/application/{application}/environment/dev/region/{region}/instance/{instance}/restart",
"/application/v4/tenant/{tenant}/application/{application}/environment/perf/region/{region}/instance/{instance}/restart"),
+ // TODO jonmv: remove
/** Path used to restart production nodes. */
productionRestart(Matcher.tenant,
Matcher.application,
@@ -131,6 +134,7 @@ enum PathGroup {
"/application/v4/tenant/{tenant}/application/{application}/environment/perf/region/{region}/instance/{instance}",
"/application/v4/tenant/{tenant}/application/{application}/environment/perf/region/{region}/instance/{instance}/deploy"),
+ // TODO jonmv: remove
/** Paths used for production deployments. */
productionDeployment(Matcher.tenant,
Matcher.application,
@@ -168,14 +172,17 @@ enum PathGroup {
"/application/v4/tenant/"),
/** Paths which contain (not very strictly) classified information about, e.g., customers. */
- classifiedInfo("/athenz/v1/{*}",
- "/cost/v1/{*}",
+ classifiedInfo("/cost/v1/{*}",
"/deployment/v1/{*}",
"/",
"/d/{*}",
"/static/{*}",
"/statuspage/v1/{*}"),
+ /** Same as classifiedInfo, but with optional /api prefix */
+ classifiedApiInfo(Optional.of("/api"),
+ "/user/v1/user"),
+
/** Paths providing public information. */
publicInfo(Optional.of("/api"),
"/badge/v1/{*}",
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java
index 074d3ef7e95..e27fb0fbf27 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java
@@ -34,9 +34,9 @@ enum Policy {
.in(SystemName.all())),
/** Access to create a user tenant in select systems. */
- userCreate(Privilege.grant(Action.update)
- .on(PathGroup.user)
- .in(SystemName.main, SystemName.cd, SystemName.dev)),
+ user(Privilege.grant(Action.create, Action.update)
+ .on(PathGroup.user)
+ .in(SystemName.main, SystemName.cd, SystemName.dev)),
/** Access to create a tenant in select systems. */
tenantCreate(Privilege.grant(Action.create)
@@ -118,6 +118,10 @@ enum Policy {
.on(PathGroup.allExcept(PathGroup.classifiedOperator))
.in(SystemName.main, SystemName.cd, SystemName.dev)),
+ classifiedApiRead(Privilege.grant(Action.read)
+ .on(PathGroup.classifiedApiInfo)
+ .in(SystemName.all())),
+
/** Read access to public info. */
publicRead(Privilege.grant(Action.read)
.on(PathGroup.publicInfo)
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java
index 67efdc3017d..10df7604667 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java
@@ -22,8 +22,9 @@ public enum RoleDefinition {
/** Base role which every user is part of. */
everyone(Policy.classifiedRead,
+ Policy.classifiedApiRead,
Policy.publicRead,
- Policy.userCreate,
+ Policy.user,
Policy.tenantCreate),
/** Application reader which can see all information about an application, its tenant and deployments. */
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java
index d1a6e39a1dd..e794334232f 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java
@@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller;
import com.yahoo.config.provision.TenantName;
import com.yahoo.vespa.curator.Lock;
+import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade;
import com.yahoo.vespa.hosted.controller.concurrent.Once;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.security.AccessControl;
@@ -61,9 +62,10 @@ public class TenantController {
.collect(Collectors.toList());
}
+ // TODO jonmv: Remove.
/** Returns the list of tenants accessible to the given user. */
public List<Tenant> asList(Credentials credentials) {
- return accessControl.accessibleTenants(asList(), credentials);
+ return ((AthenzFacade) accessControl).accessibleTenants(asList(), credentials);
}
/** Locks a tenant for modification and applies the given action. */
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java
index bb6777b9e27..7ace62ab44d 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java
@@ -11,6 +11,7 @@ import com.yahoo.vespa.athenz.api.AthenzPrincipal;
import com.yahoo.vespa.athenz.api.AthenzResourceName;
import com.yahoo.vespa.athenz.api.AthenzRole;
import com.yahoo.vespa.athenz.api.AthenzService;
+import com.yahoo.vespa.athenz.api.AthenzUser;
import com.yahoo.vespa.athenz.api.OktaAccessToken;
import com.yahoo.vespa.athenz.api.OktaIdentityToken;
import com.yahoo.vespa.athenz.client.zms.RoleAction;
@@ -174,7 +175,13 @@ public class AthenzFacade implements AccessControl {
athenzCredentials.identityToken(), athenzCredentials.accessToken());
}
- @Override
+ /**
+ * Returns the list of tenants to which a user has access.
+ * @param tenants the list of all known tenants
+ * @param credentials the credentials of user whose tenants to list
+ * @return the list of tenants the given user has access to
+ */
+ // TODO jonmv: Remove
public List<Tenant> accessibleTenants(List<Tenant> tenants, Credentials credentials) {
AthenzIdentity identity = ((AthenzPrincipal) credentials.user()).getIdentity();
List<AthenzDomain> userDomains = ztsClient.getTenantDomains(service, identity, "admin");
@@ -184,6 +191,10 @@ public class AthenzFacade implements AccessControl {
.collect(Collectors.toUnmodifiableList());
}
+ public void addTenantAdmin(AthenzDomain tenantDomain, AthenzUser user) {
+ zmsClient.addRoleMember(new AthenzRole(tenantDomain, "tenancy." + service.getFullName() + ".admin"), user);
+ }
+
private void deleteApplication(AthenzDomain domain, ApplicationName application, OktaIdentityToken identityToken, OktaAccessToken accessToken) {
log("deleteProviderResourceGroup(tenantDomain=%s, providerDomain=%s, service=%s, resourceGroup=%s)",
domain, service.getDomain().getName(), service.getName(), application);
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 114df26015f..42471cb9001 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
@@ -325,6 +325,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
: new ResourceResponse(request, "user", "tenant");
}
+ // TODO jonmv: Move to Athenz API.
private HttpResponse authenticatedUser(HttpRequest request) {
Principal user = requireUserPrincipal(request);
if (user == null)
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java
index d36a7487e59..26c4bf6292a 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java
@@ -1,25 +1,30 @@
// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.restapi.athenz;
+import com.yahoo.config.provision.SystemName;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.LoggingRequestHandler;
import com.yahoo.jdisc.http.HttpRequest.Method;
+import com.yahoo.restapi.ErrorResponse;
+import com.yahoo.restapi.MessageResponse;
import com.yahoo.restapi.Path;
+import com.yahoo.restapi.ResourceResponse;
+import com.yahoo.restapi.SlimeJsonResponse;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Slime;
import com.yahoo.vespa.athenz.api.AthenzDomain;
+import com.yahoo.vespa.athenz.api.AthenzPrincipal;
+import com.yahoo.vespa.athenz.api.AthenzUser;
import com.yahoo.vespa.hosted.controller.Controller;
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.entity.EntityService;
import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade;
-import com.yahoo.restapi.ErrorResponse;
-import com.yahoo.restapi.ResourceResponse;
-import com.yahoo.restapi.SlimeJsonResponse;
import com.yahoo.yolean.Exceptions;
import java.util.Map;
+import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -34,11 +39,13 @@ public class AthenzApiHandler extends LoggingRequestHandler {
private final static Logger log = Logger.getLogger(AthenzApiHandler.class.getName());
private final AthenzFacade athenz;
+ private final AthenzDomain sandboxDomain;
private final EntityService properties;
public AthenzApiHandler(Context parentCtx, AthenzFacade athenz, Controller controller) {
super(parentCtx);
this.athenz = athenz;
+ this.sandboxDomain = new AthenzDomain(sandboxDomainIn(controller.system()));
this.properties = controller.serviceRegistry().entityService();
}
@@ -48,6 +55,7 @@ public class AthenzApiHandler extends LoggingRequestHandler {
try {
switch (method) {
case GET: return get(request);
+ case POST: return post(request);
default: return ErrorResponse.methodNotAllowed("Method '" + method + "' is unsupported");
}
}
@@ -70,6 +78,13 @@ public class AthenzApiHandler extends LoggingRequestHandler {
request.getUri().getPath()));
}
+ private HttpResponse post(HttpRequest request) {
+ Path path = new Path(request.getUri());
+ if (path.matches("/athenz/v1/user")) return signup(request);
+ return ErrorResponse.notFoundError(String.format("No '%s' handler at '%s'", request.getMethod(),
+ request.getUri().getPath()));
+ }
+
private HttpResponse root(HttpRequest request) {
return new ResourceResponse(request, "domains", "properties");
}
@@ -96,4 +111,27 @@ public class AthenzApiHandler extends LoggingRequestHandler {
return new SlimeJsonResponse(slime);
}
+ private HttpResponse signup(HttpRequest request) {
+ AthenzUser user = athenzUser(request);
+ athenz.addTenantAdmin(sandboxDomain, user);
+ return new MessageResponse("User '" + user.getName() + "' added to admin role of '" + sandboxDomain.getName() + "'");
+ }
+
+ private static AthenzUser athenzUser(HttpRequest request) {
+ return Optional.ofNullable(request.getJDiscRequest().getUserPrincipal()).filter(AthenzPrincipal.class::isInstance)
+ .map(AthenzPrincipal.class::cast)
+ .map(AthenzPrincipal::getIdentity)
+ .filter(AthenzUser.class::isInstance)
+ .map(AthenzUser.class::cast)
+ .orElseThrow(() -> new IllegalArgumentException("No Athenz user principal on request"));
+ }
+
+ static String sandboxDomainIn(SystemName system) {
+ switch (system) {
+ case main: return "vespa.vespa.tenants.sandbox";
+ case cd: return "vespa.vespa.cd.tenants.sandbox";
+ default: throw new IllegalArgumentException("No sandbox domain in system '" + system + "'");
+ }
+ }
+
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
index 8a3adcce30e..3889058e1cf 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
@@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.restapi.user;
import com.google.inject.Inject;
import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.TenantName;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
@@ -19,22 +20,27 @@ import com.yahoo.slime.SlimeStream;
import com.yahoo.vespa.config.SlimeUtils;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.LockedTenant;
+import com.yahoo.vespa.hosted.controller.api.integration.ApplicationIdSnapshot;
import com.yahoo.vespa.hosted.controller.api.integration.user.Roles;
import com.yahoo.vespa.hosted.controller.api.integration.user.User;
import com.yahoo.vespa.hosted.controller.api.integration.user.UserId;
import com.yahoo.vespa.hosted.controller.api.integration.user.UserManagement;
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.api.role.TenantRole;
import com.yahoo.vespa.hosted.controller.restapi.application.EmptyResponse;
import com.yahoo.yolean.Exceptions;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.logging.Level;
@@ -51,6 +57,7 @@ public class UserApiHandler extends LoggingRequestHandler {
private final static Logger log = Logger.getLogger(UserApiHandler.class.getName());
private static final String optionalPrefix = "/api";
+ private static final TenantName sandboxTenant = TenantName.from("sandbox");
private final UserManagement users;
private final Controller controller;
@@ -84,6 +91,7 @@ public class UserApiHandler extends LoggingRequestHandler {
}
private HttpResponse handleGET(Path path, HttpRequest request) {
+ if (path.matches("/user/v1/user")) return userMetadata(request);
if (path.matches("/user/v1/tenant/{tenant}")) return listTenantRoleMembers(path.get("tenant"));
if (path.matches("/user/v1/tenant/{tenant}/application/{application}")) return listApplicationRoleMembers(path.get("tenant"), path.get("application"));
@@ -113,6 +121,72 @@ public class UserApiHandler extends LoggingRequestHandler {
return response;
}
+ private HttpResponse userMetadata(HttpRequest request) {
+ User user = getAttribute(request, User.ATTRIBUTE_NAME, User.class);
+ Set<Role> roles = getAttribute(request, SecurityContext.ATTRIBUTE_NAME, SecurityContext.class).roles();
+
+ ApplicationIdSnapshot snapshot = controller.applicationIdSnapshot();
+ Map<TenantName, List<TenantRole>> tenantRolesByTenantName = roles.stream()
+ .flatMap(role -> filterTenantRoles(role).stream())
+ .distinct()
+ .sorted(Comparator.comparing(Role::definition).reversed())
+ .collect(Collectors.groupingBy(TenantRole::tenant, Collectors.toList()));
+
+ // List of operator roles, currently only one available, but possible to extend
+ List<Role> operatorRoles = roles.stream()
+ .filter(role -> role.definition().equals(RoleDefinition.hostedOperator))
+ .collect(Collectors.toList());
+
+
+ Slime slime = new Slime();
+ Cursor root = slime.setObject();
+
+ toSlime(root.setObject("user"), user);
+
+ Cursor tenants = root.setObject("tenants");
+ InstanceName userInstance = InstanceName.from(user.nickname());
+ tenantRolesByTenantName.keySet().stream()
+ .sorted()
+ .forEach(tenant -> {
+ Cursor tenantObject = tenants.setObject(tenant.value());
+
+ Cursor tenantRolesObject = tenantObject.setArray("roles");
+ tenantRolesByTenantName.getOrDefault(tenant, List.of())
+ .forEach(role -> tenantRolesObject.addString(role.definition().name()));
+
+ Cursor tenantApplicationsObject = tenantObject.setObject("applications");
+ accessibleInstances(snapshot, tenant, userInstance).entrySet().stream()
+ .sorted(Map.Entry.comparingByKey())
+ .forEach(appInstances -> {
+ Cursor applicationObject = tenantApplicationsObject.setObject(appInstances.getKey().value());
+ Cursor applicationInstancesObject = applicationObject.setArray("instances");
+ appInstances.getValue().stream()
+ .sorted()
+ .forEach(instance -> applicationInstancesObject.addString(instance.value()));
+ });
+ });
+
+ if (!operatorRoles.isEmpty()) {
+ Cursor operator = root.setArray("operator");
+ operatorRoles.forEach(role -> operator.addString(role.definition().name()));
+ }
+
+ return new SlimeJsonResponse(slime);
+ }
+
+ private Map<ApplicationName, Set<InstanceName>> accessibleInstances(
+ ApplicationIdSnapshot snapshot, TenantName tenant, InstanceName userInstance) {
+ return snapshot.applications(tenant).stream()
+ .filter(application -> controller.system().isPublic()
+ || ! sandboxTenant.equals(tenant)
+ || snapshot.instances(tenant, application).contains(userInstance))
+ .collect(Collectors.toUnmodifiableMap(
+ Function.identity(),
+ application -> controller.system().isPublic() || ! sandboxTenant.equals(tenant) ?
+ snapshot.instances(tenant, application) :
+ Set.of(userInstance)));
+ }
+
private HttpResponse listTenantRoleMembers(String tenantName) {
Slime slime = new Slime();
Cursor root = slime.setObject();
@@ -151,14 +225,7 @@ public class UserApiHandler extends LoggingRequestHandler {
Cursor usersArray = root.setArray("users");
memberships.forEach((user, userRoles) -> {
Cursor userObject = usersArray.addObject();
- userObject.setString("name", user.name());
- userObject.setString("email", user.email());
- if (user.nickname() != null) {
- userObject.setString("nickname", user.nickname());
- }
- if (user.picture()!= null) {
- userObject.setString("picture", user.picture());
- }
+ toSlime(userObject, user);
Cursor rolesObject = userObject.setObject("roles");
for (Role role : roles) {
@@ -169,6 +236,13 @@ public class UserApiHandler extends LoggingRequestHandler {
});
}
+ private void toSlime(Cursor userObject, User user) {
+ if (user.name() != null) userObject.setString("name", user.name());
+ userObject.setString("email", user.email());
+ if (user.nickname() != null) userObject.setString("nickname", user.nickname());
+ if (user.picture() != null) userObject.setString("picture", user.picture());
+ }
+
private HttpResponse addTenantRoleMember(String tenantName, HttpRequest request) {
Inspector requestObject = bodyInspector(request);
if (requestObject.field("roles").valid()) {
@@ -299,4 +373,24 @@ public class UserApiHandler extends LoggingRequestHandler {
}
}
+ private static Set<TenantRole> filterTenantRoles(Role role) {
+ if (!(role instanceof TenantRole))
+ return Set.of();
+
+ TenantRole tenantRole = (TenantRole) role;
+ if (tenantRole.definition() == RoleDefinition.administrator || tenantRole.definition() == RoleDefinition.developer)
+ return Set.of(tenantRole);
+
+ if (tenantRole.definition() == RoleDefinition.athenzTenantAdmin)
+ return Set.of(Role.administrator(tenantRole.tenant()), Role.developer(tenantRole.tenant()));
+
+ return Set.of();
+ }
+
+ private static <T> T getAttribute(HttpRequest request, String attributeName, Class<T> clazz) {
+ return Optional.ofNullable(request.getJDiscRequest().context().get(attributeName))
+ .filter(clazz::isInstance)
+ .map(clazz::cast)
+ .orElseThrow(() -> new IllegalArgumentException("Attribute '" + attributeName + "' was not set on request"));
+ }
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControl.java
index 66c87a8eefd..8db96801a36 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControl.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControl.java
@@ -1,6 +1,5 @@
package com.yahoo.vespa.hosted.controller.security;
-import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.TenantName;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
@@ -63,12 +62,4 @@ public interface AccessControl {
*/
void deleteApplication(TenantAndApplicationId id, Credentials credentials);
- /**
- * Returns the list of tenants to which a user has access.
- * @param tenants the list of all known tenants
- * @param credentials the credentials of user whose tenants to list
- * @return the list of tenants the given user has access to
- */
- List<Tenant> accessibleTenants(List<Tenant> tenants, Credentials credentials);
-
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java
index 363dc348ad3..4f3a55dbea0 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java
@@ -14,7 +14,6 @@ import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import java.util.Collections;
import java.util.List;
/**
@@ -73,11 +72,4 @@ public class CloudAccessControl implements AccessControl {
userManagement.deleteRole(role);
}
- @Override
- public List<Tenant> accessibleTenants(List<Tenant> tenants, Credentials credentials) {
- // TODO: Get credential things (token with roles or something) and check what it's good for.
- // TODO ... or ignore this here, and compute it somewhere else.
- return Collections.emptyList();
- }
-
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java
index 52ac9c8088a..5eab75e4282 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java
@@ -13,8 +13,6 @@ import com.yahoo.config.provision.RegionName;
import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.TenantName;
import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.flags.Flags;
-import com.yahoo.vespa.flags.InMemoryFlagSource;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.EndpointStatus;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
@@ -557,7 +555,8 @@ public class ControllerTest {
.allow(ValidationId.globalEndpointChange)
.build();
context.submit(applicationPackage);
- tester.applications().deleteApplication(context.application().id(), tester.controllerTester().credentialsFor(context.application().id()));
+ tester.applications().deleteApplication(context.application().id(),
+ tester.controllerTester().credentialsFor(context.application().id().tenant()));
try (RotationLock lock = tester.applications().rotationRepository().lock()) {
assertTrue("Rotation is unassigned",
tester.applications().rotationRepository().availableRotations(lock)
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
index ff6a5d3795f..82d35701e7e 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
@@ -26,6 +26,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMavenRepository;
+import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.SystemApplication;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
@@ -38,7 +39,9 @@ import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb;
import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
import com.yahoo.vespa.hosted.controller.security.AthenzCredentials;
import com.yahoo.vespa.hosted.controller.security.AthenzTenantSpec;
+import com.yahoo.vespa.hosted.controller.security.CloudTenantSpec;
import com.yahoo.vespa.hosted.controller.security.Credentials;
+import com.yahoo.vespa.hosted.controller.security.TenantSpec;
import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import com.yahoo.vespa.hosted.controller.versions.ControllerVersion;
@@ -271,12 +274,24 @@ public final class ControllerTester {
return domain;
}
- public Optional<AthenzDomain> domainOf(TenantAndApplicationId id) {
- Tenant tenant = controller().tenants().require(id.tenant());
- return tenant.type() == Tenant.Type.athenz ? Optional.of(((AthenzTenant) tenant).domain()) : Optional.empty();
+ public TenantName createTenant(String tenantName) {
+ return createTenant(tenantName, "domain" + nextDomainId.getAndIncrement(),
+ nextPropertyId.getAndIncrement());
+ }
+
+ public TenantName createTenant(String tenantName, Tenant.Type type) {
+ switch (type) {
+ case athenz: return createTenant(tenantName, "domain" + nextDomainId.getAndIncrement(), nextPropertyId.getAndIncrement());
+ case cloud: return createCloudTenant(tenantName);
+ default: throw new UnsupportedOperationException();
+ }
}
- public TenantName createTenant(String tenantName, String domainName, Long propertyId, Optional<Contact> contact) {
+ public TenantName createTenant(String tenantName, String domainName, Long propertyId) {
+ return createAthenzTenant(tenantName, domainName, propertyId, Optional.empty());
+ }
+
+ private TenantName createAthenzTenant(String tenantName, String domainName, Long propertyId, Optional<Contact> contact) {
TenantName name = TenantName.from(tenantName);
Optional<Tenant> existing = controller().tenants().get(name);
if (existing.isPresent()) return name;
@@ -296,37 +311,48 @@ public final class ControllerTester {
return name;
}
- public TenantName createTenant(String tenantName) {
- return createTenant(tenantName, "domain" + nextDomainId.getAndIncrement(),
- nextPropertyId.getAndIncrement());
+ private TenantName createCloudTenant(String tenantName) {
+ TenantName tenant = TenantName.from(tenantName);
+ TenantSpec spec = new CloudTenantSpec(tenant, "token");
+ controller().tenants().create(spec, new Credentials(new SimplePrincipal("dev")));
+ return tenant;
}
- public TenantName createTenant(String tenantName, String domainName, Long propertyId) {
- return createTenant(tenantName, domainName, propertyId, Optional.empty());
- }
+ public Optional<Credentials> credentialsFor(TenantName tenantName) {
+ Tenant tenant = controller().tenants().require(tenantName);
+
+ switch (tenant.type()) {
+ case athenz:
+ return Optional.of(new AthenzCredentials(new AthenzPrincipal(new AthenzUser("user")),
+ ((AthenzTenant) tenant).domain(),
+ new OktaIdentityToken("okta-identity-token"),
+ new OktaAccessToken("okta-access-token")));
+ case cloud:
+ return Optional.of(new Credentials(new SimplePrincipal("dev")));
- public Optional<Credentials> credentialsFor(TenantAndApplicationId id) {
- return domainOf(id).map(domain -> new AthenzCredentials(new AthenzPrincipal(new AthenzUser("user")),
- domain,
- new OktaIdentityToken("okta-identity-token"),
- new OktaAccessToken("okta-access-token")));
+ default:
+ return Optional.empty();
+ }
}
- public Application createApplication(TenantName tenant, String applicationName, String instanceName) {
- return createApplication(tenant, applicationName, instanceName, nextProjectId.getAndIncrement());
+ public Application createApplication(String tenant, String applicationName, String instanceName) {
+ Application application = createApplication(tenant, applicationName);
+ controller().applications().createInstance(application.id().instance(instanceName));
+ return application;
}
- public Application createApplication(TenantName tenant, String applicationName, String instanceName, long projectId) {
- TenantAndApplicationId applicationId = TenantAndApplicationId.from(tenant.value(), applicationName);
- controller().applications().createApplication(applicationId, credentialsFor(applicationId));
- controller().applications().lockApplicationOrThrow(applicationId, application ->
- controller().applications().store(application.withProjectId(OptionalLong.of(projectId))));
- controller().applications().createInstance(applicationId.instance(instanceName));
+ public Application createApplication(String tenant, String applicationName) {
+ TenantAndApplicationId applicationId = TenantAndApplicationId.from(tenant, applicationName);
+ controller().applications().getApplication(applicationId)
+ .orElseGet(() -> controller().applications().createApplication(applicationId, credentialsFor(applicationId.tenant())));
+ controller().applications().lockApplicationOrThrow(applicationId, app ->
+ controller().applications().store(app.withProjectId(OptionalLong.of(nextProjectId.getAndIncrement()))));
Application application = controller().applications().requireApplication(applicationId);
assertTrue(application.projectId().isPresent());
return application;
}
+
public void deploy(ApplicationId id, ZoneId zone) {
deploy(id, zone, new ApplicationPackage(new byte[0]));
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java
index 6016eed4704..6e84c28ff85 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java
@@ -118,7 +118,7 @@ public class DeploymentContext {
private void createTenantAndApplication() {
try {
var tenant = tester.createTenant(instanceId.tenant().value());
- tester.createApplication(tenant, instanceId.application().value(), instanceId.instance().value());
+ tester.createApplication(tenant.value(), instanceId.application().value(), instanceId.instance().value());
} catch (IllegalArgumentException ignored) { } // Tenant and or application may already exist with custom setup.
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainerTest.java
index 0bab1cee392..ff72a2f7231 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainerTest.java
@@ -7,7 +7,6 @@ import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.NodeResources;
import com.yahoo.config.provision.NodeType;
-import com.yahoo.config.provision.TenantName;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.ControllerTester;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
@@ -30,7 +29,7 @@ public class ClusterInfoMaintainerTest {
@Test
public void maintain() {
tester.createTenant("tenant1", "domain123", 321L);
- ApplicationId app = tester.createApplication(TenantName.from("tenant1"), "app1", "default", 123).id().defaultInstance();
+ ApplicationId app = tester.createApplication("tenant1", "app1", "default").id().defaultInstance();
ZoneId zone = ZoneId.from("dev", "us-east-1");
tester.deploy(app, zone);
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java
index f7d451ab931..a1c83cb488d 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java
@@ -221,7 +221,7 @@ public class JobRunnerTest {
// Thread is still trying to deploy tester -- delete application, and see all data is garbage collected.
assertEquals(Collections.singletonList(runId), jobs.active().stream().map(run -> run.id()).collect(Collectors.toList()));
- tester.controllerTester().controller().applications().deleteApplication(TenantAndApplicationId.from(id), tester.controllerTester().credentialsFor(TenantAndApplicationId.from(id)));
+ tester.controllerTester().controller().applications().deleteApplication(TenantAndApplicationId.from(id), tester.controllerTester().credentialsFor(id.tenant()));
assertEquals(Collections.emptyList(), jobs.active());
assertEquals(runId, jobs.last(id, systemTest).get().id());
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerCloudTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerCloudTest.java
index 0c0d1f433cd..9473e88acf8 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerCloudTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerCloudTest.java
@@ -2,6 +2,7 @@ package com.yahoo.vespa.hosted.controller.restapi;
import com.yahoo.application.container.handler.Request;
import com.yahoo.config.provision.SystemName;
+import com.yahoo.vespa.hosted.controller.api.integration.user.User;
import com.yahoo.vespa.hosted.controller.api.role.Role;
import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
@@ -27,7 +28,6 @@ public class ControllerContainerCloudTest extends ControllerContainerTest {
protected String variablePartXml() {
return " <component id='com.yahoo.vespa.hosted.controller.security.CloudAccessControlRequests'/>\n" +
" <component id='com.yahoo.vespa.hosted.controller.security.CloudAccessControl'/>\n" +
- " <component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.MockUserManagement'/>\n" +
" <handler id='com.yahoo.vespa.hosted.controller.restapi.application.ApplicationApiHandler'>\n" +
" <binding>http://*/application/v4/*</binding>\n" +
@@ -41,11 +41,6 @@ public class ControllerContainerCloudTest extends ControllerContainerTest {
" <binding>http://*/api/zone/v1/*</binding>\n" +
" </handler>\n" +
- " <handler id='com.yahoo.vespa.hosted.controller.restapi.user.UserApiHandler'>\n" +
- " <binding>http://*/user/v1/*</binding>\n" +
- " <binding>http://*/api/user/v1/*</binding>\n" +
- " </handler>\n" +
-
" <http>\n" +
" <server id='default' port='8080' />\n" +
" <filtering>\n" +
@@ -69,7 +64,8 @@ public class ControllerContainerCloudTest extends ControllerContainerTest {
private final String path;
private final Request.Method method;
private byte[] data = new byte[0];
- private Principal user = () -> "user@test";
+ private Principal principal = () -> "user@test";
+ private User user;
private Set<Role> roles = Set.of(Role.everyone());
private RequestBuilder(String path, Request.Method method) {
@@ -79,13 +75,15 @@ public class ControllerContainerCloudTest extends ControllerContainerTest {
public RequestBuilder data(byte[] data) { this.data = data; return this; }
public RequestBuilder data(String data) { this.data = data.getBytes(StandardCharsets.UTF_8); return this; }
- public RequestBuilder user(String user) { this.user = new SimplePrincipal(user); return this; }
+ public RequestBuilder principal(String principal) { this.principal = new SimplePrincipal(principal); return this; }
+ public RequestBuilder user(User user) { this.user = user; return this; }
public RequestBuilder roles(Set<Role> roles) { this.roles = roles; return this; }
@Override
public Request get() {
- Request request = new Request("http://localhost:8080" + path, data, method, user);
- request.getAttributes().put(SecurityContext.ATTRIBUTE_NAME, new SecurityContext(user, roles));
+ Request request = new Request("http://localhost:8080" + path, data, method, principal);
+ request.getAttributes().put(SecurityContext.ATTRIBUTE_NAME, new SecurityContext(principal, roles));
+ if (user != null) request.getAttributes().put(User.ATTRIBUTE_NAME, user);
request.getHeaders().put("Content-Type", "application/json");
return request;
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
index dc6abcb2616..d37df2cc313 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
@@ -34,8 +34,8 @@ import static org.junit.Assert.assertEquals;
*/
public class ControllerContainerTest {
- private static final AthenzUser hostedOperator = AthenzUser.fromUserId("alice");
- private static final AthenzUser defaultUser = AthenzUser.fromUserId("bob");
+ protected static final AthenzUser hostedOperator = AthenzUser.fromUserId("alice");
+ protected static final AthenzUser defaultUser = AthenzUser.fromUserId("bob");
protected JDisc container;
@@ -76,6 +76,7 @@ public class ControllerContainerTest {
" <component id='com.yahoo.vespa.hosted.controller.maintenance.ControllerMaintenance'/>\n" +
" <component id='com.yahoo.vespa.hosted.controller.maintenance.JobControl'/>\n" +
" <component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMavenRepository'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.MockUserManagement'/>\n" +
" <handler id='com.yahoo.vespa.hosted.controller.restapi.deployment.DeploymentApiHandler'>\n" +
" <binding>http://*/deployment/v1/*</binding>\n" +
" </handler>\n" +
@@ -105,6 +106,10 @@ public class ControllerContainerTest {
" <binding>http://*/flags/v1</binding>\n" +
" <binding>http://*/flags/v1/*</binding>\n" +
" </handler>\n" +
+ " <handler id='com.yahoo.vespa.hosted.controller.restapi.user.UserApiHandler'>\n" +
+ " <binding>http://*/user/v1/*</binding>\n" +
+ " <binding>http://*/api/user/v1/*</binding>\n" +
+ " </handler>\n" +
variablePartXml() +
"</container>";
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java
index e3216a0038b..c90dcbf7e2b 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java
@@ -1,8 +1,9 @@
package com.yahoo.vespa.hosted.controller.restapi.athenz;
+import com.yahoo.application.container.handler.Request;
+import com.yahoo.config.provision.ApplicationId;
import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactoryMock;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzDbMock;
+import com.yahoo.vespa.hosted.controller.ControllerTester;
import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
import org.junit.Test;
@@ -19,8 +20,20 @@ public class AthenzApiTest extends ControllerContainerTest {
@Test
public void testAthenzApi() {
ContainerTester tester = new ContainerTester(container, responseFiles);
- ((AthenzClientFactoryMock) tester.container().components().getComponent(AthenzClientFactoryMock.class.getName()))
- .getSetup().addDomain(new AthenzDbMock.Domain(new AthenzDomain("domain1")));
+ ControllerTester controllerTester = new ControllerTester(tester);
+
+ controllerTester.createTenant("sandbox", AthenzApiHandler.sandboxDomainIn(tester.controller().system()), 123L);
+ controllerTester.createApplication("sandbox", "app", "default");
+ tester.controller().applications().createInstance(ApplicationId.from("sandbox", "app", hostedOperator.getName()));
+ tester.controller().applications().createInstance(ApplicationId.from("sandbox", "app", defaultUser.getName()));
+ controllerTester.createApplication("sandbox", "opp", "default");
+
+ controllerTester.createTenant("tenant1", "domain1", 123L);
+ controllerTester.createApplication("tenant1", "app", "default");
+ tester.athenzClientFactory().getSetup().getOrCreateDomain(new AthenzDomain("domain1")).admin(defaultUser);
+
+ controllerTester.createTenant("tenant2", "domain2", 123L);
+ controllerTester.createApplication("tenant2", "app", "default");
// GET root
tester.assertResponse(authenticatedRequest("http://localhost:8080/athenz/v1/"),
@@ -33,6 +46,10 @@ public class AthenzApiTest extends ControllerContainerTest {
// GET properties
tester.assertResponse(authenticatedRequest("http://localhost:8080/athenz/v1/properties/"),
new File("property-list.json"));
+
+ // POST user signup
+ tester.assertResponse(authenticatedRequest("http://localhost:8080/athenz/v1/user", "", Request.Method.POST),
+ "{\"message\":\"User 'bob' added to admin role of 'vespa.vespa.tenants.sandbox'\"}");
}
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/responses/athensDomain-list.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/responses/athensDomain-list.json
index 3a1cc9c6582..913b8fab62f 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/responses/athensDomain-list.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/responses/athensDomain-list.json
@@ -1,5 +1,7 @@
{
"data": [
- "domain1"
+ "domain1",
+ "domain2",
+ "vespa.vespa.tenants.sandbox"
]
} \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilterTest.java
index 0aba88ccc77..670413d2dd0 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilterTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilterTest.java
@@ -58,14 +58,14 @@ public class AthenzRoleFilterTest {
tester.athenzDb().hostedOperators.add(HOSTED_OPERATOR.getIdentity());
tester.createTenant(TENANT.value(), TENANT_DOMAIN.getName(), null);
- tester.createApplication(TENANT, APPLICATION.value(), "default", 12345);
+ tester.createApplication(TENANT.value(), APPLICATION.value(), "default");
AthenzDbMock.Domain tenantDomain = tester.athenzDb().domains.get(TENANT_DOMAIN);
tenantDomain.admins.add(TENANT_ADMIN.getIdentity());
tenantDomain.admins.add(TENANT_ADMIN_AND_PIPELINE.getIdentity());
tenantDomain.applications.get(new ApplicationId(APPLICATION.value())).addRoleMember(ApplicationAction.deploy, TENANT_PIPELINE.getIdentity());
tenantDomain.applications.get(new ApplicationId(APPLICATION.value())).addRoleMember(ApplicationAction.deploy, TENANT_ADMIN_AND_PIPELINE.getIdentity());
tester.createTenant(TENANT2.value(), TENANT_DOMAIN2.getName(), null);
- tester.createApplication(TENANT2, APPLICATION.value(), "default", 42);
+ tester.createApplication(TENANT2.value(), APPLICATION.value(), "default");
}
@Test
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiOnPremTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiOnPremTest.java
new file mode 100644
index 00000000000..137a0d016dd
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiOnPremTest.java
@@ -0,0 +1,66 @@
+package com.yahoo.vespa.hosted.controller.restapi.user;
+
+import com.yahoo.application.container.handler.Request;
+import com.yahoo.vespa.athenz.api.AthenzDomain;
+import com.yahoo.vespa.athenz.api.AthenzIdentity;
+import com.yahoo.vespa.athenz.utils.AthenzIdentities;
+import com.yahoo.vespa.hosted.controller.ControllerTester;
+import com.yahoo.vespa.hosted.controller.api.integration.user.User;
+import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
+import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.stream.Stream;
+
+/**
+ * @author freva
+ */
+public class UserApiOnPremTest extends ControllerContainerTest {
+
+ private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/";
+
+ @Test
+ public void userMetadataOnPremTest() {
+ ContainerTester tester = new ContainerTester(container, responseFiles);
+ ControllerTester controller = new ControllerTester(tester);
+ User user = new User("dev@domail", "Joe Developer", "dev", null);
+
+ controller.createTenant("tenant1", "domain1", 1L);
+ controller.createApplication("tenant1", "app1", "default");
+ controller.createApplication("tenant1", "app2", "default");
+ controller.createApplication("tenant1", "app2", "myinstance");
+ controller.createApplication("tenant1", "app3");
+
+ controller.createTenant("tenant2", "domain2", 2L);
+ controller.createApplication("tenant2", "app2", "test");
+
+ controller.createTenant("tenant3", "domain3", 3L);
+ controller.createApplication("tenant3", "app1");
+
+ controller.createTenant("sandbox", "domain4", 4L);
+ controller.createApplication("sandbox", "app1", "default");
+ controller.createApplication("sandbox", "app2", "default");
+ controller.createApplication("sandbox", "app2", "dev");
+
+ AthenzIdentity operator = AthenzIdentities.from("vespa.alice");
+ controller.athenzDb().addHostedOperator(operator);
+ AthenzIdentity tenantAdmin = AthenzIdentities.from("domain1.bob");
+ Stream.of("domain1", "domain2", "domain4")
+ .map(AthenzDomain::new)
+ .map(controller.athenzDb()::getOrCreateDomain)
+ .forEach(d -> d.admin(AthenzIdentities.from("domain1.bob")));
+
+ tester.assertResponse(createUserRequest(user, operator),
+ new File("user-without-applications.json"));
+
+ tester.assertResponse(createUserRequest(user, tenantAdmin),
+ new File("user-with-applications-athenz.json"));
+ }
+
+ private Request createUserRequest(User user, AthenzIdentity identity) {
+ Request request = new Request("http://localhost:8080/api/user/v1/user");
+ request.getAttributes().put(User.ATTRIBUTE_NAME, user);
+ return addIdentityToRequest(request, identity);
+ }
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
index 76faee222a7..4ed69bfbb99 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
@@ -3,9 +3,12 @@ package com.yahoo.vespa.hosted.controller.restapi.user;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.hosted.controller.ControllerTester;
+import com.yahoo.vespa.hosted.controller.api.integration.user.User;
import com.yahoo.vespa.hosted.controller.api.role.Role;
import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerCloudTest;
+import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import org.junit.Test;
import java.io.File;
@@ -31,7 +34,6 @@ public class UserApiTest extends ControllerContainerCloudTest {
"pDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n" +
"-----END PUBLIC KEY-----\n";
private static final String quotedPemPublicKey = pemPublicKey.replaceAll("\\n", "\\\\n");
- private static final String otherQuotedPemPublicKey = otherPemPublicKey.replaceAll("\\n", "\\\\n");
@Test
@@ -64,7 +66,7 @@ public class UserApiTest extends ControllerContainerCloudTest {
// POST a tenant is available to operators.
tester.assertResponse(request("/application/v4/tenant/my-tenant", POST)
.roles(operator)
- .user("administrator@tenant")
+ .principal("administrator@tenant")
.data("{\"token\":\"hello\"}"),
new File("tenant-without-applications.json"));
@@ -103,7 +105,7 @@ public class UserApiTest extends ControllerContainerCloudTest {
// POST an application is allowed for a tenant developer.
tester.assertResponse(request("/application/v4/tenant/my-tenant/application/my-app", POST)
- .user("developer@tenant")
+ .principal("developer@tenant")
.roles(Set.of(Role.developer(id.tenant()))),
new File("application-created.json"));
@@ -141,14 +143,14 @@ public class UserApiTest extends ControllerContainerCloudTest {
// POST a pem developer key
tester.assertResponse(request("/application/v4/tenant/my-tenant/key", POST)
- .user("joe@dev")
+ .principal("joe@dev")
.roles(Set.of(Role.developer(id.tenant())))
.data("{\"key\":\"" + pemPublicKey + "\"}"),
new File("first-developer-key.json"));
// POST the same pem developer key for a different user is forbidden
tester.assertResponse(request("/application/v4/tenant/my-tenant/key", POST)
- .user("operator@tenant")
+ .principal("operator@tenant")
.roles(Set.of(Role.developer(id.tenant())))
.data("{\"key\":\"" + pemPublicKey + "\"}"),
"{\"error-code\":\"BAD_REQUEST\",\"message\":\"Key "+ quotedPemPublicKey + " is already owned by joe@dev\"}",
@@ -156,7 +158,7 @@ public class UserApiTest extends ControllerContainerCloudTest {
// POST in a different pem developer key
tester.assertResponse(request("/application/v4/tenant/my-tenant/key", POST)
- .user("developer@tenant")
+ .principal("developer@tenant")
.roles(Set.of(Role.developer(id.tenant())))
.data("{\"key\":\"" + otherPemPublicKey + "\"}"),
new File("both-developer-keys.json"));
@@ -196,4 +198,47 @@ public class UserApiTest extends ControllerContainerCloudTest {
new File("tenant-without-applications.json"));
}
+ @Test
+ public void userMetadataTest() {
+ ContainerTester tester = new ContainerTester(container, responseFiles);
+ ControllerTester controller = new ControllerTester(tester);
+ Set<Role> operator = Set.of(Role.hostedOperator());
+ User user = new User("dev@domail", "Joe Developer", "dev", null);
+
+ tester.assertResponse(request("/api/user/v1/user")
+ .roles(operator)
+ .user(user),
+ new File("user-without-applications.json"));
+
+ controller.createTenant("tenant1", Tenant.Type.cloud);
+ controller.createApplication("tenant1", "app1", "default");
+ controller.createApplication("tenant1", "app2", "default");
+ controller.createApplication("tenant1", "app2", "myinstance");
+ controller.createApplication("tenant1", "app3");
+
+ controller.createTenant("tenant2", Tenant.Type.cloud);
+ controller.createApplication("tenant2", "app2", "test");
+
+ controller.createTenant("tenant3", Tenant.Type.cloud);
+ controller.createApplication("tenant3", "app1");
+
+ controller.createTenant("sandbox", Tenant.Type.cloud);
+ controller.createApplication("sandbox", "app1", "default");
+ controller.createApplication("sandbox", "app2", "default");
+ controller.createApplication("sandbox", "app2", "dev");
+
+ // Should still be empty because none of the roles explicitly refer to any of the applications
+ tester.assertResponse(request("/api/user/v1/user")
+ .roles(operator)
+ .user(user),
+ new File("user-without-applications.json"));
+
+ // Empty applications because tenant dummy does not exist
+ tester.assertResponse(request("/api/user/v1/user")
+ .roles(Set.of(Role.administrator(TenantName.from("tenant1")),
+ Role.developer(TenantName.from("tenant2")),
+ Role.developer(TenantName.from("sandbox"))))
+ .user(user),
+ new File("user-with-applications-cloud.json"));
+ }
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-athenz.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-athenz.json
new file mode 100644
index 00000000000..d4eaeaed84d
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-athenz.json
@@ -0,0 +1,57 @@
+{
+ "user": {
+ "name": "Joe Developer",
+ "email": "dev@domail",
+ "nickname": "dev"
+ },
+ "tenants": {
+ "sandbox": {
+ "roles": [
+ "administrator",
+ "developer"
+ ],
+ "applications": {
+ "app2": {
+ "instances": [
+ "dev"
+ ]
+ }
+ }
+ },
+ "tenant1": {
+ "roles": [
+ "administrator",
+ "developer"
+ ],
+ "applications": {
+ "app1": {
+ "instances": [
+ "default"
+ ]
+ },
+ "app2": {
+ "instances": [
+ "default",
+ "myinstance"
+ ]
+ },
+ "app3": {
+ "instances": []
+ }
+ }
+ },
+ "tenant2": {
+ "roles": [
+ "administrator",
+ "developer"
+ ],
+ "applications": {
+ "app2": {
+ "instances": [
+ "test"
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json
new file mode 100644
index 00000000000..b36b62d8ad5
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json
@@ -0,0 +1,60 @@
+{
+ "user": {
+ "name": "Joe Developer",
+ "email": "dev@domail",
+ "nickname": "dev"
+ },
+ "tenants": {
+ "sandbox": {
+ "roles": [
+ "developer"
+ ],
+ "applications": {
+ "app1": {
+ "instances": [
+ "default"
+ ]
+ },
+ "app2": {
+ "instances": [
+ "default",
+ "dev"
+ ]
+ }
+ }
+ },
+ "tenant1": {
+ "roles": [
+ "administrator"
+ ],
+ "applications": {
+ "app1": {
+ "instances": [
+ "default"
+ ]
+ },
+ "app2": {
+ "instances": [
+ "default",
+ "myinstance"
+ ]
+ },
+ "app3": {
+ "instances": []
+ }
+ }
+ },
+ "tenant2": {
+ "roles": [
+ "developer"
+ ],
+ "applications": {
+ "app2": {
+ "instances": [
+ "test"
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-applications.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-applications.json
new file mode 100644
index 00000000000..c7558f92080
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-applications.json
@@ -0,0 +1,11 @@
+{
+ "user": {
+ "name": "Joe Developer",
+ "email": "dev@domail",
+ "nickname": "dev"
+ },
+ "tenants": {},
+ "operator": [
+ "hostedOperator"
+ ]
+}
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 8129763a6d6..eaf83238145 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
@@ -99,6 +99,20 @@ public class DefaultZmsClient extends ClientBase implements ZmsClient {
}
@Override
+ public void addRoleMember(AthenzRole role, AthenzIdentity member) {
+ URI uri = zmsUrl.resolve(String.format("domain/%s/role/%s/member/%s", role.domain().getName(), role.roleName(), member.getFullName()));
+ HttpUriRequest request = RequestBuilder.put(uri).build();
+ execute(request, response -> readEntity(response, Void.class));
+ }
+
+ @Override
+ public void deleteRoleMember(AthenzRole role, AthenzIdentity member) {
+ URI uri = zmsUrl.resolve(String.format("domain/%s/role/%s/member/%s", role.domain().getName(), role.roleName(), member.getFullName()));
+ HttpUriRequest request = RequestBuilder.delete(uri).build();
+ execute(request, response -> readEntity(response, Void.class));
+ }
+
+ @Override
public boolean getMembership(AthenzRole role, AthenzIdentity identity) {
URI uri = zmsUrl.resolve(String.format("domain/%s/role/%s/member/%s", role.domain().getName(), role.roleName(), identity.getFullName()));
HttpUriRequest request = RequestBuilder.get()
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java
index 6a11a69a797..12762534bd4 100644
--- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java
@@ -28,6 +28,10 @@ public interface ZmsClient extends AutoCloseable {
void deleteProviderResourceGroup(AthenzDomain tenantDomain, AthenzIdentity providerService, String resourceGroup,
OktaIdentityToken identityToken, OktaAccessToken accessToken);
+ void addRoleMember(AthenzRole role, AthenzIdentity member);
+
+ void deleteRoleMember(AthenzRole role, AthenzIdentity member);
+
boolean getMembership(AthenzRole role, AthenzIdentity identity);
List<AthenzDomain> getDomainList(String prefix);