aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorValerij Fredriksen <freva@users.noreply.github.com>2022-08-12 14:10:18 +0200
committerGitHub <noreply@github.com>2022-08-12 14:10:18 +0200
commit910af64b854aa25689c4c8a2a610c219180f6f6b (patch)
tree27527b1eda37b9d8a5354539c79aa9721dc62ff1
parente1c3e2f3c11360702c378bf85caf61f1258c8c53 (diff)
parente8313cda153f5a4f85f12673b3d1da588940eb38 (diff)
Merge pull request #23643 from vespa-engine/freva/expire-session
Store timestamp on tenant when to expire user session
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserSessionManager.java13
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java11
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java26
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java60
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudUserSessionManager.java50
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDbTest.java3
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotifierTest.java3
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java13
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java6
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java23
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/application-roles.json19
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/security/CloudUserSessionManagerTest.java64
13 files changed, 179 insertions, 117 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserSessionManager.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserSessionManager.java
new file mode 100644
index 00000000000..eae62c66b35
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserSessionManager.java
@@ -0,0 +1,13 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.api.integration.user;
+
+import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
+
+/**
+ * @author freva
+ */
+public interface UserSessionManager {
+
+ /** Returns whether the existing session for the given SecurityContext should be expired */
+ boolean shouldExpireSessionFor(SecurityContext context);
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java
index 54924b9c456..44f9c0ea3b8 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java
@@ -25,17 +25,19 @@ public class CloudTenant extends Tenant {
private final TenantInfo info;
private final List<TenantSecretStore> tenantSecretStores;
private final ArchiveAccess archiveAccess;
+ private final Optional<Instant> invalidateUserSessionsBefore;
/** Public for the serialization layer — do not use! */
public CloudTenant(TenantName name, Instant createdAt, LastLoginInfo lastLoginInfo, Optional<Principal> creator,
BiMap<PublicKey, Principal> developerKeys, TenantInfo info,
- List<TenantSecretStore> tenantSecretStores, ArchiveAccess archiveAccess) {
+ List<TenantSecretStore> tenantSecretStores, ArchiveAccess archiveAccess, Optional<Instant> invalidateUserSessionsBefore) {
super(name, createdAt, lastLoginInfo, Optional.empty());
this.creator = creator;
this.developerKeys = developerKeys;
this.info = Objects.requireNonNull(info);
this.tenantSecretStores = tenantSecretStores;
this.archiveAccess = Objects.requireNonNull(archiveAccess);
+ this.invalidateUserSessionsBefore = invalidateUserSessionsBefore;
}
/** Creates a tenant with the given name, provided it passes validation. */
@@ -44,7 +46,7 @@ public class CloudTenant extends Tenant {
createdAt,
LastLoginInfo.EMPTY,
Optional.ofNullable(creator),
- ImmutableBiMap.of(), TenantInfo.empty(), List.of(), new ArchiveAccess());
+ ImmutableBiMap.of(), TenantInfo.empty(), List.of(), new ArchiveAccess(), Optional.empty());
}
/** The user that created the tenant */
@@ -75,6 +77,11 @@ public class CloudTenant extends Tenant {
return archiveAccess;
}
+ /** Returns instant before which all user sessions that have access to this tenant must be refreshed */
+ public Optional<Instant> invalidateUserSessionsBefore() {
+ return invalidateUserSessionsBefore;
+ }
+
@Override
public Type type() {
return Type.cloud;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java
index 4f58e87035b..ac7c6319c1b 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java
@@ -129,25 +129,27 @@ public abstract class LockedTenant {
private final TenantInfo info;
private final List<TenantSecretStore> tenantSecretStores;
private final ArchiveAccess archiveAccess;
+ private final Optional<Instant> invalidateUserSessionsBefore;
private Cloud(TenantName name, Instant createdAt, LastLoginInfo lastLoginInfo, Optional<Principal> creator,
BiMap<PublicKey, Principal> developerKeys, TenantInfo info,
- List<TenantSecretStore> tenantSecretStores, ArchiveAccess archiveAccess) {
+ List<TenantSecretStore> tenantSecretStores, ArchiveAccess archiveAccess, Optional<Instant> invalidateUserSessionsBefore) {
super(name, createdAt, lastLoginInfo);
this.developerKeys = ImmutableBiMap.copyOf(developerKeys);
this.creator = creator;
this.info = info;
this.tenantSecretStores = tenantSecretStores;
this.archiveAccess = archiveAccess;
+ this.invalidateUserSessionsBefore = invalidateUserSessionsBefore;
}
private Cloud(CloudTenant tenant) {
- this(tenant.name(), tenant.createdAt(), tenant.lastLoginInfo(), tenant.creator(), tenant.developerKeys(), tenant.info(), tenant.tenantSecretStores(), tenant.archiveAccess());
+ this(tenant.name(), tenant.createdAt(), tenant.lastLoginInfo(), tenant.creator(), tenant.developerKeys(), tenant.info(), tenant.tenantSecretStores(), tenant.archiveAccess(), tenant.invalidateUserSessionsBefore());
}
@Override
public CloudTenant get() {
- return new CloudTenant(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess);
+ return new CloudTenant(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess, invalidateUserSessionsBefore);
}
public Cloud withDeveloperKey(PublicKey key, Principal principal) {
@@ -155,38 +157,42 @@ public abstract class LockedTenant {
if (keys.containsKey(key))
throw new IllegalArgumentException("Key " + KeyUtils.toPem(key) + " is already owned by " + keys.get(key));
keys.put(key, principal);
- return new Cloud(name, createdAt, lastLoginInfo, creator, keys, info, tenantSecretStores, archiveAccess);
+ return new Cloud(name, createdAt, lastLoginInfo, creator, keys, info, tenantSecretStores, archiveAccess, invalidateUserSessionsBefore);
}
public Cloud withoutDeveloperKey(PublicKey key) {
BiMap<PublicKey, Principal> keys = HashBiMap.create(developerKeys);
keys.remove(key);
- return new Cloud(name, createdAt, lastLoginInfo, creator, keys, info, tenantSecretStores, archiveAccess);
+ return new Cloud(name, createdAt, lastLoginInfo, creator, keys, info, tenantSecretStores, archiveAccess, invalidateUserSessionsBefore);
}
public Cloud withInfo(TenantInfo newInfo) {
- return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, newInfo, tenantSecretStores, archiveAccess);
+ return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, newInfo, tenantSecretStores, archiveAccess, invalidateUserSessionsBefore);
}
@Override
public LockedTenant with(LastLoginInfo lastLoginInfo) {
- return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess);
+ return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess, invalidateUserSessionsBefore);
}
public Cloud withSecretStore(TenantSecretStore tenantSecretStore) {
ArrayList<TenantSecretStore> secretStores = new ArrayList<>(tenantSecretStores);
secretStores.add(tenantSecretStore);
- return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, secretStores, archiveAccess);
+ return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, secretStores, archiveAccess, invalidateUserSessionsBefore);
}
public Cloud withoutSecretStore(TenantSecretStore tenantSecretStore) {
ArrayList<TenantSecretStore> secretStores = new ArrayList<>(tenantSecretStores);
secretStores.remove(tenantSecretStore);
- return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, secretStores, archiveAccess);
+ return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, secretStores, archiveAccess, invalidateUserSessionsBefore);
}
public Cloud withArchiveAccess(ArchiveAccess archiveAccess) {
- return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess);
+ return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess, invalidateUserSessionsBefore);
+ }
+
+ public Cloud withInvalidateUserSessionsBefore(Instant invalidateUserSessionsBefore) {
+ return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess, Optional.of(invalidateUserSessionsBefore));
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java
index e7cf0c34511..e91fbe8b1b7 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java
@@ -81,6 +81,7 @@ public class TenantSerializer {
private static final String archiveAccessField = "archiveAccess";
private static final String awsArchiveAccessRoleField = "awsArchiveAccessRole";
private static final String gcpArchiveAccessMemberField = "gcpArchiveAccessMember";
+ private static final String invalidateUserSessionsBeforeField = "invalidateUserSessionsBefore";
private static final String awsIdField = "awsId";
private static final String roleField = "role";
@@ -123,6 +124,7 @@ public class TenantSerializer {
toSlime(tenant.info(), root);
toSlime(tenant.tenantSecretStores(), root);
toSlime(tenant.archiveAccess(), root);
+ tenant.invalidateUserSessionsBefore().ifPresent(instant -> root.setLong(invalidateUserSessionsBeforeField, instant.toEpochMilli()));
}
private void toSlime(ArchiveAccess archiveAccess, Cursor root) {
@@ -187,7 +189,8 @@ public class TenantSerializer {
TenantInfo info = tenantInfoFromSlime(tenantObject.field(tenantInfoField));
List<TenantSecretStore> tenantSecretStores = secretStoresFromSlime(tenantObject.field(secretStoresField));
ArchiveAccess archiveAccess = archiveAccessFromSlime(tenantObject);
- return new CloudTenant(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess);
+ Optional<Instant> invalidateUserSessionsBefore = SlimeUtils.optionalInstant(tenantObject.field(invalidateUserSessionsBeforeField));
+ return new CloudTenant(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess, invalidateUserSessionsBefore);
}
private DeletedTenant deletedTenantFrom(Inspector tenantObject) {
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 e10defb4416..a407e5aa211 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
@@ -111,7 +111,6 @@ public class UserApiHandler extends ThreadedHttpRequestHandler {
private HttpResponse handlePOST(Path path, HttpRequest request) {
if (path.matches("/user/v1/tenant/{tenant}")) return addTenantRoleMember(path.get("tenant"), request);
- if (path.matches("/user/v1/tenant/{tenant}/application/{application}")) return addApplicationRoleMember(path.get("tenant"), path.get("application"), request);
return ErrorResponse.notFoundError(Text.format("No '%s' handler at '%s'", request.getMethod(),
request.getUri().getPath()));
@@ -119,7 +118,6 @@ public class UserApiHandler extends ThreadedHttpRequestHandler {
private HttpResponse handleDELETE(Path path, HttpRequest request) {
if (path.matches("/user/v1/tenant/{tenant}")) return removeTenantRoleMember(path.get("tenant"), request);
- if (path.matches("/user/v1/tenant/{tenant}/application/{application}")) return removeApplicationRoleMember(path.get("tenant"), path.get("application"), request);
return ErrorResponse.notFoundError(Text.format("No '%s' handler at '%s'", request.getMethod(),
request.getUri().getPath()));
@@ -255,21 +253,6 @@ public class UserApiHandler extends ThreadedHttpRequestHandler {
private HttpResponse addTenantRoleMember(String tenantName, HttpRequest request) {
Inspector requestObject = bodyInspector(request);
- if (requestObject.field("roles").valid()) {
- return addMultipleTenantRoleMembers(tenantName, requestObject);
- }
- return addTenantRoleMember(tenantName, requestObject);
- }
-
- private HttpResponse addTenantRoleMember(String tenantName, Inspector requestObject) {
- String roleName = require("roleName", Inspector::asString, requestObject);
- UserId user = new UserId(require("user", Inspector::asString, requestObject));
- Role role = Roles.toRole(TenantName.from(tenantName), roleName);
- users.addUsers(role, List.of(user));
- return new MessageResponse(user + " is now a member of " + role);
- }
-
- private HttpResponse addMultipleTenantRoleMembers(String tenantName, Inspector requestObject) {
var tenant = TenantName.from(tenantName);
var user = new UserId(require("user", Inspector::asString, requestObject));
var roles = SlimeStream.fromArray(requestObject.field("roles"), Inspector::asString)
@@ -280,37 +263,8 @@ public class UserApiHandler extends ThreadedHttpRequestHandler {
return new MessageResponse(user + " is now a member of " + roles.stream().map(Role::toString).collect(Collectors.joining(", ")));
}
- private HttpResponse addApplicationRoleMember(String tenantName, String applicationName, HttpRequest request) {
- Inspector requestObject = bodyInspector(request);
- String roleName = require("roleName", Inspector::asString, requestObject);
- UserId user = new UserId(require("user", Inspector::asString, requestObject));
- Role role = Roles.toRole(TenantName.from(tenantName), ApplicationName.from(applicationName), roleName);
- users.addUsers(role, List.of(user));
- return new MessageResponse(user + " is now a member of " + role);
- }
-
private HttpResponse removeTenantRoleMember(String tenantName, HttpRequest request) {
Inspector requestObject = bodyInspector(request);
- if (requestObject.field("roles").valid()) {
- return removeMultipleTenantRoleMembers(tenantName, requestObject);
- }
- return removeTenantRoleMember(tenantName, requestObject);
- }
-
- private HttpResponse removeTenantRoleMember(String tenantName, Inspector requestObject) {
- TenantName tenant = TenantName.from(tenantName);
- String roleName = require("roleName", Inspector::asString, requestObject);
- UserId user = new UserId(require("user", Inspector::asString, requestObject));
- List<Role> roles = Collections.singletonList(Roles.toRole(tenant, roleName));
-
- enforceLastAdminOfTenant(tenant, user, roles);
- removeDeveloperKey(tenant, user, roles);
- users.removeFromRoles(user, roles);
-
- return new MessageResponse(user + " is no longer a member of " + roles.stream().map(Role::toString).collect(Collectors.joining(", ")));
- }
-
- private HttpResponse removeMultipleTenantRoleMembers(String tenantName, Inspector requestObject) {
var tenant = TenantName.from(tenantName);
var user = new UserId(require("user", Inspector::asString, requestObject));
var roles = SlimeStream.fromArray(requestObject.field("roles"), Inspector::asString)
@@ -321,6 +275,11 @@ public class UserApiHandler extends ThreadedHttpRequestHandler {
removeDeveloperKey(tenant, user, roles);
users.removeFromRoles(user, roles);
+ controller.tenants().lockIfPresent(tenant, LockedTenant.class, lockedTenant -> {
+ if (lockedTenant instanceof LockedTenant.Cloud cloudTenant)
+ controller.tenants().store(cloudTenant.withInvalidateUserSessionsBefore(controller.clock().instant()));
+ });
+
return new MessageResponse(user + " is no longer a member of " + roles.stream().map(Role::toString).collect(Collectors.joining(", ")));
}
@@ -348,15 +307,6 @@ public class UserApiHandler extends ThreadedHttpRequestHandler {
}
}
- private HttpResponse removeApplicationRoleMember(String tenantName, String applicationName, HttpRequest request) {
- Inspector requestObject = bodyInspector(request);
- String roleName = require("roleName", Inspector::asString, requestObject);
- UserId user = new UserId(require("user", Inspector::asString, requestObject));
- Role role = Roles.toRole(TenantName.from(tenantName), ApplicationName.from(applicationName), roleName);
- users.removeUsers(role, List.of(user));
- return new MessageResponse(user + " is no longer a member of " + role);
- }
-
private boolean hasTrialCapacity() {
if (! controller.system().isPublic()) return true;
var existing = controller.tenants().asList().stream().map(Tenant::name).collect(Collectors.toList());
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudUserSessionManager.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudUserSessionManager.java
new file mode 100644
index 00000000000..e2b5083abae
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudUserSessionManager.java
@@ -0,0 +1,50 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.security;
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.flags.LongFlag;
+import com.yahoo.vespa.flags.PermanentFlags;
+import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.TenantController;
+import com.yahoo.vespa.hosted.controller.api.integration.user.UserSessionManager;
+import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
+import com.yahoo.vespa.hosted.controller.api.role.TenantRole;
+import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
+
+import java.time.Instant;
+
+/**
+ * @author freva
+ */
+public class CloudUserSessionManager implements UserSessionManager {
+
+ private final TenantController tenantController;
+ private final LongFlag invalidateConsoleSessions;
+
+ public CloudUserSessionManager(Controller controller) {
+ this.tenantController = controller.tenants();
+ this.invalidateConsoleSessions = PermanentFlags.INVALIDATE_CONSOLE_SESSIONS.bindTo(controller.flagSource());
+ }
+
+ @Override
+ public boolean shouldExpireSessionFor(SecurityContext context) {
+ if (context.issuedAt().isBefore(Instant.ofEpochSecond(invalidateConsoleSessions.value())))
+ return true;
+
+ return context.roles().stream()
+ .filter(TenantRole.class::isInstance)
+ .map(TenantRole.class::cast)
+ .map(TenantRole::tenant)
+ .distinct()
+ .anyMatch(tenantName -> shouldExpireSessionFor(tenantName, context.issuedAt()));
+ }
+
+ private boolean shouldExpireSessionFor(TenantName tenantName, Instant contextIssuedAt) {
+ return tenantController.get(tenantName)
+ .filter(CloudTenant.class::isInstance)
+ .map(CloudTenant.class::cast)
+ .flatMap(CloudTenant::invalidateUserSessionsBefore)
+ .map(contextIssuedAt::isBefore)
+ .orElse(false);
+ }
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDbTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDbTest.java
index 1a171341ee0..4c1344650f8 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDbTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDbTest.java
@@ -61,7 +61,8 @@ public class NotificationsDbTest {
List.of(TenantContacts.Audience.NOTIFICATIONS),
email)))),
List.of(),
- new ArchiveAccess());
+ new ArchiveAccess(),
+ Optional.empty());
private static final List<Notification> notifications = List.of(
notification(1001, Type.deployment, Level.error, NotificationSource.from(tenant), "tenant msg"),
notification(1101, Type.applicationPackage, Level.warning, NotificationSource.from(TenantAndApplicationId.from(tenant.value(), "app1")), "app msg"),
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotifierTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotifierTest.java
index 00d8c100ced..7c07192d633 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotifierTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotifierTest.java
@@ -41,7 +41,8 @@ public class NotifierTest {
List.of(TenantContacts.Audience.NOTIFICATIONS),
email)))),
List.of(),
- new ArchiveAccess());
+ new ArchiveAccess(),
+ Optional.empty());
MockCuratorDb curatorDb = new MockCuratorDb(SystemName.Public);
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java
index e29d4afabff..636620acf07 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java
@@ -103,8 +103,8 @@ public class TenantSerializerTest {
otherPublicKey, new SimplePrincipal("jane")),
TenantInfo.empty(),
List.of(),
- new ArchiveAccess()
- );
+ new ArchiveAccess(),
+ Optional.empty());
CloudTenant serialized = (CloudTenant) serializer.tenantFrom(serializer.toSlime(tenant));
assertEquals(tenant.name(), serialized.name());
assertEquals(tenant.creator(), serialized.creator());
@@ -125,11 +125,12 @@ public class TenantSerializerTest {
new TenantSecretStore("ss1", "123", "role1"),
new TenantSecretStore("ss2", "124", "role2")
),
- new ArchiveAccess().withAWSRole("arn:aws:iam::123456789012:role/my-role")
- );
+ new ArchiveAccess().withAWSRole("arn:aws:iam::123456789012:role/my-role"),
+ Optional.of(Instant.ofEpochMilli(1234567)));
CloudTenant serialized = (CloudTenant) serializer.tenantFrom(serializer.toSlime(tenant));
assertEquals(tenant.info(), serialized.info());
assertEquals(tenant.tenantSecretStores(), serialized.tenantSecretStores());
+ assertEquals(tenant.invalidateUserSessionsBefore(), serialized.invalidateUserSessionsBefore());
}
@Test
@@ -174,8 +175,8 @@ public class TenantSerializerTest {
otherPublicKey, new SimplePrincipal("jane")),
TenantInfo.empty(),
List.of(),
- new ArchiveAccess().withAWSRole("arn:aws:iam::123456789012:role/my-role").withGCPMember("user:foo@example.com")
- );
+ new ArchiveAccess().withAWSRole("arn:aws:iam::123456789012:role/my-role").withGCPMember("user:foo@example.com"),
+ Optional.empty());
CloudTenant serialized = (CloudTenant) serializer.tenantFrom(serializer.toSlime(tenant));
assertEquals(serialized.archiveAccess().awsRole().get(), "arn:aws:iam::123456789012:role/my-role");
assertEquals(serialized.archiveAccess().gcpMember().get(), "user:foo@example.com");
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java
index 9ff79213983..ec9be1f04c3 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java
@@ -77,7 +77,8 @@ public class SignatureFilterTest {
ImmutableBiMap.of(),
TenantInfo.empty(),
List.of(),
- new ArchiveAccess()));
+ new ArchiveAccess(),
+ Optional.empty()));
tester.curator().writeApplication(new Application(appId, tester.clock().instant()));
}
@@ -123,7 +124,8 @@ public class SignatureFilterTest {
ImmutableBiMap.of(publicKey, () -> "user"),
TenantInfo.empty(),
List.of(),
- new ArchiveAccess()));
+ new ArchiveAccess(),
+ Optional.empty()));
verifySecurityContext(requestOf(signer.signed(request.copy(), Method.POST, () -> new ByteArrayInputStream(hiBytes)), hiBytes),
new SecurityContext(new SimplePrincipal("user"),
Set.of(Role.reader(id.tenant()),
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
index 3e9f6256134..1344b106bbe 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
@@ -84,7 +84,7 @@ public class UserApiTest extends ControllerContainerCloudTest {
// POST a hosted operator role is not allowed.
tester.assertResponse(request("/user/v1/tenant/my-tenant", POST)
.roles(Set.of(Role.administrator(id.tenant())))
- .data("{\"user\":\"evil@evil\",\"roleName\":\"hostedOperator\"}"),
+ .data("{\"user\":\"evil@evil\",\"roles\":[\"hostedOperator\"]}"),
"{\"error-code\":\"BAD_REQUEST\",\"message\":\"Malformed or illegal role name 'hostedOperator'.\"}", 400);
// POST a tenant developer is available to the tenant owner.
@@ -96,15 +96,9 @@ public class UserApiTest extends ControllerContainerCloudTest {
// POST a tenant admin is not available to a tenant developer.
tester.assertResponse(request("/user/v1/tenant/my-tenant", POST)
.roles(Set.of(Role.developer(id.tenant())))
- .data("{\"user\":\"developer@tenant\",\"roleName\":\"administrator\"}"),
+ .data("{\"user\":\"developer@tenant\",\"roles\":[\"administrator\"]}"),
accessDenied, 403);
- // POST a headless for a non-existent application fails.
- tester.assertResponse(request("/user/v1/tenant/my-tenant/application/my-app", POST)
- .roles(Set.of(Role.administrator(TenantName.from("my-tenant"))))
- .data("{\"user\":\"headless@app\",\"roleName\":\"headless\"}"),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"role 'headless' of 'my-app' owned by 'my-tenant' not found\"}", 400);
-
// POST an application is allowed for a tenant developer.
tester.assertResponse(request("/application/v4/tenant/my-tenant/application/my-app", POST)
.principal("developer@tenant")
@@ -116,22 +110,11 @@ public class UserApiTest extends ControllerContainerCloudTest {
.roles(Set.of(Role.administrator(id.tenant()))),
accessDenied, 403);
- // POST a tenant role is not allowed to an application.
- tester.assertResponse(request("/user/v1/tenant/my-tenant/application/my-app", POST)
- .roles(Set.of(Role.hostedOperator()))
- .data("{\"user\":\"developer@app\",\"roleName\":\"developer\"}"),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Malformed or illegal role name 'developer'.\"}", 400);
-
// GET tenant role information is available to readers.
tester.assertResponse(request("/user/v1/tenant/my-tenant")
.roles(Set.of(Role.reader(id.tenant()))),
new File("tenant-roles.json"));
- // GET application role information is available to tenant administrators.
- tester.assertResponse(request("/user/v1/tenant/my-tenant/application/my-app")
- .roles(Set.of(Role.administrator(id.tenant()))),
- new File("application-roles.json"));
-
// POST a pem deploy key
tester.assertResponse(request("/application/v4/tenant/my-tenant/application/my-app/key", POST)
.roles(Set.of(Role.developer(id.tenant())))
@@ -200,7 +183,7 @@ public class UserApiTest extends ControllerContainerCloudTest {
// DELETE the last tenant owner is not allowed.
tester.assertResponse(request("/user/v1/tenant/my-tenant", DELETE)
.roles(operator)
- .data("{\"user\":\"administrator@tenant\",\"roleName\":\"administrator\"}"),
+ .data("{\"user\":\"administrator@tenant\",\"roles\":[\"administrator\"]}"),
"{\"error-code\":\"BAD_REQUEST\",\"message\":\"Can't remove the last administrator of a tenant.\"}", 400);
// DELETE the tenant is not allowed
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/application-roles.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/application-roles.json
deleted file mode 100644
index 8497358fe40..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/application-roles.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "tenant": "my-tenant",
- "application": "my-app",
- "roleNames": [ ],
- "users": [
- {
- "name": "administrator@tenant",
- "email": "administrator@tenant",
- "verified": false,
- "roles": { }
- },
- {
- "name": "developer@tenant",
- "email": "developer@tenant",
- "verified": false,
- "roles": { }
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/security/CloudUserSessionManagerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/security/CloudUserSessionManagerTest.java
new file mode 100644
index 00000000000..710e75fb235
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/security/CloudUserSessionManagerTest.java
@@ -0,0 +1,64 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.security;
+
+import com.yahoo.config.provision.SystemName;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.flags.InMemoryFlagSource;
+import com.yahoo.vespa.flags.PermanentFlags;
+import com.yahoo.vespa.hosted.controller.ControllerTester;
+import com.yahoo.vespa.hosted.controller.LockedTenant;
+import com.yahoo.vespa.hosted.controller.api.role.Role;
+import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
+import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
+import com.yahoo.vespa.hosted.controller.api.role.TenantRole;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * @author freva
+ */
+class CloudUserSessionManagerTest {
+
+ private final ControllerTester tester = new ControllerTester(SystemName.Public);
+ private final CloudUserSessionManager userSessionManager = new CloudUserSessionManager(tester.controller());
+
+ @Test
+ void test() {
+ createTenant("tenant1", null);
+ createTenant("tenant2", 1234);
+ createTenant("tenant3", 1543);
+ createTenant("tenant4", 2313);
+
+ assertShouldExpire(false, 123);
+ assertShouldExpire(false, 123, "tenant1");
+ assertShouldExpire(true, 123, "tenant2");
+ assertShouldExpire(false, 2123, "tenant2");
+ assertShouldExpire(true, 123, "tenant1", "tenant2");
+
+ ((InMemoryFlagSource) tester.controller().flagSource()).withLongFlag(PermanentFlags.INVALIDATE_CONSOLE_SESSIONS.id(), 150);
+ assertShouldExpire(true, 123);
+ assertShouldExpire(true, 123, "tenant1");
+ }
+
+ private void assertShouldExpire(boolean expected, long issuedAtSeconds, String... tenantNames) {
+ Set<Role> roles = Stream.of(tenantNames).map(name -> TenantRole.developer(TenantName.from(name))).collect(Collectors.toSet());
+ SecurityContext context = new SecurityContext(new SimplePrincipal("dev"), roles, Instant.ofEpochSecond(issuedAtSeconds));
+ assertEquals(expected, userSessionManager.shouldExpireSessionFor(context));
+ }
+
+ private void createTenant(String tenantName, Integer invalidateAfterSeconds) {
+ tester.createTenant(tenantName);
+ Optional.ofNullable(invalidateAfterSeconds)
+ .map(Instant::ofEpochSecond)
+ .ifPresent(instant ->
+ tester.controller().tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, tenant ->
+ tester.controller().tenants().store(tenant.withInvalidateUserSessionsBefore(instant))));
+ }
+}