summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/application/TenantApplications.java74
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployment.java3
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSession.java12
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactory.java8
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactoryImpl.java1
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/RoleId.java116
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserId.java40
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserManagement.java33
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Action.java (renamed from controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Action.java)2
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/ApplicationRole.java29
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Context.java (renamed from controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Context.java)2
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java (renamed from controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/PathGroup.java)2
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java (renamed from controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Policy.java)13
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Privilege.java (renamed from controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Privilege.java)2
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Role.java48
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java (renamed from controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Role.java)54
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Roles.java96
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/SecurityContext.java51
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/TenantRole.java25
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/UnboundRole.java21
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/package-info.java5
-rw-r--r--controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/user/RoleIdTest.java74
-rw-r--r--controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/role/PathGroupTest.java (renamed from controller-server/src/test/java/com/yahoo/vespa/hosted/controller/role/PathGroupTest.java)3
-rw-r--r--controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/role/RoleTest.java54
-rw-r--r--controller-server/pom.xml7
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java66
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java19
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/GlobalDnsName.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainer.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java115
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleResolver.java118
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java38
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/RoleMembership.java122
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepository.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/TenantSpec.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java75
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java17
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainerTest.java1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java3
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java37
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilterTest.java122
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleResolverTest.java120
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilterTest.java56
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/role/RoleMembershipTest.java86
-rw-r--r--document/src/main/java/com/yahoo/document/CollectionDataType.java7
-rw-r--r--documentgen-test/etc/complex/music4.sd3
-rw-r--r--documentgen-test/src/test/java/com/yahoo/vespa/config/DocumentGenPluginTest.java69
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/Flags.java6
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollector.java77
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscServerConnector.java31
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyHttpServer.java13
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollectorTest.java88
-rw-r--r--jrt/pom.xml10
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java9
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/https/TlsAwareHttpClient.java101
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/https/TlsAwareHttpClientBuilder.java97
-rw-r--r--security-utils/src/main/java/com/yahoo/security/tls/https/TlsAwareHttpRequest.java103
-rw-r--r--vespa-documentgen-plugin/etc/complex/music3.sd3
-rw-r--r--vespa-documentgen-plugin/src/main/java/com/yahoo/vespa/DocumentGenMojo.java30
-rw-r--r--vespa-documentgen-plugin/src/test/java/com/yahoo/vespa/DocumentGenTest.java12
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/"), "");