1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
|
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.restapi.dataplanetoken;
import com.yahoo.config.provision.TenantName;
import com.yahoo.security.token.Token;
import com.yahoo.security.token.TokenCheckHash;
import com.yahoo.security.token.TokenDomain;
import com.yahoo.security.token.TokenGenerator;
import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.DataplaneToken;
import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.DataplaneTokenVersions;
import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.FingerPrint;
import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.TokenId;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import java.security.Principal;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
/**
* Service to list, generate and delete data plane tokens
*
* @author mortent
*/
public class DataplaneTokenService {
private static final String TOKEN_PREFIX = "vespa_cloud_";
private static final int TOKEN_BYTES = 32;
private static final int CHECK_HASH_BYTES = 32;
public static final Duration DEFAULT_TTL = Duration.ofDays(30);
private final Controller controller;
public DataplaneTokenService(Controller controller) {
this.controller = controller;
}
/**
* List valid tokens for a tenant
*/
public List<DataplaneTokenVersions> listTokens(TenantName tenantName) {
return controller.curator().readDataplaneTokens(tenantName);
}
/**
* Generates a token using tenant name as the check access context.
* Persists the token fingerprint and check access hash, but not the token value
*
* @param tenantName name of the tenant to connect the token to
* @param tokenId The user generated name/id of the token
* @param expiration Token expiration
* @param principal The principal making the request
* @return a DataplaneToken containing the secret generated token
*/
public DataplaneToken generateToken(TenantName tenantName, TokenId tokenId, Instant expiration, Principal principal) {
TokenDomain tokenDomain = TokenDomain.of("Vespa Cloud tenant data plane:%s".formatted(tenantName.value()));
Token token = TokenGenerator.generateToken(tokenDomain, TOKEN_PREFIX, TOKEN_BYTES);
TokenCheckHash checkHash = TokenCheckHash.of(token, CHECK_HASH_BYTES);
DataplaneTokenVersions.Version newTokenVersion = new DataplaneTokenVersions.Version(
FingerPrint.of(token.fingerprint().toDelimitedHexString()),
checkHash.toHexString(),
controller.clock().instant(),
Optional.ofNullable(expiration),
principal.getName());
CuratorDb curator = controller.curator();
try (Mutex lock = curator.lock(tenantName)) {
List<DataplaneTokenVersions> dataplaneTokenVersions = curator.readDataplaneTokens(tenantName);
Optional<DataplaneTokenVersions> existingToken = dataplaneTokenVersions.stream().filter(t -> Objects.equals(t.tokenId(), tokenId)).findFirst();
if (existingToken.isPresent()) {
List<DataplaneTokenVersions.Version> versions = existingToken.get().tokenVersions();
versions = Stream.concat(
versions.stream(),
Stream.of(newTokenVersion))
.toList();
dataplaneTokenVersions = Stream.concat(
dataplaneTokenVersions.stream().filter(t -> !Objects.equals(t.tokenId(), tokenId)),
Stream.of(new DataplaneTokenVersions(tokenId, versions)))
.toList();
} else {
DataplaneTokenVersions newToken = new DataplaneTokenVersions(tokenId, List.of(newTokenVersion));
dataplaneTokenVersions = Stream.concat(dataplaneTokenVersions.stream(), Stream.of(newToken)).toList();
}
curator.writeDataplaneTokens(tenantName, dataplaneTokenVersions);
// Return the data plane token including the secret token.
return new DataplaneToken(tokenId, FingerPrint.of(token.fingerprint().toDelimitedHexString()),
token.secretTokenString(), Optional.ofNullable(expiration));
}
}
/**
* Deletes the token version identitfied by tokenId and tokenFingerPrint
* @throws IllegalArgumentException if the version could not be found
*/
public void deleteToken(TenantName tenantName, TokenId tokenId, FingerPrint tokenFingerprint) {
CuratorDb curator = controller.curator();
try (Mutex lock = curator.lock(tenantName)) {
List<DataplaneTokenVersions> dataplaneTokenVersions = curator.readDataplaneTokens(tenantName);
Optional<DataplaneTokenVersions> existingToken = dataplaneTokenVersions.stream().filter(t -> Objects.equals(t.tokenId(), tokenId)).findFirst();
if (existingToken.isPresent()) {
List<DataplaneTokenVersions.Version> versions = existingToken.get().tokenVersions();
versions = versions.stream().filter(v -> !Objects.equals(v.fingerPrint(), tokenFingerprint)).toList();
if (versions.isEmpty()) {
dataplaneTokenVersions = dataplaneTokenVersions.stream().filter(t -> !Objects.equals(t.tokenId(), tokenId)).toList();
} else {
boolean fingerPrintExists = existingToken.get().tokenVersions().stream().anyMatch(v -> v.fingerPrint().equals(tokenFingerprint));
if (fingerPrintExists) {
dataplaneTokenVersions = Stream.concat(dataplaneTokenVersions.stream().filter(t -> !Objects.equals(t.tokenId(), tokenId)), Stream.of(new DataplaneTokenVersions(tokenId, versions))).toList();
} else {
throw new IllegalArgumentException("Fingerprint does not exist: " + tokenFingerprint);
}
}
curator.writeDataplaneTokens(tenantName, dataplaneTokenVersions);
} else {
throw new IllegalArgumentException("Token does not exist: " + tokenId);
}
}
}
}
|