summaryrefslogtreecommitdiffstats
path: root/controller-server
diff options
context:
space:
mode:
authorValerij Fredriksen <freva@users.noreply.github.com>2021-01-21 08:17:24 +0100
committerGitHub <noreply@github.com>2021-01-21 08:17:24 +0100
commita38d2c9c684fa9639803bbc82a7b7129c335a7cb (patch)
treeefe46c175bfda575b7c30611df7b8bc00b2e2161 /controller-server
parent3a456107e830d82ba5895f348016c84c9de5b727 (diff)
parent34d8f6b20233ec6203148690a5a099976481a6c3 (diff)
Merge pull request #16120 from vespa-engine/freva/last-login
Store last login for tenants
Diffstat (limited to 'controller-server')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java47
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java17
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java43
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java8
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java14
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilter.java73
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java8
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java6
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java6
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/LastLoginInfo.java55
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java9
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java22
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java9
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-with-contact-info.json22
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant2.json4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilterTest.java59
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java12
17 files changed, 350 insertions, 64 deletions
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 d27c585050b..e45bda0708e 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
@@ -13,6 +13,7 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
+import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import com.yahoo.vespa.hosted.controller.tenant.TenantInfo;
@@ -33,10 +34,12 @@ public abstract class LockedTenant {
final TenantName name;
final Instant createdAt;
+ final LastLoginInfo lastLoginInfo;
- private LockedTenant(TenantName name, Instant createdAt) {
+ private LockedTenant(TenantName name, Instant createdAt, LastLoginInfo lastLoginInfo) {
this.name = requireNonNull(name);
this.createdAt = requireNonNull(createdAt);
+ this.lastLoginInfo = requireNonNull(lastLoginInfo);
}
static LockedTenant of(Tenant tenant, Lock lock) {
@@ -50,6 +53,8 @@ public abstract class LockedTenant {
/** Returns a read-only copy of this */
public abstract Tenant get();
+ public abstract LockedTenant with(LastLoginInfo lastLoginInfo);
+
@Override
public String toString() {
return "tenant '" + name + "'";
@@ -65,8 +70,8 @@ public abstract class LockedTenant {
private final Optional<Contact> contact;
private Athenz(TenantName name, AthenzDomain domain, Property property, Optional<PropertyId> propertyId,
- Optional<Contact> contact, Instant createdAt) {
- super(name, createdAt);
+ Optional<Contact> contact, Instant createdAt, LastLoginInfo lastLoginInfo) {
+ super(name, createdAt, lastLoginInfo);
this.domain = domain;
this.property = property;
this.propertyId = propertyId;
@@ -74,28 +79,33 @@ public abstract class LockedTenant {
}
private Athenz(AthenzTenant tenant) {
- this(tenant.name(), tenant.domain(), tenant.property(), tenant.propertyId(), tenant.contact(), tenant.createdAt());
+ this(tenant.name(), tenant.domain(), tenant.property(), tenant.propertyId(), tenant.contact(), tenant.createdAt(), tenant.lastLoginInfo());
}
@Override
public AthenzTenant get() {
- return new AthenzTenant(name, domain, property, propertyId, contact, createdAt);
+ return new AthenzTenant(name, domain, property, propertyId, contact, createdAt, lastLoginInfo);
}
public Athenz with(AthenzDomain domain) {
- return new Athenz(name, domain, property, propertyId, contact, createdAt);
+ return new Athenz(name, domain, property, propertyId, contact, createdAt, lastLoginInfo);
}
public Athenz with(Property property) {
- return new Athenz(name, domain, property, propertyId, contact, createdAt);
+ return new Athenz(name, domain, property, propertyId, contact, createdAt, lastLoginInfo);
}
public Athenz with(PropertyId propertyId) {
- return new Athenz(name, domain, property, Optional.of(propertyId), contact, createdAt);
+ return new Athenz(name, domain, property, Optional.of(propertyId), contact, createdAt, lastLoginInfo);
}
public Athenz with(Contact contact) {
- return new Athenz(name, domain, property, propertyId, Optional.of(contact), createdAt);
+ return new Athenz(name, domain, property, propertyId, Optional.of(contact), createdAt, lastLoginInfo);
+ }
+
+ @Override
+ public LockedTenant with(LastLoginInfo lastLoginInfo) {
+ return new Athenz(name, domain, property, propertyId, contact, createdAt, lastLoginInfo);
}
}
@@ -108,20 +118,20 @@ public abstract class LockedTenant {
private final BiMap<PublicKey, Principal> developerKeys;
private final TenantInfo info;
- private Cloud(TenantName name, Instant createdAt, Optional<Principal> creator, BiMap<PublicKey, Principal> developerKeys, TenantInfo info) {
- super(name, createdAt);
+ private Cloud(TenantName name, Instant createdAt, LastLoginInfo lastLoginInfo, Optional<Principal> creator, BiMap<PublicKey, Principal> developerKeys, TenantInfo info) {
+ super(name, createdAt, lastLoginInfo);
this.developerKeys = ImmutableBiMap.copyOf(developerKeys);
this.creator = creator;
this.info = info;
}
private Cloud(CloudTenant tenant) {
- this(tenant.name(), tenant.createdAt(), Optional.empty(), tenant.developerKeys(), tenant.info());
+ this(tenant.name(), tenant.createdAt(), tenant.lastLoginInfo(), Optional.empty(), tenant.developerKeys(), tenant.info());
}
@Override
public CloudTenant get() {
- return new CloudTenant(name, createdAt, creator, developerKeys, info);
+ return new CloudTenant(name, createdAt, lastLoginInfo, creator, developerKeys, info);
}
public Cloud withDeveloperKey(PublicKey key, Principal principal) {
@@ -129,17 +139,22 @@ 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, creator, keys, info);
+ return new Cloud(name, createdAt, lastLoginInfo, creator, keys, info);
}
public Cloud withoutDeveloperKey(PublicKey key) {
BiMap<PublicKey, Principal> keys = HashBiMap.create(developerKeys);
keys.remove(key);
- return new Cloud(name, createdAt, creator, keys, info);
+ return new Cloud(name, createdAt, lastLoginInfo, creator, keys, info);
}
public Cloud withInfo(TenantInfo newInfo) {
- return new Cloud(name, createdAt, creator, developerKeys, newInfo);
+ return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, newInfo);
+ }
+
+ @Override
+ public LockedTenant with(LastLoginInfo lastLoginInfo) {
+ return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info);
}
}
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 ffb1aae7299..4c9cf4f105f 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
@@ -10,6 +10,7 @@ import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.security.AccessControl;
import com.yahoo.vespa.hosted.controller.security.Credentials;
import com.yahoo.vespa.hosted.controller.security.TenantSpec;
+import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import java.time.Duration;
@@ -121,6 +122,22 @@ public class TenantController {
}
}
+ /**
+ * Update last login times for the given tenant at the given user levers with the given instant, but only if the
+ * new instant is later
+ */
+ public void updateLastLogin(TenantName tenantName, List<LastLoginInfo.UserLevel> userLevels, Instant loggedInAt) {
+ try (Lock lock = lock(tenantName)) {
+ Tenant tenant = require(tenantName);
+ LastLoginInfo loginInfo = tenant.lastLoginInfo();
+ for (LastLoginInfo.UserLevel userLevel : userLevels)
+ loginInfo = loginInfo.withLastLoginIfLater(userLevel, loggedInAt);
+
+ if (tenant.lastLoginInfo().equals(loginInfo)) return; // no change
+ curator.writeTenant(LockedTenant.of(tenant, lock).with(loginInfo).get());
+ }
+ }
+
/** Deletes the given tenant. */
public void delete(TenantName tenant, Credentials credentials) {
try (Lock lock = lock(tenant)) {
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 79dbcc23299..3b5b01d16aa 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
@@ -18,6 +18,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
import com.yahoo.vespa.hosted.controller.api.integration.organization.BillingInfo;
import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
+import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import com.yahoo.vespa.hosted.controller.tenant.TenantInfo;
import com.yahoo.vespa.hosted.controller.tenant.TenantInfoAddress;
@@ -28,7 +29,9 @@ import java.security.Principal;
import java.security.PublicKey;
import java.time.Instant;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
@@ -66,6 +69,7 @@ public class TenantSerializer {
private static final String productCodeField = "productCode";
private static final String pemDeveloperKeysField = "pemDeveloperKeys";
private static final String tenantInfoField = "info";
+ private static final String lastLoginInfoField = "lastLoginInfo";
public Slime toSlime(Tenant tenant) {
Slime slime = new Slime();
@@ -73,6 +77,7 @@ public class TenantSerializer {
tenantObject.setString(nameField, tenant.name().value());
tenantObject.setString(typeField, valueOf(tenant.type()));
tenantObject.setLong(createdAtField, tenant.createdAt().toEpochMilli());
+ toSlime(tenant.lastLoginInfo(), tenantObject.setObject(lastLoginInfoField));
switch (tenant.type()) {
case athenz: toSlime((AthenzTenant) tenant, tenantObject); break;
@@ -116,6 +121,13 @@ public class TenantSerializer {
billingInfoObject.setString(productCodeField, billingInfo.productCode());
}
+ private void toSlime(LastLoginInfo lastLoginInfo, Cursor lastLoginInfoObject) {
+ for (LastLoginInfo.UserLevel userLevel: LastLoginInfo.UserLevel.values()) {
+ lastLoginInfo.get(userLevel).ifPresent(lastLoginAt ->
+ lastLoginInfoObject.setLong(valueOf(userLevel), lastLoginAt.toEpochMilli()));
+ }
+ }
+
public Tenant tenantFrom(Slime slime, Supplier<Instant> tenantCreateTimeSupplier) {
Inspector tenantObject = slime.get();
Tenant.Type type = typeOf(tenantObject.field(typeField).asString());
@@ -134,16 +146,18 @@ public class TenantSerializer {
Optional<PropertyId> propertyId = SlimeUtils.optionalString(tenantObject.field(propertyIdField)).map(PropertyId::new);
Optional<Contact> contact = contactFrom(tenantObject.field(contactField));
Instant createdAt = SlimeUtils.optionalLong(tenantObject.field(createdAtField)).map(Instant::ofEpochMilli).orElseGet(tenantCreateTimeSupplier);
- return new AthenzTenant(name, domain, property, propertyId, contact, createdAt);
+ LastLoginInfo lastLoginInfo = lastLoginInfoFromSlime(tenantObject.field(lastLoginInfoField));
+ return new AthenzTenant(name, domain, property, propertyId, contact, createdAt, lastLoginInfo);
}
private CloudTenant cloudTenantFrom(Inspector tenantObject, Supplier<Instant> tenantCreateTimeSupplier) {
TenantName name = TenantName.from(tenantObject.field(nameField).asString());
Instant createdAt = SlimeUtils.optionalLong(tenantObject.field(createdAtField)).map(Instant::ofEpochMilli).orElseGet(tenantCreateTimeSupplier);
+ LastLoginInfo lastLoginInfo = lastLoginInfoFromSlime(tenantObject.field(lastLoginInfoField));
Optional<Principal> creator = SlimeUtils.optionalString(tenantObject.field(creatorField)).map(SimplePrincipal::new);
BiMap<PublicKey, Principal> developerKeys = developerKeysFromSlime(tenantObject.field(pemDeveloperKeysField));
TenantInfo info = tenantInfoFromSlime(tenantObject.field(tenantInfoField));
- return new CloudTenant(name, createdAt, creator, developerKeys, info);
+ return new CloudTenant(name, createdAt, lastLoginInfo, creator, developerKeys, info);
}
private BiMap<PublicKey, Principal> developerKeysFromSlime(Inspector array) {
@@ -186,6 +200,13 @@ public class TenantSerializer {
.withAddress(tenantInfoAddressFromSlime(billingObject.field("address")));
}
+ private LastLoginInfo lastLoginInfoFromSlime(Inspector lastLoginInfoObject) {
+ Map<LastLoginInfo.UserLevel, Instant> lastLoginByUserLevel = new HashMap<>();
+ lastLoginInfoObject.traverse((String name, Inspector value) ->
+ lastLoginByUserLevel.put(userLevelOf(name), Instant.ofEpochMilli(value.asLong())));
+ return new LastLoginInfo(lastLoginByUserLevel);
+ }
+
void toSlime(TenantInfo info, Cursor parentCursor) {
if (info.isEmpty()) return;
Cursor infoCursor = parentCursor.setObject("info");
@@ -283,4 +304,22 @@ public class TenantSerializer {
default: throw new IllegalArgumentException("Unexpected tenant type '" + type + "'.");
}
}
+
+ private static LastLoginInfo.UserLevel userLevelOf(String value) {
+ switch (value) {
+ case "user": return LastLoginInfo.UserLevel.user;
+ case "developer": return LastLoginInfo.UserLevel.developer;
+ case "administrator": return LastLoginInfo.UserLevel.administrator;
+ default: throw new IllegalArgumentException("Unknown user level '" + value + "'.");
+ }
+ }
+
+ private static String valueOf(LastLoginInfo.UserLevel userLevel) {
+ switch (userLevel) {
+ case user: return "user";
+ case developer: return "developer";
+ case administrator: return "administrator";
+ default: throw new IllegalArgumentException("Unexpected user level '" + userLevel + "'.");
+ }
+ }
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
index 13599664cef..5a1496bf507 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
@@ -91,6 +91,7 @@ import com.yahoo.vespa.hosted.controller.security.AccessControlRequests;
import com.yahoo.vespa.hosted.controller.security.Credentials;
import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
+import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import com.yahoo.vespa.hosted.controller.tenant.TenantInfo;
import com.yahoo.vespa.hosted.controller.tenant.TenantInfoAddress;
@@ -1999,6 +2000,13 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
object.setLong("createdAtMillis", tenant.createdAt().toEpochMilli());
lastDev.ifPresent(instant -> object.setLong("lastDeploymentToDevMillis", instant.toEpochMilli()));
lastSubmission.ifPresent(instant -> object.setLong("lastSubmissionToProdMillis", instant.toEpochMilli()));
+
+ tenant.lastLoginInfo().get(LastLoginInfo.UserLevel.user)
+ .ifPresent(instant -> object.setLong("lastLoginByUserMillis", instant.toEpochMilli()));
+ tenant.lastLoginInfo().get(LastLoginInfo.UserLevel.developer)
+ .ifPresent(instant -> object.setLong("lastLoginByDeveloperMillis", instant.toEpochMilli()));
+ tenant.lastLoginInfo().get(LastLoginInfo.UserLevel.administrator)
+ .ifPresent(instant -> object.setLong("lastLoginByAdministratorMillis", instant.toEpochMilli()));
}
/** Returns a copy of the given URI with the host and port from the given URI, the path set to the given path and the query set to given query*/
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
index b9cf5ca4f4d..3ca7e5ac249 100644
--- 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
@@ -1,11 +1,16 @@
// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.restapi.filter;
+import com.auth0.jwt.JWT;
import com.google.inject.Inject;
import com.yahoo.config.provision.ApplicationName;
import com.yahoo.config.provision.TenantName;
import com.yahoo.jdisc.http.filter.DiscFilterRequest;
import com.yahoo.jdisc.http.filter.security.base.JsonSecurityRequestFilterBase;
+
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.util.Date;
import java.util.logging.Level;
import com.yahoo.restapi.Path;
import com.yahoo.vespa.athenz.api.AthenzDomain;
@@ -64,9 +69,14 @@ public class AthenzRoleFilter extends JsonSecurityRequestFilterBase {
try {
Principal principal = request.getUserPrincipal();
if (principal instanceof AthenzPrincipal) {
+ Instant issuedAt = request.getClientCertificateChain().stream().findFirst()
+ .map(X509Certificate::getNotBefore)
+ .or(() -> Optional.ofNullable((String) request.getAttribute("okta.access-token")).map(iat -> JWT.decode(iat).getIssuedAt()))
+ .map(Date::toInstant)
+ .orElse(Instant.EPOCH);
request.setAttribute(SecurityContext.ATTRIBUTE_NAME, new SecurityContext(principal,
- roles((AthenzPrincipal) principal,
- request.getUri())));
+ roles((AthenzPrincipal) principal, request.getUri()),
+ issuedAt));
}
}
catch (Exception e) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilter.java
new file mode 100644
index 00000000000..9b1ccc09499
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilter.java
@@ -0,0 +1,73 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.restapi.filter;
+
+import com.google.inject.Inject;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.jdisc.http.filter.DiscFilterRequest;
+import com.yahoo.jdisc.http.filter.security.base.JsonSecurityRequestFilterBase;
+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.SecurityContext;
+import com.yahoo.vespa.hosted.controller.api.role.TenantRole;
+import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo.UserLevel.administrator;
+import static com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo.UserLevel.developer;
+import static com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo.UserLevel.user;
+
+/**
+ * A security filter protects all controller apis.
+ *
+ * @author freva
+ */
+public class LastLoginUpdateFilter extends JsonSecurityRequestFilterBase {
+
+ private static final Logger log = Logger.getLogger(LastLoginUpdateFilter.class.getName());
+
+ private final TenantController tenantController;
+
+ @Inject
+ public LastLoginUpdateFilter(Controller controller) {
+ this.tenantController = controller.tenants();
+ }
+
+ @Override
+ public Optional<ErrorResponse> filter(DiscFilterRequest request) {
+ try {
+ SecurityContext context = (SecurityContext) request.getAttribute(SecurityContext.ATTRIBUTE_NAME);
+ Map<TenantName, List<LastLoginInfo.UserLevel>> userLevelsByTenant = context.roles().stream()
+ .flatMap(LastLoginUpdateFilter::filterTenantUserLevels)
+ .collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.mapping(Map.Entry::getValue, Collectors.toList())));
+
+ userLevelsByTenant.forEach((tenant, userLevels) -> tenantController.updateLastLogin(tenant, userLevels, context.issuedAt()));
+ } catch (Exception e) {
+ log.log(Level.WARNING, "Exception updating last login:", e);
+ }
+ return Optional.empty();
+ }
+
+ public static Stream<Map.Entry<TenantName, LastLoginInfo.UserLevel>> filterTenantUserLevels(Role role) {
+ if (!(role instanceof TenantRole))
+ return Stream.empty();
+
+ TenantRole tenantRole = (TenantRole) role;
+ TenantName name = tenantRole.tenant();
+ switch (tenantRole.definition()) {
+ case athenzTenantAdmin:
+ return Stream.of(Map.entry(name, user), Map.entry(name, developer), Map.entry(name, administrator));
+ case reader: return Stream.of(Map.entry(name, user));
+ case developer: return Stream.of(Map.entry(name, developer));
+ case administrator: return Stream.of(Map.entry(name, administrator));
+ default: return Stream.empty();
+ }
+ }
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java
index 3be8d0cfe66..1c6511514a0 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java
@@ -86,14 +86,14 @@ public class SignatureFilter extends JsonSecurityRequestFilterBase {
.map(CloudTenant.class::cast);
if (tenant.isPresent() && tenant.get().developerKeys().containsKey(key))
return Optional.of(new SecurityContext(tenant.get().developerKeys().get(key),
- Set.of(Role.reader(id.tenant()),
- Role.developer(id.tenant()))));
+ Set.of(Role.reader(id.tenant()), Role.developer(id.tenant())),
+ controller.clock().instant()));
Optional <Application> application = controller.applications().getApplication(TenantAndApplicationId.from(id));
if (application.isPresent() && application.get().deployKeys().contains(key))
return Optional.of(new SecurityContext(new SimplePrincipal("headless@" + id.tenant() + "." + id.application()),
- Set.of(Role.reader(id.tenant()),
- Role.headless(id.tenant(), id.application()))));
+ Set.of(Role.reader(id.tenant()), Role.headless(id.tenant(), id.application())),
+ controller.clock().instant()));
}
return Optional.empty();
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java
index 7c0e78337ee..7fa46031c98 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java
@@ -27,8 +27,8 @@ public class AthenzTenant extends Tenant {
* Use {@link #create(TenantName, AthenzDomain, Property, Optional, Instant)}.
* */
public AthenzTenant(TenantName name, AthenzDomain domain, Property property, Optional<PropertyId> propertyId,
- Optional<Contact> contact, Instant createdAt) {
- super(name, createdAt, contact);
+ Optional<Contact> contact, Instant createdAt, LastLoginInfo lastLoginInfo) {
+ super(name, createdAt, lastLoginInfo, contact);
this.domain = Objects.requireNonNull(domain, "domain must be non-null");
this.property = Objects.requireNonNull(property, "property must be non-null");
this.propertyId = Objects.requireNonNull(propertyId, "propertyId must be non-null");
@@ -62,7 +62,7 @@ public class AthenzTenant extends Tenant {
/** Create a new Athenz tenant */
public static AthenzTenant create(TenantName name, AthenzDomain domain, Property property,
Optional<PropertyId> propertyId, Instant createdAt) {
- return new AthenzTenant(requireName(name), domain, property, propertyId, Optional.empty(), createdAt);
+ return new AthenzTenant(requireName(name), domain, property, propertyId, Optional.empty(), createdAt, LastLoginInfo.EMPTY);
}
@Override
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java
index 2ed1ddc40e1..5d0bb780c81 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java
@@ -23,8 +23,9 @@ public class CloudTenant extends Tenant {
private final TenantInfo info;
/** Public for the serialization layer — do not use! */
- public CloudTenant(TenantName name, Instant createdAt, Optional<Principal> creator, BiMap<PublicKey, Principal> developerKeys, TenantInfo info) {
- super(name, createdAt, Optional.empty());
+ public CloudTenant(TenantName name, Instant createdAt, LastLoginInfo lastLoginInfo, Optional<Principal> creator,
+ BiMap<PublicKey, Principal> developerKeys, TenantInfo info) {
+ super(name, createdAt, lastLoginInfo, Optional.empty());
this.creator = creator;
this.developerKeys = developerKeys;
this.info = Objects.requireNonNull(info);
@@ -34,6 +35,7 @@ public class CloudTenant extends Tenant {
public static CloudTenant create(TenantName tenantName, Instant createdAt, Principal creator) {
return new CloudTenant(requireName(tenantName),
createdAt,
+ LastLoginInfo.EMPTY,
Optional.ofNullable(creator),
ImmutableBiMap.of(), TenantInfo.EMPTY);
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/LastLoginInfo.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/LastLoginInfo.java
new file mode 100644
index 00000000000..15f2f97e7d1
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/LastLoginInfo.java
@@ -0,0 +1,55 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.tenant;
+
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * @author freva
+ */
+public class LastLoginInfo {
+
+ public static final LastLoginInfo EMPTY = new LastLoginInfo(Map.of());
+
+ private final Map<UserLevel, Instant> lastLoginByUserLevel;
+
+ public LastLoginInfo(Map<UserLevel, Instant> lastLoginByUserLevel) {
+ this.lastLoginByUserLevel = Map.copyOf(lastLoginByUserLevel);
+ }
+
+ public Optional<Instant> get(UserLevel userLevel) {
+ return Optional.ofNullable(lastLoginByUserLevel.get(userLevel));
+ }
+
+ /**
+ * Returns new instance with updated last login time if the given {@code loginAt} timestamp is after the current
+ * for the given {@code userLevel}, otherwise returns this
+ */
+ public LastLoginInfo withLastLoginIfLater(UserLevel userLevel, Instant loginAt) {
+ Instant lastLogin = lastLoginByUserLevel.getOrDefault(userLevel, Instant.EPOCH);
+ if (loginAt.isAfter(lastLogin)) {
+ Map<UserLevel, Instant> lastLoginByUserLevel = new HashMap<>(this.lastLoginByUserLevel);
+ lastLoginByUserLevel.put(userLevel, loginAt);
+ return new LastLoginInfo(lastLoginByUserLevel);
+ }
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ LastLoginInfo lastLoginInfo = (LastLoginInfo) o;
+ return lastLoginByUserLevel.equals(lastLoginInfo.lastLoginByUserLevel);
+ }
+
+ @Override
+ public int hashCode() {
+ return lastLoginByUserLevel.hashCode();
+ }
+
+ public enum UserLevel { user, developer, administrator };
+}
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 7687796c697..f8b54e7eff3 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
@@ -17,11 +17,13 @@ public abstract class Tenant {
private final TenantName name;
private final Instant createdAt;
+ private final LastLoginInfo lastLoginInfo;
private final Optional<Contact> contact;
- Tenant(TenantName name, Instant createdAt, Optional<Contact> contact) {
+ Tenant(TenantName name, Instant createdAt, LastLoginInfo lastLoginInfo, Optional<Contact> contact) {
this.name = name;
this.createdAt = createdAt;
+ this.lastLoginInfo = lastLoginInfo;
this.contact = contact;
}
@@ -35,6 +37,11 @@ public abstract class Tenant {
return createdAt;
}
+ /** Returns login information for this tenant */
+ public LastLoginInfo lastLoginInfo() {
+ return lastLoginInfo;
+ }
+
/** Contact information for this tenant */
public Optional<Contact> contact() {
return contact;
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 c26e4db7996..4fcf4f344e3 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
@@ -13,6 +13,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
+import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
import com.yahoo.vespa.hosted.controller.tenant.TenantInfo;
import com.yahoo.vespa.hosted.controller.tenant.TenantInfoAddress;
import com.yahoo.vespa.hosted.controller.tenant.TenantInfoBillingContact;
@@ -21,8 +22,9 @@ import org.junit.Test;
import java.net.URI;
import java.security.PublicKey;
import java.time.Instant;
-import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
@@ -79,7 +81,8 @@ public class TenantSerializerTest {
new Property("property1"),
Optional.of(new PropertyId("1")),
Optional.of(contact()),
- Instant.EPOCH);
+ Instant.EPOCH,
+ lastLoginInfo(321L, 654L, 987L));
AthenzTenant serialized = (AthenzTenant) serializer.tenantFrom(serializer.toSlime(tenant), () -> { throw new UnsupportedOperationException(); });
assertEquals(tenant.contact(), serialized.contact());
}
@@ -88,6 +91,7 @@ public class TenantSerializerTest {
public void cloud_tenant() {
CloudTenant tenant = new CloudTenant(TenantName.from("elderly-lady"),
Instant.ofEpochMilli(1234L),
+ lastLoginInfo(123L, 456L, null),
Optional.of(new SimplePrincipal("foobar-user")),
ImmutableBiMap.of(publicKey, new SimplePrincipal("joe"),
otherPublicKey, new SimplePrincipal("jane")),
@@ -103,6 +107,7 @@ public class TenantSerializerTest {
public void cloud_tenant_with_info() {
CloudTenant tenant = new CloudTenant(TenantName.from("elderly-lady"),
Instant.EPOCH,
+ lastLoginInfo(null, 789L, 654L),
Optional.of(new SimplePrincipal("foobar-user")),
ImmutableBiMap.of(publicKey, new SimplePrincipal("joe"),
otherPublicKey, new SimplePrincipal("jane")),
@@ -156,18 +161,25 @@ public class TenantSerializerTest {
assertEquals(fullInfo, roundTripInfo);
}
- private Contact contact() {
+ private static Contact contact() {
return new Contact(
URI.create("http://contact1.test"),
URI.create("http://property1.test"),
URI.create("http://issue-tracker-1.test"),
List.of(
- Collections.singletonList("person1"),
- Collections.singletonList("person2")
+ List.of("person1"),
+ List.of("person2")
),
"queue",
Optional.empty()
);
}
+ private static LastLoginInfo lastLoginInfo(Long user, Long developer, Long administrator) {
+ Map<LastLoginInfo.UserLevel, Instant> lastLogins = new HashMap<>();
+ Optional.ofNullable(user).map(Instant::ofEpochMilli).ifPresent(i -> lastLogins.put(LastLoginInfo.UserLevel.user, i));
+ Optional.ofNullable(developer).map(Instant::ofEpochMilli).ifPresent(i -> lastLogins.put(LastLoginInfo.UserLevel.developer, i));
+ Optional.ofNullable(administrator).map(Instant::ofEpochMilli).ifPresent(i -> lastLogins.put(LastLoginInfo.UserLevel.administrator, i));
+ return new LastLoginInfo(lastLogins);
+ }
}
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 32f43f57152..434c83898ee 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
@@ -67,6 +67,7 @@ import com.yahoo.vespa.hosted.controller.routing.GlobalRouting;
import com.yahoo.vespa.hosted.controller.security.AthenzCredentials;
import com.yahoo.vespa.hosted.controller.security.AthenzTenantSpec;
import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
+import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
import com.yahoo.yolean.Exceptions;
import org.junit.Before;
@@ -140,7 +141,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
private static final UserId OTHER_USER_ID = new UserId("otheruser");
private static final UserId HOSTED_VESPA_OPERATOR = new UserId("johnoperator");
private static final OktaIdentityToken OKTA_IT = new OktaIdentityToken("okta-it");
- private static final OktaAccessToken OKTA_AT = new OktaAccessToken("okta-at");
+ private static final OktaAccessToken OKTA_AT = new OktaAccessToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.he0ErCNloe4J7Id0Ry2SEDg09lKkZkfsRiGsdX_vgEg");
private ContainerTester tester;
@@ -192,8 +193,10 @@ public class ApplicationApiTest extends ControllerContainerTest {
new File("tenant-without-applications-with-id.json"));
// GET a tenant with property ID and contact information
updateContactInformation();
+ tester.controller().tenants().updateLastLogin(TenantName.from("tenant2"),
+ List.of(LastLoginInfo.UserLevel.user, LastLoginInfo.UserLevel.administrator), Instant.ofEpochMilli(1234));
tester.assertResponse(request("/application/v4/tenant/tenant2", GET).userIdentity(USER_ID),
- new File("tenant-with-contact-info.json"));
+ new File("tenant2.json"));
// POST (create) an application
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1", POST)
@@ -1237,7 +1240,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
// Create legacy tenant name containing underscores
tester.controller().curator().writeTenant(new AthenzTenant(TenantName.from("my_tenant"), ATHENZ_TENANT_DOMAIN,
- new Property("property1"), Optional.empty(), Optional.empty(), Instant.EPOCH));
+ new Property("property1"), Optional.empty(), Optional.empty(), Instant.EPOCH, LastLoginInfo.EMPTY));
// POST (add) a Athenz tenant with dashes duplicates existing one with underscores
tester.assertResponse(request("/application/v4/tenant/my-tenant", POST)
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-with-contact-info.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-with-contact-info.json
deleted file mode 100644
index 921eae72161..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-with-contact-info.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "tenant": "tenant2",
- "type": "ATHENS",
- "athensDomain": "domain2",
- "property": "property2",
- "propertyId": "1234",
- "propertyUrl": "www.properties.tld/1234",
- "contactsUrl": "www.contacts.tld/1234",
- "issueCreationUrl": "www.issues.tld/1234",
- "contacts": [
- [
- "alice"
- ],
- [
- "bob"
- ]
- ],
- "applications": [],
- "metaData": {
- "createdAtMillis": "(ignore)"
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant2.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant2.json
index 7da5918419c..497d80c96a5 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant2.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant2.json
@@ -17,6 +17,8 @@
],
"applications": [],
"metaData": {
- "createdAtMillis": "(ignore)"
+ "createdAtMillis": "(ignore)",
+ "lastLoginByUserMillis": 1234,
+ "lastLoginByAdministratorMillis": 1234
}
} \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilterTest.java
new file mode 100644
index 00000000000..df402e8c594
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilterTest.java
@@ -0,0 +1,59 @@
+package com.yahoo.vespa.hosted.controller.restapi.filter;
+
+import com.yahoo.application.container.handler.Request;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.RequestHandlerTestDriver;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.jdisc.http.filter.DiscFilterRequest;
+import com.yahoo.vespa.hosted.controller.ControllerTester;
+import com.yahoo.vespa.hosted.controller.api.role.Role;
+import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
+import com.yahoo.vespa.hosted.controller.restapi.ApplicationRequestToDiscFilterRequestWrapper;
+import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
+import org.junit.Test;
+
+import java.time.Instant;
+import java.util.Set;
+
+import static com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo.UserLevel.administrator;
+import static com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo.UserLevel.developer;
+import static com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo.UserLevel.user;
+
+import static org.junit.Assert.assertEquals;
+
+public class LastLoginUpdateFilterTest {
+
+ private static final TenantName tenant1 = TenantName.from("tenant1");
+ private static final TenantName tenant2 = TenantName.from("tenant2");
+
+ private final ControllerTester tester = new ControllerTester();
+ private final LastLoginUpdateFilter filter = new LastLoginUpdateFilter(tester.controller());
+
+ @Test
+ public void updateLastLoginTimeTest() {
+ tester.createTenant(tenant1.value());
+ tester.createTenant(tenant2.value());
+
+ request(123, Role.developer(tenant1), Role.reader(tenant1), Role.athenzTenantAdmin(tenant2));
+ assertLastLoginBy(tenant1, 123L, 123L, null);
+ assertLastLoginBy(tenant2, 123L, 123L, 123L);
+
+ request(321, Role.administrator(tenant1), Role.reader(tenant1));
+ assertLastLoginBy(tenant1, 321L, 123L, 321L);
+ assertLastLoginBy(tenant2, 123L, 123L, 123L);
+ }
+
+ private void assertLastLoginBy(TenantName tenantName, Long lastUserLoginAt, Long lastDeveloperLoginAt, Long lastAdministratorLoginAt) {
+ LastLoginInfo loginInfo = tester.controller().tenants().require(tenantName).lastLoginInfo();
+ assertEquals(lastUserLoginAt, loginInfo.get(user).map(Instant::toEpochMilli).orElse(null));
+ assertEquals(lastDeveloperLoginAt, loginInfo.get(developer).map(Instant::toEpochMilli).orElse(null));
+ assertEquals(lastAdministratorLoginAt, loginInfo.get(administrator).map(Instant::toEpochMilli).orElse(null));
+ }
+
+ private void request(long issuedAt, Role... roles) {
+ SecurityContext context = new SecurityContext(() -> "bob", Set.of(roles), Instant.ofEpochMilli(issuedAt));
+ Request request = new Request("/", new byte[0], Request.Method.GET, context.principal());
+ request.getAttributes().put(SecurityContext.ATTRIBUTE_NAME, context);
+ filter.filter(new ApplicationRequestToDiscFilterRequestWrapper(request));
+ }
+} \ No newline at end of file
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 d1f8fbebdb6..390823271b4 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
@@ -17,6 +17,7 @@ import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.restapi.ApplicationRequestToDiscFilterRequestWrapper;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
+import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
import com.yahoo.vespa.hosted.controller.tenant.TenantInfo;
import org.junit.Before;
import org.junit.Test;
@@ -69,6 +70,7 @@ public class SignatureFilterTest {
tester.curator().writeTenant(new CloudTenant(appId.tenant(),
Instant.EPOCH,
+ LastLoginInfo.EMPTY,
Optional.empty(),
ImmutableBiMap.of(),
TenantInfo.EMPTY));
@@ -98,25 +100,29 @@ public class SignatureFilterTest {
verifySecurityContext(requestOf(signer.signed(request.copy(), Method.GET, InputStream::nullInputStream), emptyBody),
new SecurityContext(new SimplePrincipal("headless@my-tenant.my-app"),
Set.of(Role.reader(id.tenant()),
- Role.headless(id.tenant(), id.application()))));
+ Role.headless(id.tenant(), id.application())),
+ tester.clock().instant()));
// Signed POST request with X-Key header gets a headless role.
byte[] hiBytes = new byte[]{0x48, 0x69};
verifySecurityContext(requestOf(signer.signed(request.copy(), Method.POST, () -> new ByteArrayInputStream(hiBytes)), hiBytes),
new SecurityContext(new SimplePrincipal("headless@my-tenant.my-app"),
Set.of(Role.reader(id.tenant()),
- Role.headless(id.tenant(), id.application()))));
+ Role.headless(id.tenant(), id.application())),
+ tester.clock().instant()));
// Signed request gets a developer role when a matching developer key is stored for the tenant.
tester.curator().writeTenant(new CloudTenant(appId.tenant(),
Instant.EPOCH,
+ LastLoginInfo.EMPTY,
Optional.empty(),
ImmutableBiMap.of(publicKey, () -> "user"),
TenantInfo.EMPTY));
verifySecurityContext(requestOf(signer.signed(request.copy(), Method.POST, () -> new ByteArrayInputStream(hiBytes)), hiBytes),
new SecurityContext(new SimplePrincipal("user"),
Set.of(Role.reader(id.tenant()),
- Role.developer(id.tenant()))));
+ Role.developer(id.tenant())),
+ tester.clock().instant()));
// Unsigned requests still get no roles.
verifySecurityContext(requestOf(request.copy().method("GET", HttpRequest.BodyPublishers.ofByteArray(emptyBody)).build(), emptyBody),