diff options
author | Valerij Fredriksen <freva@users.noreply.github.com> | 2019-12-02 15:04:19 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-12-02 15:04:19 +0100 |
commit | 610dcc8a32847c0b017e55319f2c5977004c198f (patch) | |
tree | 5497df6a702d5af3850004f9b10ea5af73bb339a | |
parent | 20d1f2f1a96b2ae326127ee44e5bd50eda0bc9f5 (diff) | |
parent | 65ac775d68a898f7bc387b2b7350fdbf30db70dc (diff) |
Merge pull request #11465 from vespa-engine/freva/create-user-v1-user
Create /user/v1/user
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); |