diff options
64 files changed, 1680 insertions, 772 deletions
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/TenantApplications.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/TenantApplications.java index 7731e13eac2..3705a0ec145 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/application/TenantApplications.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/TenantApplications.java @@ -1,7 +1,6 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.config.server.application; -import com.google.common.collect.ImmutableSet; import com.yahoo.concurrent.ThreadFactoryFactory; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.TenantName; @@ -17,12 +16,13 @@ import com.yahoo.vespa.curator.transaction.CuratorTransaction; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent; -import java.util.ArrayList; import java.util.List; -import java.util.Optional; +import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * The applications of a tenant, backed by ZooKeeper. @@ -70,40 +70,39 @@ public class TenantApplications { * @return a list of {@link ApplicationId}s that are active. */ public List<ApplicationId> listApplications() { - try { - List<String> appNodes = curator.framework().getChildren().forPath(applicationsPath.getAbsolute()); - List<ApplicationId> applicationIds = new ArrayList<>(); - for (String appNode : appNodes) { - parseApplication(appNode).ifPresent(applicationIds::add); - } - return applicationIds; - } catch (Exception e) { - throw new RuntimeException(TenantRepository.logPre(tenant)+"Unable to list applications", e); - } + return curator.getChildren(applicationsPath).stream() + .flatMap(this::parseApplication) + .collect(Collectors.toUnmodifiableList()); } - private Optional<ApplicationId> parseApplication(String appNode) { + // TODO jvenstad: Remove after it has run once everywhere. + private Stream<ApplicationId> parseApplication(String appNode) { try { - ApplicationId id = ApplicationId.fromSerializedForm(appNode); - getSessionIdForApplication(id); - return Optional.of(id); - } catch (IllegalArgumentException e) { - log.log(LogLevel.INFO, TenantRepository.logPre(tenant)+"Unable to parse application with id '" + appNode + "', ignoring."); - return Optional.empty(); + return Stream.of(ApplicationId.fromSerializedForm(appNode)); + } catch (IllegalArgumentException __) { + log.log(LogLevel.INFO, TenantRepository.logPre(tenant) + "Unable to parse application id from '" + + appNode + "'; deleting it as it shouldn't be here."); + try { + curator.delete(applicationsPath.append(appNode)); + } + catch (Exception e) { + log.log(LogLevel.WARNING, TenantRepository.logPre(tenant) + "Failed to clean up stray node '" + appNode + "'!", e); + } + return Stream.empty(); } } /** - * Register active application and adds it to the repo. If it already exists it is overwritten. + * Returns a transaction which writes the given session id as the currently active for the given application. * * @param applicationId An {@link ApplicationId} that represents an active application. * @param sessionId Id of the session containing the application package for this id. */ public Transaction createPutApplicationTransaction(ApplicationId applicationId, long sessionId) { if (listApplications().contains(applicationId)) { - return new CuratorTransaction(curator).add(CuratorOperations.setData(applicationsPath.append(applicationId.serializedForm()).getAbsolute(), Utf8.toAsciiBytes(sessionId))); + return new CuratorTransaction(curator).add(CuratorOperations.setData(applicationPath(applicationId).getAbsolute(), Utf8.toAsciiBytes(sessionId))); } else { - return new CuratorTransaction(curator).add(CuratorOperations.create(applicationsPath.append(applicationId.serializedForm()).getAbsolute(), Utf8.toAsciiBytes(sessionId))); + return new CuratorTransaction(curator).add(CuratorOperations.create(applicationPath(applicationId).getAbsolute(), Utf8.toAsciiBytes(sessionId))); } } @@ -115,7 +114,7 @@ public class TenantApplications { * @throws IllegalArgumentException if the application does not exist */ public long getSessionIdForApplication(ApplicationId applicationId) { - String path = applicationsPath.append(applicationId.serializedForm()).getAbsolute(); + String path = applicationPath(applicationId).getAbsolute(); try { return Long.parseLong(Utf8.toString(curator.framework().getData().forPath(path))); } catch (Exception e) { @@ -124,18 +123,22 @@ public class TenantApplications { } /** - * Returns a transaction which deletes this application - * - * @param applicationId an {@link ApplicationId} to delete. + * Returns a transaction which deletes this application. */ public CuratorTransaction deleteApplication(ApplicationId applicationId) { - Path path = applicationsPath.append(applicationId.serializedForm()); - return CuratorTransaction.from(CuratorOperations.delete(path.getAbsolute()), curator); + return CuratorTransaction.from(CuratorOperations.delete(applicationPath(applicationId).getAbsolute()), curator); } /** - * Closes the application repo. Once a repo has been closed, it should not be used again. - */ + * Removes all applications not known to this from the config server state. + */ + public void removeUnusedApplications() { + reloadHandler.removeApplicationsExcept(Set.copyOf(listApplications())); + } + + /** + * Closes the application repo. Once a repo has been closed, it should not be used again. + */ public void close() { directoryCache.close(); } @@ -169,13 +172,8 @@ public class TenantApplications { log.log(LogLevel.DEBUG, TenantRepository.logPre(applicationId) + "Application added: " + applicationId); } - /** - * Removes unused applications - * - */ - public void removeUnusedApplications() { - ImmutableSet<ApplicationId> activeApplications = ImmutableSet.copyOf(listApplications()); - reloadHandler.removeApplicationsExcept(activeApplications); + private Path applicationPath(ApplicationId id) { + return applicationsPath.append(id.serializedForm()); } } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployment.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployment.java index 21716730825..082be2583c2 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployment.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployment.java @@ -89,9 +89,8 @@ public class Deployment implements com.yahoo.config.provision.Deployment { timeout, clock, true, true, session.getVespaVersion(), isBootstrap); } - public Deployment setIgnoreSessionStaleFailure(boolean ignoreSessionStaleFailure) { + public void setIgnoreSessionStaleFailure(boolean ignoreSessionStaleFailure) { this.ignoreSessionStaleFailure = ignoreSessionStaleFailure; - return this; } /** Prepares this. This does nothing if this is already prepared */ diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSession.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSession.java index 0f9f8b72de1..0cdf5ebfe95 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSession.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSession.java @@ -83,12 +83,6 @@ public class LocalSession extends Session implements Comparable<LocalSession> { setStatus(Session.Status.PREPARE); } - private Transaction setActive() { - Transaction transaction = createSetStatusTransaction(Status.ACTIVATE); - transaction.add(applicationRepo.createPutApplicationTransaction(zooKeeperClient.readApplicationId(), getSessionId()).operations()); - return transaction; - } - private Transaction createSetStatusTransaction(Status status) { return zooKeeperClient.createWriteStatusTransaction(status); } @@ -99,8 +93,10 @@ public class LocalSession extends Session implements Comparable<LocalSession> { public Transaction createActivateTransaction() { zooKeeperClient.createActiveWaiter(); - superModelGenerationCounter.increment(); - return setActive(); + superModelGenerationCounter.increment(); // TODO jvenstad: I hope this counter isn't used for serious things, as it's updated way ahead of activation. + Transaction transaction = createSetStatusTransaction(Status.ACTIVATE); + transaction.add(applicationRepo.createPutApplicationTransaction(zooKeeperClient.readApplicationId(), getSessionId()).operations()); + return transaction; } public Transaction createDeactivateTransaction() { diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactory.java index a3dea83d50c..5527d3060f7 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactory.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactory.java @@ -17,8 +17,6 @@ public interface SessionFactory { /** * Creates a new deployment session from an application package. * - * - * * @param applicationDirectory a File pointing to an application. * @param applicationId application id for this new session. * @param timeoutBudget Timeout for creating session and waiting for other servers. @@ -29,10 +27,10 @@ public interface SessionFactory { /** * Creates a new deployment session from an already existing session. * - * @param existingSession The session to use as base + * @param existingSession the session to use as base * @param logger a deploy logger where the deploy log will be written. - * @param internalRedeploy if this session is for a system internal redeploy not an application package change - * @param timeoutBudget Timeout for creating session and waiting for other servers. + * @param internalRedeploy whether this session is for a system internal redeploy — not an application package change + * @param timeoutBudget timeout for creating session and waiting for other servers. * @return a new session */ LocalSession createSessionFromExisting(LocalSession existingSession, DeployLogger logger, diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactoryImpl.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactoryImpl.java index b79ea720aea..90eeb89dc8e 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactoryImpl.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactoryImpl.java @@ -194,4 +194,5 @@ public class SessionFactoryImpl implements SessionFactory, LocalSessionLoader { } return nonExistingActiveSession; } + } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/RoleId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/RoleId.java new file mode 100644 index 00000000000..199f233835f --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/RoleId.java @@ -0,0 +1,116 @@ +package com.yahoo.vespa.hosted.controller.api.integration.user; + +import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.hosted.controller.api.role.ApplicationRole; +import com.yahoo.vespa.hosted.controller.api.role.RoleDefinition; +import com.yahoo.vespa.hosted.controller.api.role.Role; +import com.yahoo.vespa.hosted.controller.api.role.Roles; +import com.yahoo.vespa.hosted.controller.api.role.TenantRole; + +import java.util.Objects; + +/** + * An identifier for a role which users identified by {@link UserId}s can be members of, corresponding to a bound {@link Role}. + * + * @author jonmv + */ +public class RoleId { + + private final String value; + + private RoleId(String value) { + if (value.isBlank()) + throw new IllegalArgumentException("Id value must be non-blank."); + this.value = value; + } + + public static RoleId fromRole(TenantRole role) { + return new RoleId(valueOf(role)); + } + + public static RoleId fromRole(ApplicationRole role) { + return new RoleId(valueOf(role)); + } + + public static RoleId fromValue(String value) { + return new RoleId(value); + } + + public String value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RoleId id = (RoleId) o; + return Objects.equals(value, id.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public String toString() { + return "role '" + value + "'"; + } + + /** Returns the {@link Role} this represent. */ + public Role toRole(Roles roles) { + String[] parts = value.split("\\."); + if (parts.length == 2) switch (parts[1]) { + case "tenantOwner": return roles.tenantOwner(TenantName.from(parts[0])); + case "tenantAdmin": return roles.tenantAdmin(TenantName.from(parts[0])); + case "tenantOperator": return roles.tenantOperator(TenantName.from(parts[0])); + } + if (parts.length == 3) switch (parts[2]) { + case "applicationOwner": return roles.applicationOwner(TenantName.from(parts[0]), ApplicationName.from(parts[1])); + case "applicationAdmin": return roles.applicationAdmin(TenantName.from(parts[0]), ApplicationName.from(parts[1])); + case "applicationOperator": return roles.applicationOperator(TenantName.from(parts[0]), ApplicationName.from(parts[1])); + case "applicationDeveloper": return roles.applicationDeveloper(TenantName.from(parts[0]), ApplicationName.from(parts[1])); + case "applicationReader": return roles.applicationReader(TenantName.from(parts[0]), ApplicationName.from(parts[1])); + } + throw new IllegalArgumentException("Malformed or illegal role value '" + value + "'."); + } + + private static String valueOf(TenantRole role) { + return valueOf(role.tenant()) + "." + valueOf(role.definition()); + } + + private static String valueOf(ApplicationRole role) { + return valueOf(role.tenant()) + "." + valueOf(role.application()) + "." + valueOf(role.definition()); + } + + private static String valueOf(TenantName tenant) { + if (tenant.value().contains(".")) + throw new IllegalArgumentException("Tenant names may not contain '.'."); + + return tenant.value(); + } + + private static String valueOf(ApplicationName application) { + if (application.value().contains(".")) + throw new IllegalArgumentException("Application names may not contain '.'."); + + return application.value(); + } + + private static String valueOf(RoleDefinition role) { + switch (role) { + case tenantOwner: return "tenantOwner"; + case tenantAdmin: return "tenantAdmin"; + case tenantOperator: return "tenantOperator"; + case applicationOwner: return "applicationOwner"; + case applicationAdmin: return "applicationAdmin"; + case applicationOperator: return "applicationOperator"; + case applicationDeveloper: return "applicationDeveloper"; + case applicationReader: return "applicationReader"; + default: throw new IllegalArgumentException("No value defined for role '" + role + "'."); + } + } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserId.java new file mode 100644 index 00000000000..3b138d0ce18 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserId.java @@ -0,0 +1,40 @@ +package com.yahoo.vespa.hosted.controller.api.integration.user; + +import java.util.Objects; + +/** + * An identifier for a user. + * + * @author jonmv + */ +public class UserId { + + private final String value; + + public UserId(String value) { + if (value.isBlank()) + throw new IllegalArgumentException("Id must be non-blank."); + this.value = value; + } + + public String value() { return value; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UserId id = (UserId) o; + return Objects.equals(value, id.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public String toString() { + return "user '" + value + "'"; + } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserManagement.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserManagement.java new file mode 100644 index 00000000000..7dd7e6a6172 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserManagement.java @@ -0,0 +1,33 @@ +package com.yahoo.vespa.hosted.controller.api.integration.user; + +import com.yahoo.vespa.hosted.controller.api.role.Role; + +import java.util.Collection; +import java.util.List; + +/** + * Management of {@link UserId}s and {@link RoleId}s, used for access control with {@link Role}s. + * + * @author jonmv + */ +public interface UserManagement { + + /** Creates the given role, or throws if the role already exists. */ + void createRole(RoleId role); + + /** Deletes the given role, or throws if it doesn't already exist.. */ + void deleteRole(RoleId role); + + /** Ensures the given users exist, and are part of the given role, or throws if the role does not exist. */ + void addUsers(RoleId role, Collection<UserId> users); + + /** Ensures none of the given users are part of the given role, or throws if the role does not exist. */ + void removeUsers(RoleId role, Collection<UserId> users); + + /** Returns all known roles. */ + List<RoleId> listRoles(); + + /** Returns all users in the given role, or throws if the role does not exist. */ + List<UserId> listUsers(RoleId role); + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/package-info.java new file mode 100644 index 00000000000..ca595bab172 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.vespa.hosted.controller.api.integration.user; + +import com.yahoo.osgi.annotation.ExportPackage;
\ No newline at end of file diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Action.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Action.java index 533c28905a9..2d9ef25d1f5 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Action.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Action.java @@ -1,5 +1,5 @@ // 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.role; +package com.yahoo.vespa.hosted.controller.api.role; import com.yahoo.jdisc.http.HttpRequest; diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/ApplicationRole.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/ApplicationRole.java new file mode 100644 index 00000000000..cc1e8462580 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/ApplicationRole.java @@ -0,0 +1,29 @@ +package com.yahoo.vespa.hosted.controller.api.role; + +import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.TenantName; + +/** + * A {@link Role} with a {@link Context} of a {@link SystemName} a {@link TenantName} and an {@link ApplicationName}. + * + * @author jonmv + */ +public class ApplicationRole extends Role { + + ApplicationRole(RoleDefinition roleDefinition, SystemName system, TenantName tenant, ApplicationName application) { + super(roleDefinition, Context.limitedTo(tenant, application, system)); + } + + /** Returns the {@link TenantName} this is bound to. */ + public TenantName tenant() { return context.tenant().get(); } + + /** Returns the {@link ApplicationName} this is bound to. */ + public ApplicationName application() { return context.application().get(); } + + @Override + public String toString() { + return "role '" + definition() + "' of '" + application() + "' owned by '" + tenant() + "'"; + } + +} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Context.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Context.java index 71452a3ef20..3ba0367a00c 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Context.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Context.java @@ -1,5 +1,5 @@ // 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.role; +package com.yahoo.vespa.hosted.controller.api.role; import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.SystemName; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/PathGroup.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java index 9dbfdbb8dac..edf3f4e8711 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/PathGroup.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java @@ -1,5 +1,5 @@ // 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.role; +package com.yahoo.vespa.hosted.controller.api.role; import com.yahoo.restapi.Path; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Policy.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java index 6ae68f598f0..970717b14a3 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Policy.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java @@ -1,5 +1,5 @@ // 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.role; +package com.yahoo.vespa.hosted.controller.api.role; import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.SystemName; @@ -39,9 +39,14 @@ public enum Policy { .in(SystemName.main, SystemName.cd, SystemName.dev)), // TODO SystemName.all() /** Full access to tenant information and settings. */ - tenantWrite(Privilege.grant(Action.write()) - .on(PathGroup.tenant) - .in(SystemName.all())), + tenantDelete(Privilege.grant(Action.delete) + .on(PathGroup.tenant) + .in(SystemName.all())), + + /** Full access to tenant information and settings. */ + tenantUpdate(Privilege.grant(Action.update) + .on(PathGroup.tenant) + .in(SystemName.all())), /** Read access to tenant information and settings. */ tenantRead(Privilege.grant(Action.read) diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Privilege.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Privilege.java index 4c5ad136f56..a53717b25d6 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Privilege.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Privilege.java @@ -1,5 +1,5 @@ // 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.role; +package com.yahoo.vespa.hosted.controller.api.role; import com.yahoo.config.provision.SystemName; diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Role.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Role.java new file mode 100644 index 00000000000..86d59b4bbb6 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Role.java @@ -0,0 +1,48 @@ +// 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.api.role; + +import java.net.URI; +import java.util.Objects; + +import static java.util.Objects.requireNonNull; + +/** + * A role is a combination of a {@link RoleDefinition} and a {@link Context}, which allows evaluation + * of access control for a given action on a resource. Create using {@link Roles}. + * + * @author jonmv + */ +public abstract class Role { + + private final RoleDefinition roleDefinition; + final Context context; + + Role(RoleDefinition roleDefinition, Context context) { + this.roleDefinition = requireNonNull(roleDefinition); + this.context = requireNonNull(context); + } + + /** Returns the role definition of this bound role. */ + public RoleDefinition definition() { return roleDefinition; } + + /** Returns whether this role is allowed to perform the given action on the given resource. */ + public boolean allows(Action action, URI uri) { + return roleDefinition.policies().stream().anyMatch(policy -> policy.evaluate(action, uri, context)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Role role = (Role) o; + return roleDefinition == role.roleDefinition && + Objects.equals(context, role.context); + } + + @Override + public int hashCode() { + return Objects.hash(roleDefinition, context); + } + +} + diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Role.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java index d82e4063391..e9c2f7bc643 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Role.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java @@ -1,21 +1,20 @@ -// 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.role; +package com.yahoo.vespa.hosted.controller.api.role; -import java.net.URI; import java.util.EnumSet; import java.util.Set; /** * This declares all tenant roles known to the controller. A role contains one or more {@link Policy}s which decide - * what actions a member of a role can perform. + * what actions a member of a role can perform, given a {@link Context} for the action. * - * Optionally, some role definition also inherit all policies from a "lower ranking" role. Read the list of roles - * from {@code everyone} to {@code tenantAdmin}, in order, to see what policies these roles. + * Optionally, some role definitions also inherit all policies from a "lower ranking" role. + * + * See {@link Role} for roles bound to a context, where policies can be evaluated. * * @author mpolden * @author jonmv */ -public enum Role { +public enum RoleDefinition { /** Deus ex machina. */ hostedOperator(Policy.operator), @@ -50,45 +49,52 @@ public enum Role { Policy.productionDeployment, Policy.submission), - /** Tenant admin with full access to all tenant resources, including the ability to create new applications. */ - tenantAdmin(applicationAdmin, - Policy.applicationCreate, + /** Application administrator with the additional ability to delete an application. */ + applicationOwner(applicationOperator, + Policy.applicationDelete), + + /** Tenant operator with admin access to all applications under the tenant, as well as the ability to create applications. */ + tenantOperator(applicationAdmin, + Policy.applicationCreate), + + /** Tenant admin with full access to all tenant resources, except deleting the tenant. */ + tenantAdmin(tenantOperator, Policy.applicationDelete, Policy.manager, - Policy.tenantWrite), + Policy.tenantUpdate), + + /** Tenant admin with full access to all tenant resources. */ + tenantOwner(tenantAdmin, + Policy.tenantDelete), /** Build and continuous delivery service. */ // TODO replace with buildService, when everyone is on new pipeline. - tenantPipeline(Policy.submission, + tenantPipeline(everyone, + Policy.submission, Policy.deploymentPipeline, Policy.productionDeployment), /** Tenant administrator with full access to all child resources. */ - athenzTenantAdmin(Policy.tenantWrite, + athenzTenantAdmin(everyone, Policy.tenantRead, + Policy.tenantUpdate, + Policy.tenantDelete, Policy.applicationCreate, Policy.applicationUpdate, Policy.applicationDelete, Policy.applicationOperations, - Policy.developmentDeployment); // TODO remove, as it is covered by applicationAdmin. + Policy.developmentDeployment); private final Set<Policy> policies; - Role(Policy... policies) { + RoleDefinition(Policy... policies) { this.policies = EnumSet.copyOf(Set.of(policies)); } - Role(Role inherited, Policy... policies) { + RoleDefinition(RoleDefinition inherited, Policy... policies) { this.policies = EnumSet.copyOf(Set.of(policies)); this.policies.addAll(inherited.policies); } - /** - * Returns whether this role is allowed to perform action in given role context. Action is allowed if at least one - * policy evaluates to true. - */ - public boolean allows(Action action, URI uri, Context context) { - return policies.stream().anyMatch(policy -> policy.evaluate(action, uri, context)); - } + Set<Policy> policies() { return policies; } } - diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Roles.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Roles.java new file mode 100644 index 00000000000..a6a4fdaf16c --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Roles.java @@ -0,0 +1,96 @@ +package com.yahoo.vespa.hosted.controller.api.role; + +import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.TenantName; + +import java.util.Objects; + +/** + * Use if you need to create {@link Role}s for its system. + * + * This also defines the relationship between {@link RoleDefinition}s and their required {@link Context}s. + * + * @author jonmv + */ +public class Roles { + + private final SystemName system; + + /** Creates a Roles which can be used to create bound roles for the given system. */ + public Roles(SystemName system) { + this.system = Objects.requireNonNull(system); + } + + + // General roles. + /** Returns a {@link RoleDefinition#hostedOperator} for the current system. */ + public UnboundRole hostedOperator() { + return new UnboundRole(RoleDefinition.hostedOperator, system); + } + + /** Returns a {@link RoleDefinition#everyone} for the current system. */ + public UnboundRole everyone() { + return new UnboundRole(RoleDefinition.everyone, system); + } + + + // Athenz based roles. + /** Returns a {@link RoleDefinition#athenzTenantAdmin} for the current system and given tenant. */ + public TenantRole athenzTenantAdmin(TenantName tenant) { + return new TenantRole(RoleDefinition.athenzTenantAdmin, system, tenant); + } + + /** Returns a {@link RoleDefinition#tenantPipeline} for the current system and given tenant and application. */ + public ApplicationRole tenantPipeline(TenantName tenant, ApplicationName application) { + return new ApplicationRole(RoleDefinition.tenantPipeline, system, tenant, application); + } + + + // Other identity provider based roles. + /** Returns a {@link RoleDefinition#tenantOwner} for the current system and given tenant. */ + public TenantRole tenantOwner(TenantName tenant) { + return new TenantRole(RoleDefinition.tenantOwner, system, tenant); + } + + /** Returns a {@link RoleDefinition#tenantAdmin} for the current system and given tenant. */ + public TenantRole tenantAdmin(TenantName tenant) { + return new TenantRole(RoleDefinition.tenantAdmin, system, tenant); + } + + /** Returns a {@link RoleDefinition#tenantOperator} for the current system and given tenant. */ + public TenantRole tenantOperator(TenantName tenant) { + return new TenantRole(RoleDefinition.tenantOperator, system, tenant); + } + + /** Returns a {@link RoleDefinition#applicationOwner} for the current system and given tenant and application. */ + public ApplicationRole applicationOwner(TenantName tenant, ApplicationName application) { + return new ApplicationRole(RoleDefinition.applicationOwner, system, tenant, application); + } + + /** Returns a {@link RoleDefinition#applicationAdmin} for the current system and given tenant and application. */ + public ApplicationRole applicationAdmin(TenantName tenant, ApplicationName application) { + return new ApplicationRole(RoleDefinition.applicationAdmin, system, tenant, application); + } + + /** Returns a {@link RoleDefinition#applicationOperator} for the current system and given tenant and application. */ + public ApplicationRole applicationOperator(TenantName tenant, ApplicationName application) { + return new ApplicationRole(RoleDefinition.applicationOperator, system, tenant, application); + } + + /** Returns a {@link RoleDefinition#applicationDeveloper} for the current system and given tenant and application. */ + public ApplicationRole applicationDeveloper(TenantName tenant, ApplicationName application) { + return new ApplicationRole(RoleDefinition.applicationDeveloper, system, tenant, application); + } + + /** Returns a {@link RoleDefinition#applicationReader} for the current system and given tenant and application. */ + public ApplicationRole applicationReader(TenantName tenant, ApplicationName application) { + return new ApplicationRole(RoleDefinition.applicationReader, system, tenant, application); + } + + /** Returns a {@link RoleDefinition#buildService} for the current system and given tenant and application. */ + public ApplicationRole buildService(TenantName tenant, ApplicationName application) { + return new ApplicationRole(RoleDefinition.buildService, system, tenant, application); + } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/SecurityContext.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/SecurityContext.java new file mode 100644 index 00000000000..41444258a68 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/SecurityContext.java @@ -0,0 +1,51 @@ +// 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.api.role; + +import java.security.Principal; +import java.util.Objects; +import java.util.Set; + +import static java.util.Objects.requireNonNull; + +public class SecurityContext { + + public static final String ATTRIBUTE_NAME = SecurityContext.class.getName(); + + private final Principal principal; + private final Set<Role> roles; + + public SecurityContext(Principal principal, Set<Role> roles) { + this.principal = requireNonNull(principal); + this.roles = Set.copyOf(roles); + } + + public Principal principal() { + return principal; + } + + public Set<Role> roles() { + return roles; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SecurityContext that = (SecurityContext) o; + return Objects.equals(principal, that.principal) && + Objects.equals(roles, that.roles); + } + + @Override + public int hashCode() { + return Objects.hash(principal, roles); + } + + @Override + public String toString() { + return "SecurityContext{" + + "principal=" + principal + + ", roles=" + roles + + '}'; + } +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/TenantRole.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/TenantRole.java new file mode 100644 index 00000000000..134628ec3a3 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/TenantRole.java @@ -0,0 +1,25 @@ +package com.yahoo.vespa.hosted.controller.api.role; + +import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.TenantName; + +/** + * A {@link Role} with a {@link Context} of a {@link SystemName} and a {@link TenantName}. + * + * @author jonmv + */ +public class TenantRole extends Role { + + TenantRole(RoleDefinition roleDefinition, SystemName system, TenantName tenant) { + super(roleDefinition, Context.limitedTo(tenant, system)); + } + + /** Returns the {@link TenantName} this is bound to. */ + public TenantName tenant() { return context.tenant().get(); } + + @Override + public String toString() { + return "role '" + definition() + "' of '" + tenant() + "'"; + } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/UnboundRole.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/UnboundRole.java new file mode 100644 index 00000000000..eb8319b2012 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/UnboundRole.java @@ -0,0 +1,21 @@ +package com.yahoo.vespa.hosted.controller.api.role; + +import com.yahoo.config.provision.SystemName; + +/** + * A {@link Role} with a {@link Context} of only a {@link SystemName}. + * + * @author jonmv + */ +public class UnboundRole extends Role { + + UnboundRole(RoleDefinition roleDefinition, SystemName system) { + super(roleDefinition, Context.unlimitedIn(system)); + } + + @Override + public String toString() { + return "role '" + definition() + "'"; + } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/package-info.java new file mode 100644 index 00000000000..a7f70d6fe3c --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.vespa.hosted.controller.api.role; + +import com.yahoo.osgi.annotation.ExportPackage;
\ No newline at end of file diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/user/RoleIdTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/user/RoleIdTest.java new file mode 100644 index 00000000000..609646eb672 --- /dev/null +++ b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/user/RoleIdTest.java @@ -0,0 +1,74 @@ +package com.yahoo.vespa.hosted.controller.api.integration.user; + +import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.hosted.controller.api.role.ApplicationRole; +import com.yahoo.vespa.hosted.controller.api.role.Roles; +import com.yahoo.vespa.hosted.controller.api.role.TenantRole; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author jonmv + */ +public class RoleIdTest { + + @Test + public void testSerialization() { + Roles roles = new Roles(SystemName.main); + + TenantName tenant = TenantName.from("my-tenant"); + for (TenantRole role : List.of(roles.tenantOwner(tenant), + roles.tenantAdmin(tenant), + roles.tenantOperator(tenant))) + assertEquals(role, RoleId.fromRole(role).toRole(roles)); + + ApplicationName application = ApplicationName.from("my-application"); + for (ApplicationRole role : List.of(roles.applicationOwner(tenant, application), + roles.applicationAdmin(tenant, application), + roles.applicationOperator(tenant, application), + roles.applicationDeveloper(tenant, application), + roles.applicationReader(tenant, application))) + assertEquals(role, RoleId.fromRole(role).toRole(roles)); + + assertEquals(roles.tenantOperator(tenant), + RoleId.fromValue("my-tenant.tenantOperator").toRole(roles)); + assertEquals(roles.applicationReader(tenant, application), + RoleId.fromValue("my-tenant.my-application.applicationReader").toRole(roles)); + } + + @Test(expected = IllegalArgumentException.class) + public void illegalTenantName() { + RoleId.fromRole(new Roles(SystemName.main).tenantAdmin(TenantName.from("my.tenant"))); + } + + @Test(expected = IllegalArgumentException.class) + public void illegalApplicationName() { + RoleId.fromRole(new Roles(SystemName.main).applicationOperator(TenantName.from("my-tenant"), ApplicationName.from("my.app"))); + } + + @Test(expected = IllegalArgumentException.class) + public void illegalRole() { + RoleId.fromRole(new Roles(SystemName.main).tenantPipeline(TenantName.from("my-tenant"), ApplicationName.from("my-app"))); + } + + @Test(expected = IllegalArgumentException.class) + public void illegalRoleValue() { + RoleId.fromValue("my-tenant.awesomePerson").toRole(new Roles(SystemName.cd)); + } + + @Test(expected = IllegalArgumentException.class) + public void illegalCombination() { + RoleId.fromValue("my-tenant.my-application.tenantOwner").toRole(new Roles(SystemName.cd)); + } + + @Test(expected = IllegalArgumentException.class) + public void illegalValue() { + RoleId.fromValue("hostedOperator").toRole(new Roles(SystemName.Public)); + } + +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/role/PathGroupTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/role/PathGroupTest.java index b4a3e674594..9d76d055877 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/role/PathGroupTest.java +++ b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/role/PathGroupTest.java @@ -1,9 +1,8 @@ -package com.yahoo.vespa.hosted.controller.role; +package com.yahoo.vespa.hosted.controller.api.role; import org.junit.Test; import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.Set; import java.util.regex.Pattern; diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/role/RoleTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/role/RoleTest.java new file mode 100644 index 00000000000..1badd157b1b --- /dev/null +++ b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/role/RoleTest.java @@ -0,0 +1,54 @@ +// 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.api.role; + +import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.TenantName; +import org.junit.Test; + +import java.net.URI; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author mpolden + */ +public class RoleTest { + + @Test + public void operator_membership() { + Role role = new Roles(SystemName.main).hostedOperator(); + + // Operator actions + assertFalse(role.allows(Action.create, URI.create("/not/explicitly/defined"))); + assertTrue(role.allows(Action.create, URI.create("/controller/v1/foo"))); + assertTrue(role.allows(Action.update, URI.create("/os/v1/bar"))); + assertTrue(role.allows(Action.update, URI.create("/application/v4/tenant/t1/application/a1"))); + assertTrue(role.allows(Action.update, URI.create("/application/v4/tenant/t2/application/a2"))); + } + + @Test + public void tenant_membership() { + Role role = new Roles(SystemName.main).athenzTenantAdmin(TenantName.from("t1")); + assertFalse(role.allows(Action.create, URI.create("/not/explicitly/defined"))); + assertFalse("Deny access to operator API", role.allows(Action.create, URI.create("/controller/v1/foo"))); + assertFalse("Deny access to other tenant and app", role.allows(Action.update, URI.create("/application/v4/tenant/t2/application/a2"))); + assertTrue(role.allows(Action.update, URI.create("/application/v4/tenant/t1/application/a1"))); + + Role publicSystem = new Roles(SystemName.vaas).athenzTenantAdmin(TenantName.from("t1")); + assertFalse(publicSystem.allows(Action.read, URI.create("/controller/v1/foo"))); + assertTrue(publicSystem.allows(Action.read, URI.create("/badge/v1/badge"))); + assertTrue(publicSystem.allows(Action.update, URI.create("/application/v4/tenant/t1/application/a1"))); + } + + @Test + public void build_service_membership() { + Role role = new Roles(SystemName.vaas).tenantPipeline(TenantName.from("t1"), ApplicationName.from("a1")); + assertFalse(role.allows(Action.create, URI.create("/not/explicitly/defined"))); + assertFalse(role.allows(Action.update, URI.create("/application/v4/tenant/t1/application/a1"))); + assertTrue(role.allows(Action.create, URI.create("/application/v4/tenant/t1/application/a1/jobreport"))); + assertFalse("No global read access", role.allows(Action.read, URI.create("/controller/v1/foo"))); + } + +} diff --git a/controller-server/pom.xml b/controller-server/pom.xml index c4cb66de3ec..f22142db727 100644 --- a/controller-server/pom.xml +++ b/controller-server/pom.xml @@ -100,6 +100,13 @@ <scope>provided</scope> </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>flags</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <!-- compile --> <dependency> diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java index 1d685895914..b6993fbc421 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java @@ -13,6 +13,9 @@ 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.curator.Lock; +import com.yahoo.vespa.flags.BooleanFlag; +import com.yahoo.vespa.flags.FetchVector; +import com.yahoo.vespa.flags.Flags; import com.yahoo.vespa.hosted.controller.api.ActivateResult; import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions; import com.yahoo.vespa.hosted.controller.api.application.v4.model.EndpointStatus; @@ -42,6 +45,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics; +import com.yahoo.vespa.hosted.controller.application.GlobalDnsName; import com.yahoo.vespa.hosted.controller.application.JobList; import com.yahoo.vespa.hosted.controller.application.JobStatus; import com.yahoo.vespa.hosted.controller.application.JobStatus.JobRun; @@ -112,6 +116,7 @@ public class ApplicationController { private final ConfigServer configServer; private final RoutingGenerator routingGenerator; private final Clock clock; + private final BooleanFlag redirectLegacyDnsFlag; private final DeploymentTrigger deploymentTrigger; @@ -127,6 +132,7 @@ public class ApplicationController { this.configServer = configServer; this.routingGenerator = routingGenerator; this.clock = clock; + this.redirectLegacyDnsFlag = Flags.REDIRECT_LEGACY_DNS_NAMES.bindTo(controller.flagSource()); this.artifactRepository = artifactRepository; this.applicationStore = applicationStore; @@ -231,14 +237,14 @@ public class ApplicationController { com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId.validate(id.application().value()); Optional<Tenant> tenant = controller.tenants().get(id.tenant()); - if ( ! tenant.isPresent()) + if (tenant.isEmpty()) throw new IllegalArgumentException("Could not create '" + id + "': This tenant does not exist"); if (get(id).isPresent()) throw new IllegalArgumentException("Could not create '" + id + "': Application already exists"); if (get(dashToUnderscore(id)).isPresent()) // VESPA-1945 throw new IllegalArgumentException("Could not create '" + id + "': Application " + dashToUnderscore(id) + " already exists"); if (tenant.get().type() != Tenant.Type.user) { - if ( ! credentials.isPresent()) + if (credentials.isEmpty()) throw new IllegalArgumentException("Could not create '" + id + "': No credentials provided"); if (id.instance().isDefault()) // Only store the application permits for non-user applications. @@ -269,7 +275,7 @@ public class ApplicationController { throw new IllegalArgumentException("'" + applicationId + "' is a tester application!"); Tenant tenant = controller.tenants().require(applicationId.tenant()); - if (tenant.type() == Tenant.Type.user && ! get(applicationId).isPresent()) + if (tenant.type() == Tenant.Type.user && get(applicationId).isEmpty()) createApplication(applicationId, Optional.empty()); try (Lock deploymentLock = lockForDeployment(applicationId, zone)) { @@ -292,15 +298,15 @@ public class ApplicationController { () -> new IllegalArgumentException("Application package must be given when deploying to " + zone)); platformVersion = options.vespaVersion.map(Version::new).orElse(applicationPackage.deploymentSpec().majorVersion() .flatMap(this::lastCompatibleVersion) - .orElse(controller.systemVersion())); + .orElseGet(controller::systemVersion)); } else { JobType jobType = JobType.from(controller.system(), zone) .orElseThrow(() -> new IllegalArgumentException("No job is known for " + zone + ".")); Optional<JobStatus> job = Optional.ofNullable(application.get().deploymentJobs().jobStatus().get(jobType)); - if ( ! job.isPresent() - || ! job.get().lastTriggered().isPresent() - || job.get().lastCompleted().isPresent() && job.get().lastCompleted().get().at().isAfter(job.get().lastTriggered().get().at())) + if ( job.isEmpty() + || job.get().lastTriggered().isEmpty() + || job.get().lastCompleted().isPresent() && job.get().lastCompleted().get().at().isAfter(job.get().lastTriggered().get().at())) return unexpectedDeployment(applicationId, zone); JobRun triggered = job.get().lastTriggered().get(); platformVersion = preferOldestVersion ? triggered.sourcePlatform().orElse(triggered.platform()) @@ -382,7 +388,7 @@ public class ApplicationController { application = withoutUnreferencedDeploymentJobs(application); store(application); - return(application); + return application; } /** Deploy a system application to given zone */ @@ -432,20 +438,28 @@ public class ApplicationController { application = application.with(rotation.id()); store(application); // store assigned rotation even if deployment fails - registerRotationInDns(rotation, application.get().globalDnsName(controller.system()).get().dnsName()); - registerRotationInDns(rotation, application.get().globalDnsName(controller.system()).get().secureDnsName()); - registerRotationInDns(rotation, application.get().globalDnsName(controller.system()).get().oathDnsName()); + GlobalDnsName dnsName = application.get().globalDnsName(controller.system()) + .orElseThrow(() -> new IllegalStateException("Expected rotation to be assigned")); + boolean redirectLegacyDns = redirectLegacyDnsFlag.with(FetchVector.Dimension.APPLICATION_ID, application.get().id().serializedForm()) + .value(); + registerCname(dnsName.oathDnsName(), rotation.name()); + if (redirectLegacyDns) { + registerCname(dnsName.dnsName(), dnsName.oathDnsName()); + registerCname(dnsName.secureDnsName(), dnsName.oathDnsName()); + } else { + registerCname(dnsName.dnsName(), rotation.name()); + registerCname(dnsName.secureDnsName(), rotation.name()); + } } } return application; } - private ActivateResult unexpectedDeployment(ApplicationId applicationId, ZoneId zone) { - + private ActivateResult unexpectedDeployment(ApplicationId application, ZoneId zone) { Log logEntry = new Log(); logEntry.level = "WARNING"; logEntry.time = clock.instant().toEpochMilli(); - logEntry.message = "Ignoring deployment of " + require(applicationId) + " to " + zone + + logEntry.message = "Ignoring deployment of application '" + application + "' to " + zone + " as a deployment is not currently expected"; PrepareResponse prepareResponse = new PrepareResponse(); prepareResponse.log = Collections.singletonList(logEntry); @@ -495,24 +509,22 @@ public class ApplicationController { options.deployCurrentVersion); } - /** Register a DNS name for rotation */ - private void registerRotationInDns(Rotation rotation, String dnsName) { + /** Register a CNAME record in DNS */ + private void registerCname(String name, String targetName) { try { - - RecordData rotationName = RecordData.fqdn(rotation.name()); - List<Record> records = nameService.findRecords(Record.Type.CNAME, RecordName.from(dnsName)); + RecordData data = RecordData.fqdn(targetName); + List<Record> records = nameService.findRecords(Record.Type.CNAME, RecordName.from(name)); records.forEach(record -> { - // Ensure that the existing record points to the correct rotation - if ( ! record.data().equals(rotationName)) { - nameService.updateRecord(record, rotationName); - log.info("Updated mapping for record '" + record + "': '" + dnsName - + "' -> '" + rotation.name() + "'"); + // Ensure that the existing record points to the correct target + if ( ! record.data().equals(data)) { + log.info("Updating mapping for record '" + record + "': '" + name + + "' -> '" + data.asString() + "'"); + nameService.updateRecord(record, data); } }); - if (records.isEmpty()) { - Record record = nameService.createCname(RecordName.from(dnsName), rotationName); - log.info("Registered mapping as record '" + record + "'"); + Record record = nameService.createCname(RecordName.from(name), data); + log.info("Registered mapping as record '" + record + "'"); } } catch (RuntimeException e) { log.log(Level.WARNING, "Failed to register CNAME", e); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java index 6e59c384485..7754286ba9e 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java @@ -9,8 +9,7 @@ import com.yahoo.config.provision.CloudName; import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.SystemName; import com.yahoo.vespa.curator.Lock; -import com.yahoo.vespa.hosted.controller.api.identifiers.Property; -import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; +import com.yahoo.vespa.flags.FlagSource; import com.yahoo.vespa.hosted.controller.api.integration.BuildService; import com.yahoo.vespa.hosted.controller.api.integration.MetricsService; import com.yahoo.vespa.hosted.controller.api.integration.RunDataStore; @@ -76,6 +75,7 @@ public class Controller extends AbstractComponent { private final Chef chef; private final Mailer mailer; private final AuditLogger auditLogger; + private final FlagSource flagSource; /** * Creates a controller @@ -88,11 +88,11 @@ public class Controller extends AbstractComponent { NameService nameService, RoutingGenerator routingGenerator, Chef chef, AccessControl accessControl, ArtifactRepository artifactRepository, ApplicationStore applicationStore, TesterCloud testerCloud, - BuildService buildService, RunDataStore runDataStore, Mailer mailer) { + BuildService buildService, RunDataStore runDataStore, Mailer mailer, FlagSource flagSource) { this(curator, rotationsConfig, gitHub, zoneRegistry, configServer, metricsService, nameService, routingGenerator, chef, Clock.systemUTC(), accessControl, artifactRepository, applicationStore, testerCloud, - buildService, runDataStore, com.yahoo.net.HostName::getLocalhost, mailer); + buildService, runDataStore, com.yahoo.net.HostName::getLocalhost, mailer, flagSource); } public Controller(CuratorDb curator, RotationsConfig rotationsConfig, GitHub gitHub, @@ -102,7 +102,7 @@ public class Controller extends AbstractComponent { AccessControl accessControl, ArtifactRepository artifactRepository, ApplicationStore applicationStore, TesterCloud testerCloud, BuildService buildService, RunDataStore runDataStore, Supplier<String> hostnameSupplier, - Mailer mailer) { + Mailer mailer, FlagSource flagSource) { this.hostnameSupplier = Objects.requireNonNull(hostnameSupplier, "HostnameSupplier cannot be null"); this.curator = Objects.requireNonNull(curator, "Curator cannot be null"); @@ -113,6 +113,7 @@ public class Controller extends AbstractComponent { this.chef = Objects.requireNonNull(chef, "Chef cannot be null"); this.clock = Objects.requireNonNull(clock, "Clock cannot be null"); this.mailer = Objects.requireNonNull(mailer, "Mailer cannot be null"); + this.flagSource = Objects.requireNonNull(flagSource, "FlagSource cannot be null"); jobController = new JobController(this, runDataStore, Objects.requireNonNull(testerCloud)); applicationController = new ApplicationController(this, curator, accessControl, @@ -123,7 +124,8 @@ public class Controller extends AbstractComponent { Objects.requireNonNull(applicationStore, "ApplicationStore cannot be null"), Objects.requireNonNull(routingGenerator, "RoutingGenerator cannot be null"), Objects.requireNonNull(buildService, "BuildService cannot be null"), - clock); + clock + ); tenantController = new TenantController(this, curator, accessControl); auditLogger = new AuditLogger(curator, clock); @@ -146,6 +148,11 @@ public class Controller extends AbstractComponent { return mailer; } + /** Provides access to the feature flags of this */ + public FlagSource flagSource() { + return flagSource; + } + public Clock clock() { return clock; } public ZoneRegistry zoneRegistry() { return zoneRegistry; } 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 e8b3e334631..19148a6c9bd 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 @@ -147,10 +147,11 @@ public class TenantController { } private void requireNonExistent(TenantName name) { - if (get(name).isPresent() || + if ( "hosted-vespa".equals(name.value()) + || get(name).isPresent() // Underscores are allowed in existing tenant names, but tenants with - and _ cannot co-exist. E.g. // my-tenant cannot be created if my_tenant exists. - get(name.value().replace('-', '_')).isPresent()) { + || get(name.value().replace('-', '_')).isPresent()) { throw new IllegalArgumentException("Tenant '" + name + "' already exists"); } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/GlobalDnsName.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/GlobalDnsName.java index 0254bf2fd38..ae638beed5c 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/GlobalDnsName.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/GlobalDnsName.java @@ -9,7 +9,7 @@ import java.net.URI; import java.util.Objects; /** - * Represents an application's global rotation. + * Represents names for an application's global rotation. * * @author mpolden */ diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainer.java index 2fe6af02480..7693f224b56 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainer.java @@ -72,7 +72,7 @@ public class DnsMaintainer extends Maintainer { private Optional<Rotation> rotationToCheckOf(Collection<Rotation> rotations) { if (rotations.isEmpty()) return Optional.empty(); List<Rotation> rotationList = new ArrayList<>(rotations); - int index = rotationIndex.getAndUpdate((i)-> { + int index = rotationIndex.getAndUpdate((i) -> { if (i < rotationList.size() - 1) { return ++i; } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java new file mode 100644 index 00000000000..f25deb11a52 --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java @@ -0,0 +1,115 @@ +package com.yahoo.vespa.hosted.controller.restapi.filter; + +import com.google.inject.Inject; +import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.TenantName; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.http.filter.DiscFilterRequest; +import com.yahoo.jdisc.http.filter.security.cors.CorsFilterConfig; +import com.yahoo.jdisc.http.filter.security.cors.CorsRequestFilterBase; +import com.yahoo.log.LogLevel; +import com.yahoo.restapi.Path; +import com.yahoo.vespa.athenz.api.AthenzDomain; +import com.yahoo.vespa.athenz.api.AthenzIdentity; +import com.yahoo.vespa.athenz.api.AthenzPrincipal; +import com.yahoo.vespa.athenz.client.zms.ZmsClientException; +import com.yahoo.vespa.hosted.controller.Controller; +import com.yahoo.vespa.hosted.controller.TenantController; +import com.yahoo.vespa.hosted.controller.api.role.Role; +import com.yahoo.vespa.hosted.controller.api.role.Roles; +import com.yahoo.vespa.hosted.controller.athenz.ApplicationAction; +import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade; +import com.yahoo.vespa.hosted.controller.api.role.SecurityContext; +import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; +import com.yahoo.vespa.hosted.controller.tenant.Tenant; +import com.yahoo.vespa.hosted.controller.tenant.UserTenant; +import com.yahoo.yolean.Exceptions; + +import java.net.URI; +import java.util.Optional; +import java.util.Set; +import java.util.logging.Logger; + +import static com.yahoo.vespa.hosted.controller.athenz.HostedAthenzIdentities.SCREWDRIVER_DOMAIN; + +/** + * Enriches the request principal with roles from Athenz. + * + * @author jonmv + */ +public class AthenzRoleFilter extends CorsRequestFilterBase { // TODO: No need for this super anyway. + + private static final Logger logger = Logger.getLogger(AthenzRoleFilter.class.getName()); + + private final AthenzFacade athenz; + private final TenantController tenants; + private final Roles roles; + + @Inject + public AthenzRoleFilter(CorsFilterConfig config, AthenzFacade athenz, Controller controller) { + super(Set.copyOf(config.allowedUrls())); + this.athenz = athenz; + this.tenants = controller.tenants(); + this.roles = new Roles(controller.system()); + } + + @Override + protected Optional<ErrorResponse> filterRequest(DiscFilterRequest request) { + try { + AthenzPrincipal athenzPrincipal = (AthenzPrincipal) request.getUserPrincipal(); + request.setAttribute(SecurityContext.ATTRIBUTE_NAME, new SecurityContext(athenzPrincipal, + roles(athenzPrincipal, request.getUri()))); + return Optional.empty(); + } + catch (Exception e) { + logger.log(LogLevel.DEBUG, () -> "Exception mapping Athenz principal to roles: " + Exceptions.toMessageString(e)); + return Optional.of(new ErrorResponse(Response.Status.UNAUTHORIZED, "Access denied")); + } + } + + Set<Role> roles(AthenzPrincipal principal, URI uri) { + Path path = new Path(uri); + + path.matches("/application/v4/tenant/{tenant}/{*}"); + Optional<Tenant> tenant = Optional.ofNullable(path.get("tenant")).map(TenantName::from).flatMap(tenants::get); + + path.matches("/application/v4/tenant/{tenant}/application/{application}/{*}"); + Optional<ApplicationName> application = Optional.ofNullable(path.get("application")).map(ApplicationName::from); + + AthenzIdentity identity = principal.getIdentity(); + + if (athenz.hasHostedOperatorAccess(identity)) + return Set.of(roles.hostedOperator()); + + if (tenant.isPresent() && isTenantAdmin(identity, tenant.get())) + return Set.of(roles.athenzTenantAdmin(tenant.get().name())); + + if (identity.getDomain().equals(SCREWDRIVER_DOMAIN) && application.isPresent() && tenant.isPresent()) + // NOTE: Only fine-grained deploy authorization for Athenz tenants + if ( tenant.get().type() != Tenant.Type.athenz + || hasDeployerAccess(identity, ((AthenzTenant) tenant.get()).domain(), application.get())) + return Set.of(roles.tenantPipeline(tenant.get().name(), application.get())); + + return Set.of(roles.everyone()); + } + + private boolean isTenantAdmin(AthenzIdentity identity, Tenant tenant) { + switch (tenant.type()) { + case athenz: return athenz.hasTenantAdminAccess(identity, ((AthenzTenant) tenant).domain()); + case user: return ((UserTenant) tenant).is(identity.getName()) || athenz.hasHostedOperatorAccess(identity); + default: throw new IllegalArgumentException("Unexpected tenant type '" + tenant.type() + "'."); + } + } + + private boolean hasDeployerAccess(AthenzIdentity identity, AthenzDomain tenantDomain, ApplicationName application) { + try { + return athenz.hasApplicationAccess(identity, + ApplicationAction.deploy, + tenantDomain, + application); + } catch (ZmsClientException e) { + throw new RuntimeException("Failed to authorize operation: (" + e.getMessage() + ")", e); + } + } + +} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleResolver.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleResolver.java deleted file mode 100644 index a1dfdbeb245..00000000000 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleResolver.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.yahoo.vespa.hosted.controller.restapi.filter; - -import com.google.inject.Inject; -import com.yahoo.config.provision.ApplicationName; -import com.yahoo.config.provision.SystemName; -import com.yahoo.config.provision.TenantName; -import com.yahoo.restapi.Path; -import com.yahoo.vespa.athenz.api.AthenzDomain; -import com.yahoo.vespa.athenz.api.AthenzIdentity; -import com.yahoo.vespa.athenz.api.AthenzPrincipal; -import com.yahoo.vespa.athenz.api.AthenzUser; -import com.yahoo.vespa.athenz.client.zms.ZmsClientException; -import com.yahoo.vespa.hosted.controller.Controller; -import com.yahoo.vespa.hosted.controller.TenantController; -import com.yahoo.vespa.hosted.controller.athenz.ApplicationAction; -import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade; -import com.yahoo.vespa.hosted.controller.role.Role; -import com.yahoo.vespa.hosted.controller.role.RoleMembership; -import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; -import com.yahoo.vespa.hosted.controller.tenant.Tenant; -import com.yahoo.vespa.hosted.controller.tenant.UserTenant; - -import javax.ws.rs.InternalServerErrorException; -import java.security.Principal; -import java.util.Optional; - -import static com.yahoo.vespa.hosted.controller.athenz.HostedAthenzIdentities.SCREWDRIVER_DOMAIN; - -/** - * Translates Athenz principals to role memberships for use in access control. - * - * @author tokle - * @author mpolden - */ -public class AthenzRoleResolver implements RoleMembership.Resolver { - - private final AthenzFacade athenz; - private final TenantController tenants; - private final SystemName system; - - @Inject - public AthenzRoleResolver(AthenzFacade athenz, Controller controller) { - this.athenz = athenz; - this.tenants = controller.tenants(); - this.system = controller.system(); - } - - private boolean isTenantAdmin(AthenzIdentity identity, Tenant tenant) { - if (tenant instanceof AthenzTenant) { - return athenz.hasTenantAdminAccess(identity, ((AthenzTenant) tenant).domain()); - } else if (tenant instanceof UserTenant) { - if (!(identity instanceof AthenzUser)) { - return false; - } - AthenzUser user = (AthenzUser) identity; - return ((UserTenant) tenant).is(user.getName()) || isHostedOperator(identity); - } - throw new InternalServerErrorException("Unknown tenant type: " + tenant.getClass().getSimpleName()); - } - - private boolean hasDeployerAccess(AthenzIdentity identity, AthenzDomain tenantDomain, ApplicationName application) { - try { - return athenz.hasApplicationAccess(identity, - ApplicationAction.deploy, - tenantDomain, - application); - } catch (ZmsClientException e) { - throw new InternalServerErrorException("Failed to authorize operation: (" + e.getMessage() + ")", e); - } - } - - private boolean isHostedOperator(AthenzIdentity identity) { - return athenz.hasHostedOperatorAccess(identity); - } - - @Override - public RoleMembership membership(Principal principal, Optional<String> uriPath) { - if ( ! (principal instanceof AthenzPrincipal)) - throw new IllegalStateException("Expected an AthenzPrincipal to be set on the request."); - - @SuppressWarnings("deprecation") // TODO: Use URI when refactoring this. - Path path = new Path(uriPath.orElseThrow(() -> new IllegalArgumentException("This resolver needs the request path."))); - - path.matches("/application/v4/tenant/{tenant}/{*}"); - Optional<Tenant> tenant = Optional.ofNullable(path.get("tenant")).map(TenantName::from).flatMap(tenants::get); - - path.matches("/application/v4/tenant/{tenant}/application/{application}/{*}"); - Optional<ApplicationName> application = Optional.ofNullable(path.get("application")).map(ApplicationName::from); - - AthenzIdentity identity = ((AthenzPrincipal) principal).getIdentity(); - - RoleMembership.Builder memberships = RoleMembership.in(system); - if (isHostedOperator(identity)) { - memberships.add(Role.hostedOperator); - } - if (tenant.isPresent() && isTenantAdmin(identity, tenant.get())) { - memberships.add(Role.athenzTenantAdmin).limitedTo(tenant.get().name()); - } - AthenzDomain principalDomain = identity.getDomain(); - if (principalDomain.equals(SCREWDRIVER_DOMAIN)) { - if (application.isPresent() && tenant.isPresent()) { - // NOTE: Only fine-grained deploy authorization for Athenz tenants - if (tenant.get() instanceof AthenzTenant) { - AthenzDomain tenantDomain = ((AthenzTenant) tenant.get()).domain(); - if (hasDeployerAccess(identity, tenantDomain, application.get())) { - memberships.add(Role.tenantPipeline).limitedTo(tenant.get().name(), application.get()); - } - } - else { - memberships.add(Role.tenantPipeline).limitedTo(tenant.get().name(), application.get()); - } - } - } - memberships.add(Role.everyone); - return memberships.build(); - } - -} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java index dfcc5f732f8..39736d709d0 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.hosted.controller.restapi.filter; import com.google.inject.Inject; +import com.yahoo.config.provision.SystemName; import com.yahoo.jdisc.Response; import com.yahoo.jdisc.http.HttpRequest; import com.yahoo.jdisc.http.filter.DiscFilterRequest; @@ -9,12 +10,11 @@ import com.yahoo.jdisc.http.filter.security.cors.CorsFilterConfig; import com.yahoo.jdisc.http.filter.security.cors.CorsRequestFilterBase; import com.yahoo.log.LogLevel; import com.yahoo.vespa.hosted.controller.Controller; -import com.yahoo.vespa.hosted.controller.role.Action; -import com.yahoo.vespa.hosted.controller.role.RoleMembership; -import com.yahoo.yolean.chain.After; -import com.yahoo.yolean.chain.Provides; +import com.yahoo.vespa.hosted.controller.api.role.Action; +import com.yahoo.vespa.hosted.controller.api.role.Role; +import com.yahoo.vespa.hosted.controller.api.role.Roles; +import com.yahoo.vespa.hosted.controller.api.role.SecurityContext; -import javax.ws.rs.WebApplicationException; import java.security.Principal; import java.util.Optional; import java.util.Set; @@ -25,45 +25,41 @@ import java.util.logging.Logger; * * @author bjorncs */ -@After("com.yahoo.vespa.hosted.controller.athenz.filter.UserAuthWithAthenzPrincipalFilter") -@Provides("ControllerAuthorizationFilter") public class ControllerAuthorizationFilter extends CorsRequestFilterBase { private static final Logger log = Logger.getLogger(ControllerAuthorizationFilter.class.getName()); - private final RoleMembership.Resolver roleResolver; - private final Controller controller; + private final Roles roles; @Inject - public ControllerAuthorizationFilter(RoleMembership.Resolver roleResolver, - Controller controller, + public ControllerAuthorizationFilter(Controller controller, CorsFilterConfig corsConfig) { - this(roleResolver, controller, Set.copyOf(corsConfig.allowedUrls())); + this(controller.system(), Set.copyOf(corsConfig.allowedUrls())); } - ControllerAuthorizationFilter(RoleMembership.Resolver roleResolver, - Controller controller, + ControllerAuthorizationFilter(SystemName system, Set<String> allowedUrls) { super(allowedUrls); - this.roleResolver = roleResolver; - this.controller = controller; + this.roles = new Roles(system); } @Override public Optional<ErrorResponse> filterRequest(DiscFilterRequest request) { try { Principal principal = request.getUserPrincipal(); - if (principal == null) + Optional<SecurityContext> securityContext = Optional.ofNullable((SecurityContext)request.getAttribute(SecurityContext.ATTRIBUTE_NAME)); + + if (securityContext.isEmpty()) return Optional.of(new ErrorResponse(Response.Status.FORBIDDEN, "Access denied")); Action action = Action.from(HttpRequest.Method.valueOf(request.getMethod())); - // Avoid expensive lookups when request is always legal. - if (RoleMembership.everyoneIn(controller.system()).allows(action, request.getUri())) + // Avoid expensive look-ups when request is always legal. + if (roles.everyone().allows(action, request.getUri())) return Optional.empty(); - RoleMembership roles = this.roleResolver.membership(principal, Optional.of(request.getRequestURI())); - if (roles.allows(action, request.getUri())) + Set<Role> roles = securityContext.get().roles(); + if (roles.stream().anyMatch(role -> role.allows(action, request.getUri()))) return Optional.empty(); } catch (Exception e) { 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 18b124778d5..067e6095b4d 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 @@ -85,4 +85,6 @@ public class UserApiHandler extends LoggingRequestHandler { return response; } + + } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/RoleMembership.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/RoleMembership.java deleted file mode 100644 index 09e66528913..00000000000 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/RoleMembership.java +++ /dev/null @@ -1,122 +0,0 @@ -// 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.role; - -import com.yahoo.config.provision.ApplicationName; -import com.yahoo.config.provision.SystemName; -import com.yahoo.config.provision.TenantName; - -import java.net.URI; -import java.security.Principal; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * A list of roles and their associated contexts. This defines the role membership of a tenant, and in which contexts - * (see {@link Context}) those roles apply. - * - * @author mpolden - * @author jonmv - */ -public class RoleMembership { - - private final Map<Role, Set<Context>> roles; - - private RoleMembership(Map<Role, Set<Context>> roles) { - this.roles = roles.entrySet().stream() - .collect(Collectors.toUnmodifiableMap(entry -> entry.getKey(), - entry -> Set.copyOf(entry.getValue()))); - } - - public static RoleMembership everyoneIn(SystemName system) { - return in(system).add(Role.everyone).build(); - } - - public static Builder in(SystemName system) { return new BuilderWithRole(system); } - - /** Returns whether any role in this allows action to take place in path */ - public boolean allows(Action action, URI uri) { - return roles.entrySet().stream().anyMatch(kv -> { - Role role = kv.getKey(); - Set<Context> contexts = kv.getValue(); - return contexts.stream().anyMatch(context -> role.allows(action, uri, context)); - }); - } - - /** Returns the set of contexts for which the given role is valid. */ - public Set<Context> contextsFor(Role role) { - return roles.getOrDefault(role, Collections.emptySet()); - } - - @Override - public String toString() { - return "roles " + roles; - } - - /** - * A role resolver. Identity providers can implement this to translate their internal representation of role - * membership to a {@link RoleMembership}. - */ - public interface Resolver { - RoleMembership membership(Principal user, Optional<String> path); // TODO get rid of path. - } - - public interface Builder { - - BuilderWithRole add(Role role); - - RoleMembership build(); - - } - - public static class BuilderWithRole implements Builder { - - private final SystemName system; - private final Map<Role, Set<Context>> roles; - - private Role current; - - private BuilderWithRole(SystemName system) { - this.system = Objects.requireNonNull(system); - this.roles = new HashMap<>(); - } - - @Override - public BuilderWithRole add(Role role) { - consumeCurrent(Context.unlimitedIn(system)); - current = role; - return this; - } - - public Builder limitedTo(TenantName tenant) { - consumeCurrent(Context.limitedTo(tenant, system)); - return this; - } - - public Builder limitedTo(TenantName tenant, ApplicationName application) { - consumeCurrent(Context.limitedTo(tenant, application, system)); - return this; - } - - @Override - public RoleMembership build() { - consumeCurrent(Context.unlimitedIn(system)); - return new RoleMembership(roles); - } - - private void consumeCurrent(Context context) { - if (current != null) { - roles.putIfAbsent(current, new HashSet<>()); - roles.get(current).add(context); - } - current = null; - } - - } - -} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java index a22e5259919..b3953c47c01 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java @@ -63,7 +63,7 @@ public class RotationRepository { if (application.rotation().isPresent()) { return allRotations.get(application.rotation().get()); } - if (!application.deploymentSpec().globalServiceId().isPresent()) { + if (application.deploymentSpec().globalServiceId().isEmpty()) { throw new IllegalArgumentException("global-service-id is not set in deployment spec"); } long productionZones = application.deploymentSpec().zones().stream() diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/TenantSpec.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/TenantSpec.java index 2f7dd656678..358088e9b08 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/TenantSpec.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/TenantSpec.java @@ -1,6 +1,7 @@ package com.yahoo.vespa.hosted.controller.security; import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.hosted.controller.tenant.Tenant; import static java.util.Objects.requireNonNull; @@ -14,7 +15,7 @@ public abstract class TenantSpec { private final TenantName tenant; protected TenantSpec(TenantName tenant) { - this.tenant = requireNonNull(tenant); + this.tenant = Tenant.requireName(requireNonNull(tenant)); } /** The name of the tenant. */ diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java index 19b7229515b..e0c750dec80 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java @@ -49,7 +49,7 @@ public abstract class Tenant { return Objects.hash(name); } - static TenantName requireName(TenantName name) { + public static TenantName requireName(TenantName name) { if ( ! name.value().matches("^(?=.{1,20}$)[a-z](-?[a-z0-9]+)*$")) { throw new IllegalArgumentException("New tenant or application names must start with a letter, may " + "contain no more than 20 characters, and may only contain lowercase " + 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 1f00d99350a..bc42b672da4 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 @@ -12,13 +12,14 @@ import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.TenantName; +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; import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision; 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.routing.RoutingEndpoint; import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; @@ -281,40 +282,60 @@ public class ControllerTest { .region("us-central-1") // Two deployments should result in each DNS alias being registered once .build(); - Function<String, Optional<Record>> findCname = (name) -> tester.controllerTester().nameService() - .findRecords(Record.Type.CNAME, - RecordName.from(name)) - .stream() - .findFirst(); - tester.deployCompletely(application, applicationPackage); assertEquals(3, tester.controllerTester().nameService().records().size()); - Optional<Record> record = findCname.apply("app1--tenant1.global.vespa.yahooapis.com"); + Optional<Record> record = tester.controllerTester().findCname("app1--tenant1.global.vespa.yahooapis.com"); assertTrue(record.isPresent()); assertEquals("app1--tenant1.global.vespa.yahooapis.com", record.get().name().asString()); assertEquals("rotation-fqdn-01.", record.get().data().asString()); - record = findCname.apply("app1--tenant1.global.vespa.oath.cloud"); + record = tester.controllerTester().findCname("app1--tenant1.global.vespa.oath.cloud"); assertTrue(record.isPresent()); assertEquals("app1--tenant1.global.vespa.oath.cloud", record.get().name().asString()); assertEquals("rotation-fqdn-01.", record.get().data().asString()); - record = findCname.apply("app1.tenant1.global.vespa.yahooapis.com"); + record = tester.controllerTester().findCname("app1.tenant1.global.vespa.yahooapis.com"); assertTrue(record.isPresent()); assertEquals("app1.tenant1.global.vespa.yahooapis.com", record.get().name().asString()); assertEquals("rotation-fqdn-01.", record.get().data().asString()); } @Test - public void testUpdatesExistingDnsAlias() { + public void testRedirectLegacyDnsNames() { // TODO: Remove together with Flags.REDIRECT_LEGACY_DNS_NAMES DeploymentTester tester = new DeploymentTester(); + Application application = tester.createApplication("app1", "tenant1", 1, 1L); + ApplicationPackage applicationPackage = new ApplicationPackageBuilder() + .environment(Environment.prod) + .globalServiceId("foo") + .region("us-west-1") + .region("us-central-1") + .build(); + + ((InMemoryFlagSource) tester.controller().flagSource()).withBooleanFlag(Flags.REDIRECT_LEGACY_DNS_NAMES.id(), true); + + tester.deployCompletely(application, applicationPackage); + assertEquals(3, tester.controllerTester().nameService().records().size()); + + Optional<Record> record = tester.controllerTester().findCname("app1--tenant1.global.vespa.yahooapis.com"); + assertTrue(record.isPresent()); + assertEquals("app1--tenant1.global.vespa.yahooapis.com", record.get().name().asString()); + assertEquals("app1--tenant1.global.vespa.oath.cloud.", record.get().data().asString()); - Function<String, Optional<Record>> findCname = (name) -> tester.controllerTester().nameService() - .findRecords(Record.Type.CNAME, - RecordName.from(name)) - .stream() - .findFirst(); + record = tester.controllerTester().findCname("app1--tenant1.global.vespa.oath.cloud"); + assertTrue(record.isPresent()); + assertEquals("app1--tenant1.global.vespa.oath.cloud", record.get().name().asString()); + assertEquals("rotation-fqdn-01.", record.get().data().asString()); + + record = tester.controllerTester().findCname("app1.tenant1.global.vespa.yahooapis.com"); + assertTrue(record.isPresent()); + assertEquals("app1.tenant1.global.vespa.yahooapis.com", record.get().name().asString()); + assertEquals("app1--tenant1.global.vespa.oath.cloud.", record.get().data().asString()); + } + + @Test + public void testUpdatesExistingDnsAlias() { + DeploymentTester tester = new DeploymentTester(); // Application 1 is deployed and deleted { @@ -329,12 +350,12 @@ public class ControllerTest { tester.deployCompletely(app1, applicationPackage); assertEquals(3, tester.controllerTester().nameService().records().size()); - Optional<Record> record = findCname.apply("app1--tenant1.global.vespa.yahooapis.com"); + Optional<Record> record = tester.controllerTester().findCname("app1--tenant1.global.vespa.yahooapis.com"); assertTrue(record.isPresent()); assertEquals("app1--tenant1.global.vespa.yahooapis.com", record.get().name().asString()); assertEquals("rotation-fqdn-01.", record.get().data().asString()); - record = findCname.apply("app1.tenant1.global.vespa.yahooapis.com"); + record = tester.controllerTester().findCname("app1.tenant1.global.vespa.yahooapis.com"); assertTrue(record.isPresent()); assertEquals("app1.tenant1.global.vespa.yahooapis.com", record.get().name().asString()); assertEquals("rotation-fqdn-01.", record.get().data().asString()); @@ -356,13 +377,13 @@ public class ControllerTest { } // Records remain - record = findCname.apply("app1--tenant1.global.vespa.yahooapis.com"); + record = tester.controllerTester().findCname("app1--tenant1.global.vespa.yahooapis.com"); assertTrue(record.isPresent()); - record = findCname.apply("app1--tenant1.global.vespa.oath.cloud"); + record = tester.controllerTester().findCname("app1--tenant1.global.vespa.oath.cloud"); assertTrue(record.isPresent()); - record = findCname.apply("app1.tenant1.global.vespa.yahooapis.com"); + record = tester.controllerTester().findCname("app1.tenant1.global.vespa.yahooapis.com"); assertTrue(record.isPresent()); } @@ -378,17 +399,17 @@ public class ControllerTest { tester.deployCompletely(app2, applicationPackage); assertEquals(6, tester.controllerTester().nameService().records().size()); - Optional<Record> record = findCname.apply("app2--tenant2.global.vespa.yahooapis.com"); + Optional<Record> record = tester.controllerTester().findCname("app2--tenant2.global.vespa.yahooapis.com"); assertTrue(record.isPresent()); assertEquals("app2--tenant2.global.vespa.yahooapis.com", record.get().name().asString()); assertEquals("rotation-fqdn-01.", record.get().data().asString()); - record = findCname.apply("app2--tenant2.global.vespa.oath.cloud"); + record = tester.controllerTester().findCname("app2--tenant2.global.vespa.oath.cloud"); assertTrue(record.isPresent()); assertEquals("app2--tenant2.global.vespa.oath.cloud", record.get().name().asString()); assertEquals("rotation-fqdn-01.", record.get().data().asString()); - record = findCname.apply("app2.tenant2.global.vespa.yahooapis.com"); + record = tester.controllerTester().findCname("app2.tenant2.global.vespa.yahooapis.com"); assertTrue(record.isPresent()); assertEquals("app2.tenant2.global.vespa.yahooapis.com", record.get().name().asString()); assertEquals("rotation-fqdn-01.", record.get().data().asString()); @@ -411,15 +432,15 @@ public class ControllerTest { // Existing DNS records are updated to point to the newly assigned rotation assertEquals(6, tester.controllerTester().nameService().records().size()); - Optional<Record> record = findCname.apply("app1--tenant1.global.vespa.yahooapis.com"); + Optional<Record> record = tester.controllerTester().findCname("app1--tenant1.global.vespa.yahooapis.com"); assertTrue(record.isPresent()); assertEquals("rotation-fqdn-02.", record.get().data().asString()); - record = findCname.apply("app1--tenant1.global.vespa.oath.cloud"); + record = tester.controllerTester().findCname("app1--tenant1.global.vespa.oath.cloud"); assertTrue(record.isPresent()); assertEquals("rotation-fqdn-02.", record.get().data().asString()); - record = findCname.apply("app1.tenant1.global.vespa.yahooapis.com"); + record = tester.controllerTester().findCname("app1.tenant1.global.vespa.yahooapis.com"); assertTrue(record.isPresent()); assertEquals("rotation-fqdn-02.", record.get().data().asString()); } 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 d7845e4bfa1..c18e9c46f07 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 @@ -13,6 +13,7 @@ import com.yahoo.vespa.athenz.api.AthenzUser; import com.yahoo.vespa.athenz.api.OktaAccessToken; import com.yahoo.vespa.curator.Lock; import com.yahoo.vespa.curator.mock.MockCurator; +import com.yahoo.vespa.flags.InMemoryFlagSource; import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions; import com.yahoo.vespa.hosted.controller.api.identifiers.Property; import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; @@ -22,14 +23,15 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationS import com.yahoo.vespa.hosted.controller.api.integration.deployment.ArtifactRepository; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.api.integration.dns.MemoryNameService; -import com.yahoo.vespa.hosted.controller.api.integration.entity.MemoryEntityService; +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.github.GitHubMock; import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact; import com.yahoo.vespa.hosted.controller.api.integration.organization.MockContactRetriever; import com.yahoo.vespa.hosted.controller.api.integration.organization.MockIssueHandler; -import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMailer; import com.yahoo.vespa.hosted.controller.api.integration.routing.RoutingGenerator; import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockBuildService; +import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMailer; import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockRunDataStore; import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockTesterCloud; import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; @@ -43,11 +45,11 @@ import com.yahoo.vespa.hosted.controller.integration.ConfigServerMock; import com.yahoo.vespa.hosted.controller.integration.MetricsServiceMock; import com.yahoo.vespa.hosted.controller.integration.RoutingGeneratorMock; import com.yahoo.vespa.hosted.controller.integration.ZoneRegistryMock; -import com.yahoo.vespa.hosted.controller.security.AthenzCredentials; -import com.yahoo.vespa.hosted.controller.security.AthenzTenantSpec; import com.yahoo.vespa.hosted.controller.persistence.ApplicationSerializer; import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb; +import com.yahoo.vespa.hosted.controller.security.AthenzCredentials; +import com.yahoo.vespa.hosted.controller.security.AthenzTenantSpec; import com.yahoo.vespa.hosted.controller.security.Credentials; import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; import com.yahoo.vespa.hosted.controller.tenant.Tenant; @@ -188,6 +190,10 @@ public final class ControllerTester { return contactRetriever; } + public Optional<Record> findCname(String name) { + return nameService.findRecords(Record.Type.CNAME, RecordName.from(name)).stream().findFirst(); + } + /** Create a new controller instance. Useful to verify that controller state is rebuilt from persistence */ public final void createNewController() { controller = createController(curator, rotationsConfig, configServer, clock, gitHub, zoneRegistry, athenzDb, @@ -345,7 +351,8 @@ public final class ControllerTester { buildService, new MockRunDataStore(), () -> "test-controller", - new MockMailer()); + new MockMailer(), + new InMemoryFlagSource()); // Calculate initial versions controller.updateVersionStatus(VersionStatus.compute(controller)); return controller; diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainerTest.java index 23c7ec537f5..2b8e4f52d23 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainerTest.java @@ -4,7 +4,6 @@ package com.yahoo.vespa.hosted.controller.maintenance; import com.yahoo.config.application.api.ValidationId; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.RegionName; -import com.yahoo.vespa.athenz.api.OktaAccessToken; import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.ControllerTester; import com.yahoo.vespa.hosted.controller.api.integration.dns.Record; 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 331a6ba9ac8..ddc8d68e08b 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 @@ -63,6 +63,7 @@ public class ControllerContainerTest { " <item>http://localhost</item>\n" + " </allowedUrls>\n" + " </config>\n" + + " <component id='com.yahoo.vespa.flags.InMemoryFlagSource'/>\n" + " <component id='com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb'/>\n" + " <component id='com.yahoo.vespa.hosted.controller.athenz.mock.AthenzClientFactoryMock'/>\n" + " <component id='com.yahoo.vespa.hosted.controller.api.integration.chef.ChefMock'/>\n" + @@ -96,7 +97,6 @@ public class ControllerContainerTest { " <component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMailer'/>\n" + " <component id='com.yahoo.vespa.hosted.controller.security.AthenzAccessControlRequests'/>\n" + " <component id='com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade'/>\n" + - " <component id='com.yahoo.vespa.hosted.controller.restapi.filter.AthenzRoleResolver'/>\n" + " <handler id='com.yahoo.vespa.hosted.controller.restapi.application.ApplicationApiHandler'>\n" + " <binding>http://*/application/v4/*</binding>\n" + " </handler>\n" + @@ -134,6 +134,7 @@ public class ControllerContainerTest { " <filtering>\n" + " <request-chain id='default'>\n" + " <filter id='com.yahoo.vespa.hosted.controller.integration.AthenzFilterMock'/>\n" + + " <filter id='com.yahoo.vespa.hosted.controller.restapi.filter.AthenzRoleFilter'/>\n" + " <filter id='com.yahoo.vespa.hosted.controller.restapi.filter.ControllerAuthorizationFilter'/>\n" + " <binding>http://*/*</binding>\n" + " </request-chain>\n" + diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java index 9db0c17a40c..bde1c037bf2 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java @@ -758,31 +758,6 @@ public class ApplicationApiTest extends ControllerContainerTest { new File("deploy-no-deployment.json"), 400); } - // Tests deployment to config server when using just on API call - // For now this depends on a switch in ApplicationController that does this for by- tenants in CD only - @Test - public void testDeployDirectlyUsingOneCallForDeploy() { - // Setup - tester.computeVersionStatus(); - UserId userId = new UserId("new_user"); - createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, userId); - - // Create tenant - // PUT (create) the authenticated user - byte[] data = new byte[0]; - tester.assertResponse(request("/application/v4/user?user=new_user&domain=by", PUT) - .data(data) - .userIdentity(userId), // Normalized to by-new-user by API - new File("create-user-response.json")); - - // POST (deploy) an application to a dev zone - HttpEntity entity = createApplicationDeployData(applicationPackage, true); - tester.assertResponse(request("/application/v4/tenant/by-new-user/application/application1/environment/dev/region/cd-us-central-1/instance/default", POST) - .data(entity) - .userIdentity(userId), - new File("deploy-result.json")); - } - @Test public void testSortsDeploymentsAndJobs() { tester.computeVersionStatus(); @@ -897,7 +872,7 @@ public class ApplicationApiTest extends ControllerContainerTest { "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Tenant 'tenant1' already exists\"}", 400); - // POST (add) a Athenz tenant with underscore in name + // POST (add) an Athenz tenant with underscore in name tester.assertResponse(request("/application/v4/tenant/my_tenant_2", POST) .userIdentity(USER_ID) .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}") @@ -905,7 +880,7 @@ public class ApplicationApiTest extends ControllerContainerTest { "{\"error-code\":\"BAD_REQUEST\",\"message\":\"New tenant or application names must start with a letter, may contain no more than 20 characters, and may only contain lowercase letters, digits or dashes, but no double-dashes.\"}", 400); - // POST (add) a Athenz tenant with by- prefix + // POST (add) an Athenz tenant with by- prefix tester.assertResponse(request("/application/v4/tenant/by-tenant2", POST) .userIdentity(USER_ID) .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}") @@ -913,6 +888,14 @@ public class ApplicationApiTest extends ControllerContainerTest { "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Athenz tenant name cannot have prefix 'by-'\"}", 400); + // POST (add) an Athenz tenant with a reserved name + tester.assertResponse(request("/application/v4/tenant/hosted-vespa", POST) + .userIdentity(USER_ID) + .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}") + .oktaAccessToken(OKTA_AT), + "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Tenant 'hosted-vespa' already exists\"}", + 400); + // POST (create) an (empty) application tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST) .userIdentity(USER_ID) 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 new file mode 100644 index 00000000000..dc4235e52bf --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilterTest.java @@ -0,0 +1,122 @@ +package com.yahoo.vespa.hosted.controller.restapi.filter; + +import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.TenantName; +import com.yahoo.jdisc.http.filter.security.cors.CorsFilterConfig; +import com.yahoo.vespa.athenz.api.AthenzDomain; +import com.yahoo.vespa.athenz.api.AthenzPrincipal; +import com.yahoo.vespa.athenz.api.AthenzService; +import com.yahoo.vespa.athenz.api.AthenzUser; +import com.yahoo.vespa.hosted.controller.ControllerTester; +import com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId; +import com.yahoo.vespa.hosted.controller.api.identifiers.ScrewdriverId; +import com.yahoo.vespa.hosted.controller.api.role.Roles; +import com.yahoo.vespa.hosted.controller.athenz.ApplicationAction; +import com.yahoo.vespa.hosted.controller.athenz.HostedAthenzIdentities; +import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade; +import com.yahoo.vespa.hosted.controller.athenz.mock.AthenzClientFactoryMock; +import com.yahoo.vespa.hosted.controller.athenz.mock.AthenzDbMock; +import org.junit.Before; +import org.junit.Test; + +import java.net.URI; +import java.util.Set; + +import static org.junit.Assert.assertEquals; + +/** + * @author jonmv + */ +public class AthenzRoleFilterTest { + + private static final AthenzPrincipal USER = new AthenzPrincipal(new AthenzUser("john")); + private static final AthenzPrincipal HOSTED_OPERATOR = new AthenzPrincipal(new AthenzUser("hosted-operator")); + private static final AthenzDomain TENANT_DOMAIN = new AthenzDomain("tenantdomain"); + private static final AthenzDomain TENANT_DOMAIN2 = new AthenzDomain("tenantdomain2"); + private static final AthenzPrincipal TENANT_ADMIN = new AthenzPrincipal(new AthenzService(TENANT_DOMAIN, "adminservice")); + private static final AthenzPrincipal TENANT_PIPELINE = new AthenzPrincipal(HostedAthenzIdentities.from(new ScrewdriverId("12345"))); + private static final TenantName TENANT = TenantName.from("mytenant"); + private static final TenantName TENANT2 = TenantName.from("othertenant"); + private static final ApplicationName APPLICATION = ApplicationName.from("myapp"); + private static final URI NO_CONTEXT_PATH = URI.create("/application/v4/"); + private static final URI TENANT_CONTEXT_PATH = URI.create("/application/v4/tenant/mytenant/"); + private static final URI APPLICATION_CONTEXT_PATH = URI.create("/application/v4/tenant/mytenant/application/myapp/"); + private static final URI TENANT2_CONTEXT_PATH = URI.create("/application/v4/tenant/othertenant/"); + private static final URI APPLICATION2_CONTEXT_PATH = URI.create("/application/v4/tenant/othertenant/application/myapp/"); + + private ControllerTester tester; + private AthenzRoleFilter filter; + + @Before + public void setup() { + tester = new ControllerTester(); + filter = new AthenzRoleFilter(new CorsFilterConfig.Builder().build(), + new AthenzFacade(new AthenzClientFactoryMock(tester.athenzDb())), + tester.controller()); + + tester.athenzDb().hostedOperators.add(HOSTED_OPERATOR.getIdentity()); + tester.createTenant(TENANT.value(), TENANT_DOMAIN.getName(), null); + tester.createApplication(TENANT, APPLICATION.value(), "default", 12345); + AthenzDbMock.Domain tenantDomain = tester.athenzDb().domains.get(TENANT_DOMAIN); + tenantDomain.admins.add(TENANT_ADMIN.getIdentity()); + tenantDomain.applications.get(new ApplicationId(APPLICATION.value())).addRoleMember(ApplicationAction.deploy, TENANT_PIPELINE.getIdentity()); + tester.createTenant(TENANT2.value(), TENANT_DOMAIN2.getName(), null); + tester.createApplication(TENANT2, APPLICATION.value(), "default", 42); + } + + @Test + public void testTranslations() { + + Roles roles = new Roles(tester.controller().system()); + + // Hosted operators are always members of the hostedOperator role. + assertEquals(Set.of(roles.hostedOperator()), + filter.roles(HOSTED_OPERATOR, NO_CONTEXT_PATH)); + + assertEquals(Set.of(roles.hostedOperator()), + filter.roles(HOSTED_OPERATOR, TENANT_CONTEXT_PATH)); + + assertEquals(Set.of(roles.hostedOperator()), + filter.roles(HOSTED_OPERATOR, APPLICATION_CONTEXT_PATH)); + + // Tenant admins are members of the athenzTenantAdmin role within their tenant subtree. + assertEquals(Set.of(roles.everyone()), + filter.roles(TENANT_PIPELINE, NO_CONTEXT_PATH)); + + assertEquals(Set.of(roles.athenzTenantAdmin(TENANT)), + filter.roles(TENANT_ADMIN, TENANT_CONTEXT_PATH)); + + assertEquals(Set.of(roles.athenzTenantAdmin(TENANT)), + filter.roles(TENANT_ADMIN, APPLICATION_CONTEXT_PATH)); + + assertEquals(Set.of(roles.everyone()), + filter.roles(TENANT_ADMIN, TENANT2_CONTEXT_PATH)); + + assertEquals(Set.of(roles.everyone()), + filter.roles(TENANT_ADMIN, APPLICATION2_CONTEXT_PATH)); + + // Build services are members of the tenantPipeline role within their application subtree. + assertEquals(Set.of(roles.everyone()), + filter.roles(TENANT_PIPELINE, NO_CONTEXT_PATH)); + + assertEquals(Set.of(roles.everyone()), + filter.roles(TENANT_PIPELINE, TENANT_CONTEXT_PATH)); + + assertEquals(Set.of(roles.tenantPipeline(TENANT, APPLICATION)), + filter.roles(TENANT_PIPELINE, APPLICATION_CONTEXT_PATH)); + + assertEquals(Set.of(roles.everyone()), + filter.roles(TENANT_PIPELINE, APPLICATION2_CONTEXT_PATH)); + + // Unprivileged users are just members of the everyone role. + assertEquals(Set.of(roles.everyone()), + filter.roles(USER, NO_CONTEXT_PATH)); + + assertEquals(Set.of(roles.everyone()), + filter.roles(USER, TENANT_CONTEXT_PATH)); + + assertEquals(Set.of(roles.everyone()), + filter.roles(USER, APPLICATION_CONTEXT_PATH)); + } + +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleResolverTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleResolverTest.java deleted file mode 100644 index 4628b95ad3c..00000000000 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleResolverTest.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.yahoo.vespa.hosted.controller.restapi.filter; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.yahoo.config.provision.ApplicationName; -import com.yahoo.config.provision.TenantName; -import com.yahoo.vespa.athenz.api.AthenzDomain; -import com.yahoo.vespa.athenz.api.AthenzPrincipal; -import com.yahoo.vespa.athenz.api.AthenzService; -import com.yahoo.vespa.athenz.api.AthenzUser; -import com.yahoo.vespa.hosted.controller.ControllerTester; -import com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId; -import com.yahoo.vespa.hosted.controller.api.identifiers.ScrewdriverId; -import com.yahoo.vespa.hosted.controller.athenz.ApplicationAction; -import com.yahoo.vespa.hosted.controller.athenz.HostedAthenzIdentities; -import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade; -import com.yahoo.vespa.hosted.controller.athenz.mock.AthenzClientFactoryMock; -import com.yahoo.vespa.hosted.controller.athenz.mock.AthenzDbMock; -import com.yahoo.vespa.hosted.controller.role.Context; -import com.yahoo.vespa.hosted.controller.role.Role; -import org.junit.Before; -import org.junit.Test; - -import java.util.Optional; -import java.util.Set; - -import static java.util.Collections.emptySet; -import static org.junit.Assert.assertEquals; - -/** - * @author jonmv - */ -public class AthenzRoleResolverTest { - - private static final ObjectMapper mapper = new ObjectMapper(); - - private static final AthenzPrincipal USER = new AthenzPrincipal(new AthenzUser("john")); - private static final AthenzPrincipal HOSTED_OPERATOR = new AthenzPrincipal(new AthenzUser("hosted-operator")); - private static final AthenzDomain TENANT_DOMAIN = new AthenzDomain("tenantdomain"); - private static final AthenzDomain TENANT_DOMAIN2 = new AthenzDomain("tenantdomain2"); - private static final AthenzPrincipal TENANT_ADMIN = new AthenzPrincipal(new AthenzService(TENANT_DOMAIN, "adminservice")); - private static final AthenzPrincipal TENANT_PIPELINE = new AthenzPrincipal(HostedAthenzIdentities.from(new ScrewdriverId("12345"))); - private static final TenantName TENANT = TenantName.from("mytenant"); - private static final TenantName TENANT2 = TenantName.from("othertenant"); - private static final ApplicationName APPLICATION = ApplicationName.from("myapp"); - private static final Optional<String> NO_CONTEXT_PATH = Optional.of("/application/v4/"); - private static final Optional<String> TENANT_CONTEXT_PATH = Optional.of("/application/v4/tenant/mytenant/"); - private static final Optional<String> APPLICATION_CONTEXT_PATH = Optional.of("/application/v4/tenant/mytenant/application/myapp/"); - private static final Optional<String> TENANT2_CONTEXT_PATH = Optional.of("/application/v4/tenant/othertenant/"); - private static final Optional<String> APPLICATION2_CONTEXT_PATH = Optional.of("/application/v4/tenant/othertenant/application/myapp/"); - - private ControllerTester tester; - private AthenzRoleResolver resolver; - - @Before - public void setup() { - tester = new ControllerTester(); - resolver = new AthenzRoleResolver(new AthenzFacade(new AthenzClientFactoryMock(tester.athenzDb())), - tester.controller()); - - tester.athenzDb().hostedOperators.add(HOSTED_OPERATOR.getIdentity()); - tester.createTenant(TENANT.value(), TENANT_DOMAIN.getName(), null); - tester.createApplication(TENANT, APPLICATION.value(), "default", 12345); - AthenzDbMock.Domain tenantDomain = tester.athenzDb().domains.get(TENANT_DOMAIN); - tenantDomain.admins.add(TENANT_ADMIN.getIdentity()); - tenantDomain.applications.get(new ApplicationId(APPLICATION.value())).addRoleMember(ApplicationAction.deploy, TENANT_PIPELINE.getIdentity()); - tester.createTenant(TENANT2.value(), TENANT_DOMAIN2.getName(), null); - tester.createApplication(TENANT2, APPLICATION.value(), "default", 42); - } - - @Test - public void testTranslations() { - - // Everyone is member of the everyone role. - assertEquals(Set.of(Context.unlimitedIn(tester.controller().system())), - resolver.membership(HOSTED_OPERATOR, APPLICATION_CONTEXT_PATH).contextsFor(Role.everyone)); - assertEquals(Set.of(Context.unlimitedIn(tester.controller().system())), - resolver.membership(TENANT_ADMIN, TENANT_CONTEXT_PATH).contextsFor(Role.everyone)); - assertEquals(Set.of(Context.unlimitedIn(tester.controller().system())), - resolver.membership(TENANT_PIPELINE, NO_CONTEXT_PATH).contextsFor(Role.everyone)); - assertEquals(Set.of(Context.unlimitedIn(tester.controller().system())), - resolver.membership(USER, APPLICATION_CONTEXT_PATH).contextsFor(Role.everyone)); - - // Only operators are members of the operator role. - assertEquals(Set.of(Context.unlimitedIn(tester.controller().system())), - resolver.membership(HOSTED_OPERATOR, TENANT_CONTEXT_PATH).contextsFor(Role.hostedOperator)); - assertEquals(emptySet(), - resolver.membership(TENANT_ADMIN, NO_CONTEXT_PATH).contextsFor(Role.hostedOperator)); - assertEquals(emptySet(), - resolver.membership(TENANT_PIPELINE, APPLICATION_CONTEXT_PATH).contextsFor(Role.hostedOperator)); - assertEquals(emptySet(), - resolver.membership(USER, TENANT_CONTEXT_PATH).contextsFor(Role.hostedOperator)); - - // Operators and tenant admins are tenant admins of their tenants. - assertEquals(Set.of(Context.limitedTo(TENANT, tester.controller().system())), - resolver.membership(HOSTED_OPERATOR, APPLICATION_CONTEXT_PATH).contextsFor(Role.athenzTenantAdmin)); - assertEquals(emptySet(), // TODO this is wrong, but we can't do better until we ask ZMS for roles. - resolver.membership(TENANT_ADMIN, NO_CONTEXT_PATH).contextsFor(Role.athenzTenantAdmin)); - assertEquals(Set.of(Context.limitedTo(TENANT, tester.controller().system())), - resolver.membership(TENANT_ADMIN, TENANT_CONTEXT_PATH).contextsFor(Role.athenzTenantAdmin)); - assertEquals(emptySet(), - resolver.membership(TENANT_ADMIN, TENANT2_CONTEXT_PATH).contextsFor(Role.athenzTenantAdmin)); - assertEquals(emptySet(), - resolver.membership(TENANT_PIPELINE, APPLICATION_CONTEXT_PATH).contextsFor(Role.athenzTenantAdmin)); - assertEquals(emptySet(), - resolver.membership(USER, TENANT_CONTEXT_PATH).contextsFor(Role.athenzTenantAdmin)); - - // Only build services are pipeline operators of their applications. - assertEquals(emptySet(), - resolver.membership(HOSTED_OPERATOR, APPLICATION_CONTEXT_PATH).contextsFor(Role.tenantPipeline)); - assertEquals(emptySet(), - resolver.membership(TENANT_ADMIN, APPLICATION_CONTEXT_PATH).contextsFor(Role.tenantPipeline)); - assertEquals(Set.of(Context.limitedTo(TENANT, APPLICATION, tester.controller().system())), - resolver.membership(TENANT_PIPELINE, APPLICATION_CONTEXT_PATH).contextsFor(Role.tenantPipeline)); - assertEquals(emptySet(), - resolver.membership(TENANT_PIPELINE, APPLICATION2_CONTEXT_PATH).contextsFor(Role.tenantPipeline)); - assertEquals(emptySet(), - resolver.membership(USER, APPLICATION_CONTEXT_PATH).contextsFor(Role.tenantPipeline)); - } - -} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilterTest.java index 39b08695986..105e10eefd2 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilterTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilterTest.java @@ -6,13 +6,10 @@ import com.yahoo.application.container.handler.Request; import com.yahoo.config.provision.SystemName; import com.yahoo.jdisc.http.HttpRequest.Method; import com.yahoo.jdisc.http.filter.DiscFilterRequest; -import com.yahoo.vespa.athenz.api.AthenzIdentity; -import com.yahoo.vespa.athenz.api.AthenzPrincipal; -import com.yahoo.vespa.athenz.api.AthenzUser; import com.yahoo.vespa.hosted.controller.ControllerTester; +import com.yahoo.vespa.hosted.controller.api.role.Roles; +import com.yahoo.vespa.hosted.controller.api.role.SecurityContext; import com.yahoo.vespa.hosted.controller.restapi.ApplicationRequestToDiscFilterRequestWrapper; -import com.yahoo.vespa.hosted.controller.role.Role; -import com.yahoo.vespa.hosted.controller.role.RoleMembership; import org.junit.Test; import java.io.IOException; @@ -33,39 +30,42 @@ import static org.junit.Assert.assertTrue; public class ControllerAuthorizationFilterTest { private static final ObjectMapper mapper = new ObjectMapper(); - private static AthenzIdentity identity = new AthenzUser("user"); @Test public void operator() { ControllerTester tester = new ControllerTester(); - RoleMembership.Resolver operatorResolver = (user, path) -> RoleMembership.in(tester.controller().system()) - .add(Role.hostedOperator) - .build(); - ControllerAuthorizationFilter filter = createFilter(tester, operatorResolver); - assertIsAllowed(invokeFilter(filter, createRequest(Method.POST, "/zone/v2/path", identity))); - assertIsAllowed(invokeFilter(filter, createRequest(Method.PUT, "/application/v4/user", identity))); - assertIsAllowed(invokeFilter(filter, createRequest(Method.GET, "/zone/v1/path", identity))); + Roles roles = new Roles(tester.controller().system()); + SecurityContext securityContext = new SecurityContext(() -> "operator", Set.of(roles.hostedOperator())); + ControllerAuthorizationFilter filter = createFilter(tester); + + assertIsAllowed(invokeFilter(filter, createRequest(Method.POST, "/zone/v2/path", securityContext))); + assertIsAllowed(invokeFilter(filter, createRequest(Method.PUT, "/application/v4/user", securityContext))); + assertIsAllowed(invokeFilter(filter, createRequest(Method.GET, "/zone/v1/path", securityContext))); } @Test public void unprivileged() { ControllerTester tester = new ControllerTester(); - RoleMembership.Resolver emptyResolver = (user, path) -> RoleMembership.in(tester.controller().system()).build(); - ControllerAuthorizationFilter filter = createFilter(tester, emptyResolver); - assertIsForbidden(invokeFilter(filter, createRequest(Method.POST, "/zone/v2/path", identity))); - assertIsAllowed(invokeFilter(filter, createRequest(Method.PUT, "/application/v4/user", identity))); - assertIsAllowed(invokeFilter(filter, createRequest(Method.GET, "/zone/v1/path", identity))); + Roles roles = new Roles(tester.controller().system()); + SecurityContext securityContext = new SecurityContext(() -> "user", Set.of(roles.everyone())); + ControllerAuthorizationFilter filter = createFilter(tester); + + assertIsForbidden(invokeFilter(filter, createRequest(Method.POST, "/zone/v2/path", securityContext))); + assertIsAllowed(invokeFilter(filter, createRequest(Method.PUT, "/application/v4/user", securityContext))); + assertIsAllowed(invokeFilter(filter, createRequest(Method.GET, "/zone/v1/path", securityContext))); } @Test public void unprivilegedInPublic() { ControllerTester tester = new ControllerTester(); tester.zoneRegistry().setSystemName(SystemName.Public); - RoleMembership.Resolver emptyResolver = (user, path) -> RoleMembership.in(tester.controller().system()).build(); - ControllerAuthorizationFilter filter = createFilter(tester, emptyResolver); - assertIsForbidden(invokeFilter(filter, createRequest(Method.POST, "/zone/v2/path", identity))); - assertIsForbidden(invokeFilter(filter, createRequest(Method.PUT, "/application/v4/user", identity))); - assertIsAllowed(invokeFilter(filter, createRequest(Method.GET, "/zone/v1/path", identity))); + Roles roles = new Roles(tester.controller().system()); + SecurityContext securityContext = new SecurityContext(() -> "user", Set.of(roles.everyone())); + + ControllerAuthorizationFilter filter = createFilter(tester); + assertIsForbidden(invokeFilter(filter, createRequest(Method.POST, "/zone/v2/path", securityContext))); + assertIsForbidden(invokeFilter(filter, createRequest(Method.PUT, "/application/v4/user", securityContext))); + assertIsAllowed(invokeFilter(filter, createRequest(Method.GET, "/zone/v1/path", securityContext))); } private static void assertIsAllowed(Optional<AuthorizationResponse> response) { @@ -79,8 +79,8 @@ public class ControllerAuthorizationFilterTest { assertEquals("Invalid status code", FORBIDDEN, response.get().statusCode); } - private static ControllerAuthorizationFilter createFilter(ControllerTester tester, RoleMembership.Resolver resolver) { - return new ControllerAuthorizationFilter(resolver, tester.controller(), Set.of("http://localhost")); + private static ControllerAuthorizationFilter createFilter(ControllerTester tester) { + return new ControllerAuthorizationFilter(tester.controller().system(), Set.of("http://localhost")); } private static Optional<AuthorizationResponse> invokeFilter(ControllerAuthorizationFilter filter, @@ -91,9 +91,9 @@ public class ControllerAuthorizationFilterTest { .map(response -> new AuthorizationResponse(response.getStatus(), getErrorMessage(responseHandlerMock))); } - private static DiscFilterRequest createRequest(Method method, String path, AthenzIdentity identity) { - Request request = new Request(path, new byte[0], Request.Method.valueOf(method.name()), - new AthenzPrincipal(identity)); + private static DiscFilterRequest createRequest(Method method, String path, SecurityContext securityContext) { + Request request = new Request(path, new byte[0], Request.Method.valueOf(method.name()), securityContext.principal()); + request.getAttributes().put(SecurityContext.ATTRIBUTE_NAME, securityContext); return new ApplicationRequestToDiscFilterRequestWrapper(request); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/role/RoleMembershipTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/role/RoleMembershipTest.java deleted file mode 100644 index 1da5d3764f6..00000000000 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/role/RoleMembershipTest.java +++ /dev/null @@ -1,86 +0,0 @@ -// 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.role; - -import com.yahoo.config.provision.ApplicationName; -import com.yahoo.config.provision.SystemName; -import com.yahoo.config.provision.TenantName; -import org.junit.Test; - -import java.net.URI; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -/** - * @author mpolden - */ -public class RoleMembershipTest { - - @Test - public void operator_membership() { - RoleMembership roles = RoleMembership.in(SystemName.main) - .add(Role.hostedOperator) - .build(); - - // Operator actions - assertFalse(roles.allows(Action.create, URI.create("/not/explicitly/defined"))); - assertTrue(roles.allows(Action.create, URI.create("/controller/v1/foo"))); - assertTrue(roles.allows(Action.update, URI.create("/os/v1/bar"))); - assertTrue(roles.allows(Action.update, URI.create("/application/v4/tenant/t1/application/a1"))); - assertTrue(roles.allows(Action.update, URI.create("/application/v4/tenant/t2/application/a2"))); - } - - @Test - public void tenant_membership() { - RoleMembership roles = RoleMembership.in(SystemName.main) - .add(Role.athenzTenantAdmin).limitedTo(TenantName.from("t1"), ApplicationName.from("a1")) - .build(); - assertFalse(roles.allows(Action.create, URI.create("/not/explicitly/defined"))); - assertFalse("Deny access to operator API", roles.allows(Action.create, URI.create("/controller/v1/foo"))); - assertFalse("Deny access to other tenant and app", roles.allows(Action.update, URI.create("/application/v4/tenant/t2/application/a2"))); - assertFalse("Deny access to other app", roles.allows(Action.update, URI.create("/application/v4/tenant/t1/application/a2"))); - assertTrue(roles.allows(Action.update, URI.create("/application/v4/tenant/t1/application/a1"))); - - RoleMembership multiContext = RoleMembership.in(SystemName.main) - .add(Role.athenzTenantAdmin).limitedTo(TenantName.from("t1"), ApplicationName.from("a1")) - .add(Role.athenzTenantAdmin).limitedTo(TenantName.from("t2"), ApplicationName.from("a2")) - .build(); - assertFalse("Deny access to other tenant and app", multiContext.allows(Action.update, URI.create("/application/v4/tenant/t3/application/a3"))); - assertTrue(multiContext.allows(Action.update, URI.create("/application/v4/tenant/t2/application/a2"))); - assertTrue(multiContext.allows(Action.update, URI.create("/application/v4/tenant/t1/application/a1"))); - - RoleMembership publicSystem = RoleMembership.in(SystemName.vaas) - .add(Role.athenzTenantAdmin).limitedTo(TenantName.from("t1"), ApplicationName.from("a1")) - .build(); - assertFalse(publicSystem.allows(Action.read, URI.create("/controller/v1/foo"))); - assertTrue(multiContext.allows(Action.update, URI.create("/application/v4/tenant/t1/application/a1"))); - } - - @Test - public void build_service_membership() { - RoleMembership roles = RoleMembership.in(SystemName.main) - .add(Role.tenantPipeline).build(); - assertFalse(roles.allows(Action.create, URI.create("/not/explicitly/defined"))); - assertFalse(roles.allows(Action.update, URI.create("/application/v4/tenant/t1/application/a1"))); - assertTrue(roles.allows(Action.create, URI.create("/application/v4/tenant/t1/application/a1/jobreport"))); - assertFalse("No global read access", roles.allows(Action.read, URI.create("/controller/v1/foo"))); - } - - @Test - public void multi_role_membership() { - RoleMembership roles = RoleMembership.in(SystemName.main) - .add(Role.athenzTenantAdmin).limitedTo(TenantName.from("t1"), ApplicationName.from("a1")) - .add(Role.tenantPipeline) - .add(Role.everyone) - .build(); - assertFalse(roles.allows(Action.create, URI.create("/not/explicitly/defined"))); - assertFalse(roles.allows(Action.create, URI.create("/controller/v1/foo"))); - assertTrue(roles.allows(Action.create, URI.create("/application/v4/tenant/t1/application/a1/jobreport"))); - assertTrue(roles.allows(Action.update, URI.create("/application/v4/tenant/t1/application/a1"))); - assertTrue("Global read access", roles.allows(Action.read, URI.create("/controller/v1/foo"))); - assertTrue("Dashboard read access", roles.allows(Action.read, URI.create("/"))); - assertTrue("Dashboard read access", roles.allows(Action.read, URI.create("/d/nodes"))); - assertTrue("Dashboard read access", roles.allows(Action.read, URI.create("/statuspage/v1/incidents"))); - } - -} diff --git a/document/src/main/java/com/yahoo/document/CollectionDataType.java b/document/src/main/java/com/yahoo/document/CollectionDataType.java index a73588a710c..c6420b5e71f 100644 --- a/document/src/main/java/com/yahoo/document/CollectionDataType.java +++ b/document/src/main/java/com/yahoo/document/CollectionDataType.java @@ -32,7 +32,6 @@ public abstract class CollectionDataType extends DataType { return type; } - @SuppressWarnings("deprecation") public DataType getNestedType() { return nestedType; } @@ -58,11 +57,7 @@ public abstract class CollectionDataType extends DataType { return false; } CollectionFieldValue cfv = (CollectionFieldValue) value; - if (equals(cfv.getDataType())) { - //the field value if of this type: - return true; - } - return false; + return equals(cfv.getDataType()); } @Override diff --git a/documentgen-test/etc/complex/music4.sd b/documentgen-test/etc/complex/music4.sd index c8100ba7de2..eab0018360d 100644 --- a/documentgen-test/etc/complex/music4.sd +++ b/documentgen-test/etc/complex/music4.sd @@ -4,5 +4,8 @@ search music4 { field mu4 type string { } + field pos type position { + + } } } diff --git a/documentgen-test/src/test/java/com/yahoo/vespa/config/DocumentGenPluginTest.java b/documentgen-test/src/test/java/com/yahoo/vespa/config/DocumentGenPluginTest.java index deec438a332..b6a0f165ca6 100644 --- a/documentgen-test/src/test/java/com/yahoo/vespa/config/DocumentGenPluginTest.java +++ b/documentgen-test/src/test/java/com/yahoo/vespa/config/DocumentGenPluginTest.java @@ -5,21 +5,53 @@ import com.yahoo.compress.CompressionType; import com.yahoo.docproc.DocumentProcessor; import com.yahoo.docproc.Processing; import com.yahoo.docproc.proxy.ProxyDocument; -import com.yahoo.document.*; +import com.yahoo.document.ArrayDataType; +import com.yahoo.document.DataType; +import com.yahoo.document.Document; +import com.yahoo.document.DocumentId; +import com.yahoo.document.DocumentPut; +import com.yahoo.document.DocumentType; +import com.yahoo.document.DocumentTypeManager; +import com.yahoo.document.Field; +import com.yahoo.document.Generated; +import com.yahoo.document.MapDataType; +import com.yahoo.document.ReferenceDataType; +import com.yahoo.document.StructDataType; +import com.yahoo.document.WeightedSetDataType; import com.yahoo.document.annotation.Annotation; import com.yahoo.document.annotation.AnnotationType; import com.yahoo.document.annotation.SpanTree; import com.yahoo.document.config.DocumentmanagerConfig; -import com.yahoo.document.datatypes.*; -import com.yahoo.document.serialization.*; +import com.yahoo.document.datatypes.Array; +import com.yahoo.document.datatypes.DoubleFieldValue; +import com.yahoo.document.datatypes.FieldValue; +import com.yahoo.document.datatypes.FloatFieldValue; +import com.yahoo.document.datatypes.IntegerFieldValue; +import com.yahoo.document.datatypes.LongFieldValue; +import com.yahoo.document.datatypes.MapFieldValue; +import com.yahoo.document.datatypes.Raw; +import com.yahoo.document.datatypes.ReferenceFieldValue; +import com.yahoo.document.datatypes.StringFieldValue; +import com.yahoo.document.datatypes.Struct; +import com.yahoo.document.datatypes.StructuredFieldValue; +import com.yahoo.document.datatypes.WeightedSet; +import com.yahoo.document.serialization.DocumentDeserializerFactory; +import com.yahoo.document.serialization.DocumentSerializer; +import com.yahoo.document.serialization.DocumentSerializerFactory; import com.yahoo.io.GrowableByteBuffer; import com.yahoo.searchdefinition.derived.Deriver; import com.yahoo.tensor.Tensor; import com.yahoo.vespa.document.NodeImpl; import com.yahoo.vespa.document.dom.DocumentImpl; -import com.yahoo.vespa.documentgen.test.*; +import com.yahoo.vespa.documentgen.test.Book; import com.yahoo.vespa.documentgen.test.Book.Ss0; import com.yahoo.vespa.documentgen.test.Book.Ss1; +import com.yahoo.vespa.documentgen.test.Common; +import com.yahoo.vespa.documentgen.test.ConcreteDocumentFactory; +import com.yahoo.vespa.documentgen.test.Music; +import com.yahoo.vespa.documentgen.test.Music3; +import com.yahoo.vespa.documentgen.test.Music4; +import com.yahoo.vespa.documentgen.test.Parent; import com.yahoo.vespa.documentgen.test.annotation.Artist; import com.yahoo.vespa.documentgen.test.annotation.Date; import com.yahoo.vespa.documentgen.test.annotation.Emptyannotation; @@ -32,10 +64,24 @@ import java.lang.Class; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.nio.ByteBuffer; -import java.util.*; - +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertNotSame; import static org.hamcrest.CoreMatchers.instanceOf; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertThat; + /** * Testcases for vespa-documentgen-plugin @@ -675,7 +721,7 @@ public class DocumentGenPluginTest { } if (generated.getAnnotation(com.yahoo.document.Generated.class)==null) return null; Book book = new Book(d.getId()); - for (Iterator<Map.Entry<Field, FieldValue>>i=d.iterator() ; i.hasNext() ; ) { + for (Iterator<Map.Entry<Field, FieldValue>> i = d.iterator(); i.hasNext() ; ) { Map.Entry<Field, FieldValue> e = i.next(); Field f = e.getKey(); FieldValue fv = e.getValue(); @@ -928,5 +974,12 @@ public class DocumentGenPluginTest { book.setVector(Tensor.from("{{x:0}:1.0, {x:1}:2.0, {x:2}:3.0}")); assertEquals("tensor(x{}):{{x:0}:1.0,{x:1}:2.0,{x:2}:3.0}", book.getVector().toString()); } + + @Test + public void testPositionType() { + Music4 book = new Music4(new DocumentId("doc:music4:0")); + book.setPos(new Music4.Position().setX(7).setY(8)); + assertEquals(new Music4.Position().setX(7).setY(8), book.getPos()); + } } diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java index 33ce6e6db04..0612cee040c 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java @@ -155,6 +155,12 @@ public class Flags { "Takes effect at redeployment", APPLICATION_ID); + public static final UnboundBooleanFlag REDIRECT_LEGACY_DNS_NAMES = defineFeatureFlag( + "redirect-legacy-dns", false, + "Redirect legacy DNS names to the main DNS name", + "Takes effect on deployment through controller", + APPLICATION_ID); + /** WARNING: public for testing: All flags should be defined in {@link Flags}. */ public static UnboundBooleanFlag defineFeatureFlag(String flagId, boolean defaultValue, String description, String modificationEffect, FetchVector.Dimension... dimensions) { diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollector.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollector.java index 1e92fbef967..4239d2120cf 100644 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollector.java +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollector.java @@ -16,10 +16,9 @@ import javax.servlet.AsyncListener; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; - import java.io.IOException; -import java.util.HashMap; -import java.util.Map; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.Future; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; @@ -40,19 +39,25 @@ public class HttpResponseStatisticsCollector extends HandlerWrapper implements G GET, PATCH, POST, PUT, DELETE, OPTIONS, HEAD, OTHER } + public enum HttpScheme { + HTTP, HTTPS, OTHER + } + private static final String[] HTTP_RESPONSE_GROUPS = { Metrics.RESPONSES_1XX, Metrics.RESPONSES_2XX, Metrics.RESPONSES_3XX, Metrics.RESPONSES_4XX, Metrics.RESPONSES_5XX, Metrics.RESPONSES_401, Metrics.RESPONSES_403}; private final AtomicLong inFlight = new AtomicLong(); - private final LongAdder statistics[][]; + private final LongAdder statistics[][][]; public HttpResponseStatisticsCollector() { super(); - statistics = new LongAdder[HttpMethod.values().length][]; - for (int method = 0; method < statistics.length; method++) { - statistics[method] = new LongAdder[HTTP_RESPONSE_GROUPS.length]; - for (int group = 0; group < HTTP_RESPONSE_GROUPS.length; group++) { - statistics[method][group] = new LongAdder(); + statistics = new LongAdder[HttpScheme.values().length][HttpMethod.values().length][]; + for (int scheme = 0; scheme < HttpScheme.values().length; ++scheme) { + for (int method = 0; method < HttpMethod.values().length; method++) { + statistics[scheme][method] = new LongAdder[HTTP_RESPONSE_GROUPS.length]; + for (int group = 0; group < HTTP_RESPONSE_GROUPS.length; group++) { + statistics[scheme][method][group] = new LongAdder(); + } } } } @@ -110,10 +115,11 @@ public class HttpResponseStatisticsCollector extends HandlerWrapper implements G private void observeEndOfRequest(Request request, HttpServletResponse flushableResponse) throws IOException { int group = groupIndex(request); if (group >= 0) { + HttpScheme scheme = getScheme(request); HttpMethod method = getMethod(request); - statistics[method.ordinal()][group].increment(); + statistics[scheme.ordinal()][method.ordinal()][group].increment(); if (group == 5 || group == 6) { // if 401/403, also increment 4xx - statistics[method.ordinal()][3].increment(); + statistics[scheme.ordinal()][method.ordinal()][3].increment(); } } @@ -146,6 +152,17 @@ public class HttpResponseStatisticsCollector extends HandlerWrapper implements G } } + private HttpScheme getScheme(Request request) { + switch (request.getScheme()) { + case "http": + return HttpScheme.HTTP; + case "https": + return HttpScheme.HTTPS; + default: + return HttpScheme.OTHER; + } + } + private HttpMethod getMethod(Request request) { switch (request.getMethod()) { case "GET": @@ -167,17 +184,18 @@ public class HttpResponseStatisticsCollector extends HandlerWrapper implements G } } - public Map<String, Map<String, Long>> takeStatisticsByMethod() { - Map<String, Map<String, Long>> ret = new HashMap<>(); - - for (HttpMethod method : HttpMethod.values()) { - int methodIndex = method.ordinal(); - Map<String, Long> methodStats = new HashMap<>(); - ret.put(method.toString(), methodStats); - - for (int group = 0; group < HTTP_RESPONSE_GROUPS.length; group++) { - long value = statistics[methodIndex][group].sumThenReset(); - methodStats.put(HTTP_RESPONSE_GROUPS[group], value); + public List<StatisticsEntry> takeStatistics() { + var ret = new ArrayList<StatisticsEntry>(); + for (HttpScheme scheme : HttpScheme.values()) { + int schemeIndex = scheme.ordinal(); + for (HttpMethod method : HttpMethod.values()) { + int methodIndex = method.ordinal(); + for (int group = 0; group < HTTP_RESPONSE_GROUPS.length; group++) { + long value = statistics[schemeIndex][methodIndex][group].sumThenReset(); + if (value > 0) { + ret.add(new StatisticsEntry(scheme.name().toLowerCase(), method.name(), HTTP_RESPONSE_GROUPS[group], value)); + } + } } } return ret; @@ -216,4 +234,19 @@ public class HttpResponseStatisticsCollector extends HandlerWrapper implements G FutureCallback futureCallback = shutdown.get(); return futureCallback != null && futureCallback.isDone(); } + + public static class StatisticsEntry { + public final String scheme; + public final String method; + public final String name; + public final long value; + + + public StatisticsEntry(String scheme, String method, String name, long value) { + this.scheme = scheme; + this.method = method; + this.name = name; + this.value = value; + } + } } diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscServerConnector.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscServerConnector.java index 6b371473a57..556d80d3772 100644 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscServerConnector.java +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscServerConnector.java @@ -17,6 +17,7 @@ import java.net.SocketException; import java.nio.channels.ServerSocketChannel; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; @@ -28,7 +29,7 @@ class JDiscServerConnector extends ServerConnector { public static final String REQUEST_ATTRIBUTE = JDiscServerConnector.class.getName(); private final static Logger log = Logger.getLogger(JDiscServerConnector.class.getName()); private final Metric.Context metricCtx; - private final Map<String, Metric.Context> requestMetricContextCache = new ConcurrentHashMap<>(); + private final Map<RequestDimensions, Metric.Context> requestMetricContextCache = new ConcurrentHashMap<>(); private final ServerConnectionStatistics statistics; private final boolean tcpKeepAlive; private final boolean tcpNoDelay; @@ -124,9 +125,12 @@ class JDiscServerConnector extends ServerConnector { public Metric.Context getRequestMetricContext(HttpServletRequest request) { String method = request.getMethod(); - return requestMetricContextCache.computeIfAbsent(method, ignored -> { + String scheme = request.getScheme(); + var requestDimensions = new RequestDimensions(method, scheme); + return requestMetricContextCache.computeIfAbsent(requestDimensions, ignored -> { Map<String, Object> dimensions = createConnectorDimensions(listenPort, connectorName); dimensions.put(JettyHttpServer.Metrics.METHOD_DIMENSION, method); + dimensions.put(JettyHttpServer.Metrics.SCHEME_DIMENSION, scheme); return metric.createContext(dimensions); }); } @@ -142,4 +146,27 @@ class JDiscServerConnector extends ServerConnector { return props; } + private static class RequestDimensions { + final String method; + final String scheme; + + RequestDimensions(String method, String scheme) { + this.method = method; + this.scheme = scheme; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RequestDimensions that = (RequestDimensions) o; + return Objects.equals(method, that.method) && Objects.equals(scheme, that.scheme); + } + + @Override + public int hashCode() { + return Objects.hash(method, scheme); + } + } + } diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyHttpServer.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyHttpServer.java index 0dbc5f59f67..30a1b1d885c 100644 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyHttpServer.java +++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyHttpServer.java @@ -8,7 +8,6 @@ import com.yahoo.component.ComponentId; import com.yahoo.component.provider.ComponentRegistry; import com.yahoo.container.logging.AccessLog; import com.yahoo.jdisc.Metric; -import com.yahoo.jdisc.Metric.Context; import com.yahoo.jdisc.application.OsgiFramework; import com.yahoo.jdisc.http.ServerConfig; import com.yahoo.jdisc.http.ServletPathsConfig; @@ -71,6 +70,7 @@ public class JettyHttpServer extends AbstractServerProvider { String NAME_DIMENSION = "serverName"; String PORT_DIMENSION = "serverPort"; String METHOD_DIMENSION = "httpMethod"; + String SCHEME_DIMENSION = "scheme"; String NUM_OPEN_CONNECTIONS = "serverNumOpenConnections"; String NUM_CONNECTIONS_OPEN_MAX = "serverConnectionsOpenMax"; @@ -357,13 +357,12 @@ public class JettyHttpServer extends AbstractServerProvider { } private void addResponseMetrics(HttpResponseStatisticsCollector statisticsCollector) { - Map<String, Map<String, Long>> statistics = statisticsCollector.takeStatisticsByMethod(); - statistics.forEach((httpMethod, statsByResponseType) -> { + for (var metricEntry : statisticsCollector.takeStatistics()) { Map<String, Object> dimensions = new HashMap<>(); - dimensions.put(Metrics.METHOD_DIMENSION, httpMethod); - Context ctx = metric.createContext(dimensions); - statsByResponseType.forEach((group, value) -> metric.add(group, value, ctx)); - }); + dimensions.put(Metrics.METHOD_DIMENSION, metricEntry.method); + dimensions.put(Metrics.SCHEME_DIMENSION, metricEntry.scheme); + metric.add(metricEntry.name, metricEntry.value, metric.createContext(dimensions)); + } } private void setConnectorMetrics(JDiscServerConnector connector) { diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollectorTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollectorTest.java index 3c23a2b0937..df2308f6dd0 100644 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollectorTest.java +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollectorTest.java @@ -1,6 +1,7 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.jdisc.http.server.jetty; +import com.yahoo.jdisc.http.server.jetty.HttpResponseStatisticsCollector.StatisticsEntry; import com.yahoo.jdisc.http.server.jetty.JettyHttpServer.Metrics; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpURI; @@ -22,10 +23,9 @@ import org.testng.annotations.Test; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; - import java.io.IOException; import java.nio.ByteBuffer; -import java.util.Map; +import java.util.List; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -40,55 +40,62 @@ public class HttpResponseStatisticsCollectorTest { @Test public void statistics_are_aggregated_by_category() throws Exception { - testRequest(300, "GET"); - testRequest(301, "GET"); - testRequest(200, "GET"); + testRequest("http", 300, "GET"); + testRequest("http", 301, "GET"); + testRequest("http", 200, "GET"); - Map<String, Map<String, Long>> stats = collector.takeStatisticsByMethod(); - assertThat(stats.get("GET").get(Metrics.RESPONSES_2XX), equalTo(1L)); - assertThat(stats.get("GET").get(Metrics.RESPONSES_3XX), equalTo(2L)); + var stats = collector.takeStatistics(); + assertStatisticsEntryPresent(stats, "http", "GET", Metrics.RESPONSES_2XX, 1L); + assertStatisticsEntryPresent(stats, "http", "GET", Metrics.RESPONSES_3XX, 2L); } @Test - public void statistics_are_grouped_by_http_method() throws Exception { - testRequest(200, "GET"); - testRequest(200, "PUT"); - testRequest(200, "POST"); - testRequest(200, "POST"); - testRequest(404, "GET"); - - Map<String, Map<String, Long>> stats = collector.takeStatisticsByMethod(); - assertThat(stats.get("GET").get(Metrics.RESPONSES_2XX), equalTo(1L)); - assertThat(stats.get("GET").get(Metrics.RESPONSES_4XX), equalTo(1L)); - assertThat(stats.get("PUT").get(Metrics.RESPONSES_2XX), equalTo(1L)); - assertThat(stats.get("POST").get(Metrics.RESPONSES_2XX), equalTo(2L)); + public void statistics_are_grouped_by_http_method_and_scheme() throws Exception { + testRequest("http", 200, "GET"); + testRequest("http", 200, "PUT"); + testRequest("http", 200, "POST"); + testRequest("http", 200, "POST"); + testRequest("http", 404, "GET"); + testRequest("https", 404, "GET"); + testRequest("https", 200, "POST"); + testRequest("https", 200, "POST"); + testRequest("https", 200, "POST"); + testRequest("https", 200, "POST"); + + var stats = collector.takeStatistics(); + assertStatisticsEntryPresent(stats, "http", "GET", Metrics.RESPONSES_2XX, 1L); + assertStatisticsEntryPresent(stats, "http", "GET", Metrics.RESPONSES_4XX, 1L); + assertStatisticsEntryPresent(stats, "http", "PUT", Metrics.RESPONSES_2XX, 1L); + assertStatisticsEntryPresent(stats, "http", "POST", Metrics.RESPONSES_2XX, 2L); + assertStatisticsEntryPresent(stats, "https", "GET", Metrics.RESPONSES_4XX, 1L); + assertStatisticsEntryPresent(stats, "https", "POST", Metrics.RESPONSES_2XX, 4L); } @Test public void statistics_include_grouped_and_single_statuscodes() throws Exception { - testRequest(401, "GET"); - testRequest(404, "GET"); - testRequest(403, "GET"); + testRequest("http", 401, "GET"); + testRequest("http", 404, "GET"); + testRequest("http", 403, "GET"); - Map<String, Map<String, Long>> stats = collector.takeStatisticsByMethod(); - assertThat(stats.get("GET").get(Metrics.RESPONSES_4XX), equalTo(3L)); - assertThat(stats.get("GET").get(Metrics.RESPONSES_401), equalTo(1L)); - assertThat(stats.get("GET").get(Metrics.RESPONSES_403), equalTo(1L)); + var stats = collector.takeStatistics(); + assertStatisticsEntryPresent(stats, "http", "GET", Metrics.RESPONSES_4XX, 3L); + assertStatisticsEntryPresent(stats, "http", "GET", Metrics.RESPONSES_401, 1L); + assertStatisticsEntryPresent(stats, "http", "GET", Metrics.RESPONSES_403, 1L); } @Test public void retrieving_statistics_resets_the_counters() throws Exception { - testRequest(200, "GET"); - testRequest(200, "GET"); + testRequest("http", 200, "GET"); + testRequest("http", 200, "GET"); - Map<String, Map<String, Long>> stats = collector.takeStatisticsByMethod(); - assertThat(stats.get("GET").get(Metrics.RESPONSES_2XX), equalTo(2L)); + var stats = collector.takeStatistics(); + assertStatisticsEntryPresent(stats, "http", "GET", Metrics.RESPONSES_2XX, 2L); - testRequest(200, "GET"); + testRequest("http", 200, "GET"); - stats = collector.takeStatisticsByMethod(); - assertThat(stats.get("GET").get(Metrics.RESPONSES_2XX), equalTo(1L)); + stats = collector.takeStatistics(); + assertStatisticsEntryPresent(stats, "http", "GET", Metrics.RESPONSES_2XX, 1L); } @BeforeTest @@ -116,9 +123,9 @@ public class HttpResponseStatisticsCollectorTest { server.start(); } - private Request testRequest(int responseCode, String httpMethod) throws Exception { + private Request testRequest(String scheme, int responseCode, String httpMethod) throws Exception { HttpChannel channel = new HttpChannel(connector, new HttpConfiguration(), null, new DummyTransport()); - MetaData.Request metaData = new MetaData.Request(httpMethod, new HttpURI("http://foo/bar"), HttpVersion.HTTP_1_1, new HttpFields()); + MetaData.Request metaData = new MetaData.Request(httpMethod, new HttpURI(scheme + "://foo/bar"), HttpVersion.HTTP_1_1, new HttpFields()); Request req = channel.getRequest(); req.setMetaData(metaData); @@ -127,6 +134,15 @@ public class HttpResponseStatisticsCollectorTest { return req; } + private static void assertStatisticsEntryPresent(List<StatisticsEntry> result, String scheme, String method, String name, long expectedValue) { + long value = result.stream() + .filter(entry -> entry.method.equals(method) && entry.scheme.equals(scheme) && entry.name.equals(name)) + .mapToLong(entry -> entry.value) + .findAny() + .orElseThrow(() -> new AssertionError(String.format("Not matching entry in result (scheme=%s, method=%s, name=%s)", scheme, method, name))); + assertThat(value, equalTo(expectedValue)); + } + private final class DummyTransport implements HttpTransport { @Override public void send(Response info, boolean head, ByteBuffer content, boolean lastContent, Callback callback) { diff --git a/jrt/pom.xml b/jrt/pom.xml index 5208c0417cc..e9383654e30 100644 --- a/jrt/pom.xml +++ b/jrt/pom.xml @@ -34,6 +34,16 @@ <artifactId>security-utils</artifactId> <version>${project.version}</version> <scope>compile</scope> + <exclusions> + <exclusion> <!-- not needed --> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient</artifactId> + </exclusion> + <exclusion> <!-- not needed --> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpcore</artifactId> + </exclusion> + </exclusions> </dependency> <dependency> <!-- required due to bug in maven dependency resolving - bouncycastle is compile scope in security-utils, yet it is not part of test scope here --> <groupId>org.bouncycastle</groupId> diff --git a/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java b/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java index f5f9182fc4e..2ea1e1efe83 100644 --- a/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java +++ b/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java @@ -1,6 +1,9 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.security.tls; +import com.yahoo.security.tls.https.TlsAwareHttpClientBuilder; + +import java.net.http.HttpClient; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; @@ -48,6 +51,12 @@ public class TransportSecurityUtils { .map(configFile -> new ReloadingTlsContext(configFile, getInsecureAuthorizationMode())); } + public static HttpClient.Builder createHttpClientBuilder(String userAgent) { + return createTlsContext() + .map(tlsContext -> new TlsAwareHttpClientBuilder(tlsContext, userAgent)) + .orElseGet(() -> new TlsAwareHttpClientBuilder(userAgent)); + } + private static Optional<String> getEnvironmentVariable(String environmentVariable) { return Optional.ofNullable(System.getenv(environmentVariable)) .filter(var -> !var.isEmpty()); diff --git a/security-utils/src/main/java/com/yahoo/security/tls/https/TlsAwareHttpClient.java b/security-utils/src/main/java/com/yahoo/security/tls/https/TlsAwareHttpClient.java new file mode 100644 index 00000000000..2911b77707a --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/https/TlsAwareHttpClient.java @@ -0,0 +1,101 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security.tls.https; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import java.io.IOException; +import java.net.Authenticator; +import java.net.CookieHandler; +import java.net.ProxySelector; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +/** + * A {@link HttpClient} that uses either http or https based on the global Vespa TLS configuration. + * + * @author bjorncs + */ +class TlsAwareHttpClient extends HttpClient { + + private final HttpClient wrappedClient; + private final String userAgent; + + TlsAwareHttpClient(HttpClient wrappedClient, String userAgent) { + this.wrappedClient = wrappedClient; + this.userAgent = userAgent; + } + + @Override + public Optional<CookieHandler> cookieHandler() { + return wrappedClient.cookieHandler(); + } + + @Override + public Optional<Duration> connectTimeout() { + return wrappedClient.connectTimeout(); + } + + @Override + public Redirect followRedirects() { + return wrappedClient.followRedirects(); + } + + @Override + public Optional<ProxySelector> proxy() { + return wrappedClient.proxy(); + } + + @Override + public SSLContext sslContext() { + return wrappedClient.sslContext(); + } + + @Override + public SSLParameters sslParameters() { + return wrappedClient.sslParameters(); + } + + @Override + public Optional<Authenticator> authenticator() { + return wrappedClient.authenticator(); + } + + @Override + public Version version() { + return wrappedClient.version(); + } + + @Override + public Optional<Executor> executor() { + return wrappedClient.executor(); + } + + @Override + public <T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) throws IOException, InterruptedException { + return wrappedClient.send(wrapRequest(request), responseBodyHandler); + } + + @Override + public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) { + return wrappedClient.sendAsync(wrapRequest(request), responseBodyHandler); + } + + @Override + public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler, HttpResponse.PushPromiseHandler<T> pushPromiseHandler) { + return wrappedClient.sendAsync(wrapRequest(request), responseBodyHandler, pushPromiseHandler); + } + + @Override + public String toString() { + return wrappedClient.toString(); + } + + private HttpRequest wrapRequest(HttpRequest request) { + return new TlsAwareHttpRequest(request, userAgent); + } +} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/https/TlsAwareHttpClientBuilder.java b/security-utils/src/main/java/com/yahoo/security/tls/https/TlsAwareHttpClientBuilder.java new file mode 100644 index 00000000000..5a375cf663f --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/https/TlsAwareHttpClientBuilder.java @@ -0,0 +1,97 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security.tls.https; + +import com.yahoo.security.tls.TlsContext; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import java.net.Authenticator; +import java.net.CookieHandler; +import java.net.ProxySelector; +import java.net.http.HttpClient; +import java.time.Duration; +import java.util.concurrent.Executor; + +/** + * A client builder for {@link HttpClient} which uses {@link TlsContext} for TLS configuration. + * Intended for internal Vespa communication only. + * + * @author bjorncs + */ +public class TlsAwareHttpClientBuilder implements HttpClient.Builder { + + private final HttpClient.Builder wrappedBuilder; + private final String userAgent; + + public TlsAwareHttpClientBuilder(String userAgent) { + this(null, userAgent); + } + + public TlsAwareHttpClientBuilder(TlsContext tlsContext, String userAgent) { + this.wrappedBuilder = tlsContext != null ? + HttpClient.newBuilder().sslContext(tlsContext.context()).sslParameters(tlsContext.parameters()) : + HttpClient.newBuilder(); + this.userAgent = userAgent; + } + + @Override + public HttpClient.Builder cookieHandler(CookieHandler cookieHandler) { + throw new UnsupportedOperationException(); + } + + @Override + public HttpClient.Builder connectTimeout(Duration duration) { + wrappedBuilder.connectTimeout(duration); + return this; + } + + @Override + public HttpClient.Builder sslContext(SSLContext sslContext) { + throw new UnsupportedOperationException("SSLContext is given from tls context"); + } + + @Override + public HttpClient.Builder sslParameters(SSLParameters sslParameters) { + throw new UnsupportedOperationException("SSLParameters is given from tls context"); + } + + @Override + public HttpClient.Builder executor(Executor executor) { + wrappedBuilder.executor(executor); + return this; + } + + @Override + public HttpClient.Builder followRedirects(HttpClient.Redirect policy) { + wrappedBuilder.followRedirects(policy); + return this; + } + + @Override + public HttpClient.Builder version(HttpClient.Version version) { + wrappedBuilder.version(version); + return this; + } + + @Override + public HttpClient.Builder priority(int priority) { + wrappedBuilder.priority(priority); + return this; + } + + @Override + public HttpClient.Builder proxy(ProxySelector proxySelector) { + throw new UnsupportedOperationException(); + } + + @Override + public HttpClient.Builder authenticator(Authenticator authenticator) { + throw new UnsupportedOperationException(); + } + + @Override + public HttpClient build() { + // TODO Stop wrapping the client once TLS is mandatory + return new TlsAwareHttpClient(wrappedBuilder.build(), userAgent); + } +} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/https/TlsAwareHttpRequest.java b/security-utils/src/main/java/com/yahoo/security/tls/https/TlsAwareHttpRequest.java new file mode 100644 index 00000000000..bbdd8af791f --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/https/TlsAwareHttpRequest.java @@ -0,0 +1,103 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security.tls.https; + +import com.yahoo.security.tls.MixedMode; +import com.yahoo.security.tls.TransportSecurityUtils; + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; + +/** + * A {@link HttpRequest} where the scheme is either http or https based on the global Vespa TLS configuration. + * + * @author bjorncs + */ +class TlsAwareHttpRequest extends HttpRequest { + + private final URI rewrittenUri; + private final HttpRequest wrappedRequest; + private final HttpHeaders rewrittenHeaders; + + TlsAwareHttpRequest(HttpRequest wrappedRequest, String userAgent) { + this.wrappedRequest = wrappedRequest; + this.rewrittenUri = rewriteUri(wrappedRequest.uri()); + this.rewrittenHeaders = rewriteHeaders(wrappedRequest, userAgent); + } + + @Override + public Optional<BodyPublisher> bodyPublisher() { + return wrappedRequest.bodyPublisher(); + } + + @Override + public String method() { + return wrappedRequest.method(); + } + + @Override + public Optional<Duration> timeout() { + return wrappedRequest.timeout(); + } + + @Override + public boolean expectContinue() { + return wrappedRequest.expectContinue(); + } + + @Override + public URI uri() { + return rewrittenUri; + } + + @Override + public Optional<HttpClient.Version> version() { + return wrappedRequest.version(); + } + + @Override + public HttpHeaders headers() { + return rewrittenHeaders; + } + + private static URI rewriteUri(URI uri) { + if (!uri.getScheme().equals("http")) { + return uri; + } + String rewrittenScheme = + TransportSecurityUtils.getConfigFile().isPresent() && TransportSecurityUtils.getInsecureMixedMode() != MixedMode.PLAINTEXT_CLIENT_MIXED_SERVER ? + "https" : + "http"; + int port = uri.getPort(); + int rewrittenPort = port != -1 ? port : (rewrittenScheme.equals("http") ? 80 : 443); + try { + return new URI(rewrittenScheme, uri.getUserInfo(), uri.getHost(), rewrittenPort, uri.getPath(), uri.getQuery(), uri.getFragment()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + private static HttpHeaders rewriteHeaders(HttpRequest request, String userAgent) { + HttpHeaders headers = request.headers(); + if (headers.firstValue("User-Agent").isPresent()) { + return headers; + } + HashMap<String, List<String>> rewrittenHeaders = new HashMap<>(headers.map()); + rewrittenHeaders.put("User-Agent", List.of(userAgent)); + return HttpHeaders.of(rewrittenHeaders, (ignored1, ignored2) -> true); + } + + @Override + public String toString() { + return "TlsAwareHttpRequest{" + + "rewrittenUri=" + rewrittenUri + + ", wrappedRequest=" + wrappedRequest + + '}'; + } +} diff --git a/vespa-documentgen-plugin/etc/complex/music3.sd b/vespa-documentgen-plugin/etc/complex/music3.sd index 65f37029d04..45ce11fd581 100644 --- a/vespa-documentgen-plugin/etc/complex/music3.sd +++ b/vespa-documentgen-plugin/etc/complex/music3.sd @@ -4,5 +4,8 @@ search music3 { field mu3 type string { } + field pos type position { + + } } } diff --git a/vespa-documentgen-plugin/src/main/java/com/yahoo/vespa/DocumentGenMojo.java b/vespa-documentgen-plugin/src/main/java/com/yahoo/vespa/DocumentGenMojo.java index 7e73d6b5915..bc34a4ac3df 100644 --- a/vespa-documentgen-plugin/src/main/java/com/yahoo/vespa/DocumentGenMojo.java +++ b/vespa-documentgen-plugin/src/main/java/com/yahoo/vespa/DocumentGenMojo.java @@ -3,11 +3,14 @@ package com.yahoo.vespa; import com.yahoo.collections.Pair; import com.yahoo.document.ArrayDataType; +import com.yahoo.document.CollectionDataType; import com.yahoo.document.DataType; import com.yahoo.document.Field; import com.yahoo.document.MapDataType; +import com.yahoo.document.PositionDataType; import com.yahoo.document.ReferenceDataType; import com.yahoo.document.StructDataType; +import com.yahoo.document.StructuredDataType; import com.yahoo.document.TensorDataType; import com.yahoo.document.WeightedSetDataType; import com.yahoo.document.annotation.AnnotationReferenceDataType; @@ -18,7 +21,6 @@ import com.yahoo.searchdefinition.Search; import com.yahoo.searchdefinition.SearchBuilder; import com.yahoo.searchdefinition.parser.ParseException; import org.apache.maven.plugin.AbstractMojo; -import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; @@ -31,6 +33,7 @@ import java.io.FilenameFilter; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.HashMap; @@ -468,6 +471,9 @@ public class DocumentGenMojo extends AbstractMojo { exportHashCode(allUniqueFields, out, 1, "(getDataType() != null ? getDataType().hashCode() : 0) + getId().hashCode()"); exportEquals(className, allUniqueFields, out, 1); Set<DataType> exportedStructs = exportStructTypes(docType.getTypes(), out, 1, null); + if (hasAnyPositionField(allUniqueFields)) { + exportedStructs = exportStructTypes(Arrays.asList(PositionDataType.INSTANCE), out, 1, exportedStructs); + } docTypes.put(docType.getName(), packageName+"."+className); for (DataType exportedStruct : exportedStructs) { structTypes.put(exportedStruct.getName(), packageName+"."+className+"."+className(exportedStruct.getName())); @@ -475,6 +481,25 @@ public class DocumentGenMojo extends AbstractMojo { out.write("}\n"); } + private static boolean hasAnyPostionDataType(DataType dt) { + if (dt instanceof CollectionDataType) { + return hasAnyPostionDataType(((CollectionDataType)dt).getNestedType()); + } else if (dt instanceof StructuredDataType) { + return hasAnyPositionField(((StructuredDataType)dt).getFields()); + } else { + return PositionDataType.INSTANCE.equals(dt); + } + } + + private static boolean hasAnyPositionField(Collection<Field> fields) { + for (Field f : fields) { + if (hasAnyPostionDataType(f.getDataType())) { + return true; + } + } + return true; + } + private Collection<Field> getAllUniqueFields(Boolean multipleInheritance, Collection<Field> allFields) { if (multipleInheritance) { Map<String, Field> seen = new HashMap<>(); @@ -732,7 +757,8 @@ public class DocumentGenMojo extends AbstractMojo { ind(ind)+" * Input struct type: "+structType.getName()+"\n" + ind(ind)+" * Date: "+new Date()+"\n" + ind(ind)+" */\n" + - ind(ind)+"@com.yahoo.document.Generated public static class "+structClassName+" extends com.yahoo.document.datatypes.Struct {\n\n" + + ind(ind)+"@com.yahoo.document.Generated\n" + + ind(ind) + "public static class "+structClassName+" extends com.yahoo.document.datatypes.Struct {\n\n" + ind(ind+1)+"/** The type of this.*/\n" + ind(ind+1)+"public static final com.yahoo.document.StructDataType type = getStructType();\n\n"); out.write(ind(ind+1)+"public "+structClassName+"() {\n" + diff --git a/vespa-documentgen-plugin/src/test/java/com/yahoo/vespa/DocumentGenTest.java b/vespa-documentgen-plugin/src/test/java/com/yahoo/vespa/DocumentGenTest.java index b21f38c586a..c195e116bf0 100644 --- a/vespa-documentgen-plugin/src/test/java/com/yahoo/vespa/DocumentGenTest.java +++ b/vespa-documentgen-plugin/src/test/java/com/yahoo/vespa/DocumentGenTest.java @@ -5,8 +5,6 @@ import com.yahoo.document.DataType; import com.yahoo.document.StructDataType; import com.yahoo.document.WeightedSetDataType; import com.yahoo.searchdefinition.Search; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.MojoFailureException; import org.junit.Test; import java.io.File; @@ -19,7 +17,7 @@ import static org.junit.Assert.fail; public class DocumentGenTest { @Test - public void testMusic() throws MojoExecutionException, MojoFailureException { + public void testMusic() { DocumentGenMojo mojo = new DocumentGenMojo(); mojo.execute(new File("etc/music/"), new File("target/generated-test-sources/vespa-documentgen-plugin/"), "com.yahoo.vespa.document"); Map<String, Search> searches = mojo.getSearches(); @@ -28,19 +26,21 @@ public class DocumentGenTest { } @Test - public void testComplex() throws MojoFailureException { + public void testComplex() { DocumentGenMojo mojo = new DocumentGenMojo(); mojo.execute(new File("etc/complex/"), new File("target/generated-test-sources/vespa-documentgen-plugin/"), "com.yahoo.vespa.document"); Map<String, Search> searches = mojo.getSearches(); assertEquals(searches.get("video").getDocument("video").getField("weight").getDataType(), DataType.FLOAT); assertEquals(searches.get("book").getDocument("book").getField("sw1").getDataType(), DataType.FLOAT); + assertTrue(searches.get("music3").getDocument("music3").getField("pos").getDataType() instanceof StructDataType); + assertEquals(searches.get("music3").getDocument("music3").getField("pos").getDataType().getName(), "position"); assertTrue(searches.get("book").getDocument("book").getField("mystruct").getDataType() instanceof StructDataType); assertTrue(searches.get("book").getDocument("book").getField("mywsfloat").getDataType() instanceof WeightedSetDataType); assertTrue(((WeightedSetDataType)(searches.get("book").getDocument("book").getField("mywsfloat").getDataType())).getNestedType() == DataType.FLOAT); } @Test - public void testLocalApp() throws MojoFailureException { + public void testLocalApp() { DocumentGenMojo mojo = new DocumentGenMojo(); mojo.execute(new File("etc/localapp/"), new File("target/generated-test-sources/vespa-documentgen-plugin/"), "com.yahoo.vespa.document"); Map<String, Search> searches = mojo.getSearches(); @@ -51,7 +51,7 @@ public class DocumentGenTest { } @Test - public void testEmptyPkgNameForbidden() throws MojoFailureException { + public void testEmptyPkgNameForbidden() { DocumentGenMojo mojo = new DocumentGenMojo(); try { mojo.execute(new File("etc/localapp/"), new File("target/generated-test-sources/vespa-documentgen-plugin/"), ""); |