forked from AthenZ/athenz
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ZTS Client to read access tokens from file system (AthenZ#1028)
Co-authored-by: dma <[email protected]>
- Loading branch information
1 parent
9a3c35a
commit 3b5bcdf
Showing
11 changed files
with
378 additions
and
20 deletions.
There are no files selected for viewing
108 changes: 108 additions & 0 deletions
108
clients/java/zts/core/src/main/java/com/yahoo/athenz/zts/ZTSAccessTokenFileLoader.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
package com.yahoo.athenz.zts; | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.yahoo.athenz.auth.token.AccessToken; | ||
import com.yahoo.athenz.auth.token.jwts.JwtsSigningKeyResolver; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.io.File; | ||
import java.io.IOException; | ||
import java.util.*; | ||
|
||
public class ZTSAccessTokenFileLoader { | ||
|
||
private static final Logger LOG = LoggerFactory.getLogger(ZTSAccessTokenFileLoader.class); | ||
|
||
static public final String ACCESS_TOKEN_PATH_PROPERTY = "athenz.zts.client.accesstoken.path"; | ||
static private final String DEFAULT_ACCESS_TOKEN_DIR_PATH = "/var/lib/sia/tokens/"; | ||
static private final String ROLE_NAME_CONNECTOR = ","; | ||
static private final String DOMAIN_ROLE_CONNECTOR = ":role:"; | ||
final private String path; | ||
private JwtsSigningKeyResolver accessSignKeyResolver; | ||
private ObjectMapper objectMapper = new ObjectMapper(); | ||
private Map<String, String> roleNameMap; | ||
|
||
public ZTSAccessTokenFileLoader(JwtsSigningKeyResolver resolver) { | ||
roleNameMap = new HashMap<>(); | ||
accessSignKeyResolver = resolver; | ||
path = System.getProperty(ACCESS_TOKEN_PATH_PROPERTY, DEFAULT_ACCESS_TOKEN_DIR_PATH); | ||
} | ||
|
||
public void preload() { | ||
File dir = new File(path); | ||
|
||
// preload the map from the <domain, rolesname> -> <file path> | ||
// expected dir should be <base token path>/<domain dir>/<token file>s | ||
// after preload the map, when we look up the access token, | ||
// the map will directly read the required file | ||
if (dir.exists() && dir.isDirectory()) { | ||
for (File domainDir: dir.listFiles()) { | ||
if (domainDir.isDirectory()) { | ||
for (File tokenFile: domainDir.listFiles()) { | ||
if (!tokenFile.isDirectory()) { | ||
AccessTokenResponse accessTokenResponse = null; | ||
try { | ||
accessTokenResponse = objectMapper.readValue(tokenFile, AccessTokenResponse.class); | ||
} catch (IOException e) { | ||
LOG.error("Failed to load or parse token file: {}", tokenFile); | ||
} | ||
|
||
// if access token parsed fail, continue to scan tokens | ||
if (accessTokenResponse == null) { | ||
continue; | ||
} | ||
|
||
AccessTokenResponseCacheEntry cacheEntry = new AccessTokenResponseCacheEntry(accessTokenResponse); | ||
|
||
// check access token is still valid | ||
if (!cacheEntry.isExpired(-1)) { | ||
addToRoleMap(domainDir.getName(), tokenFile.getName(), accessTokenResponse); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
} | ||
|
||
// function load the access token from file | ||
public AccessTokenResponse lookupAccessTokenFromDisk(String domain, List<String> rolesName) throws IOException { | ||
final String rolesStr = getRolesStr(domain, rolesName); | ||
final String fileName = roleNameMap.get(rolesStr); | ||
LOG.debug("Trying to fetch access token from disk for domain: {}, roleNames: {}, roleMap key: {}. file name: {}", | ||
domain, rolesName, rolesStr, fileName); | ||
if (fileName == null) { | ||
return null; | ||
} | ||
File tokenFile = new File(path + File.separator + domain + File.separator + fileName); | ||
|
||
return objectMapper.readValue(tokenFile, AccessTokenResponse.class); | ||
} | ||
|
||
static private String getRolesStr(String domain, List<String> roleNames) { | ||
// in case the rolesName is immutable, make a copy of role name list | ||
if (roleNames == null || roleNames.isEmpty()) { | ||
//if no role name specific, should return all roles | ||
return domain + DOMAIN_ROLE_CONNECTOR + "*"; | ||
} | ||
List<String> roleNamesCopy = new ArrayList<>(roleNames); | ||
Collections.sort(roleNamesCopy); | ||
return domain + DOMAIN_ROLE_CONNECTOR + String.join(ROLE_NAME_CONNECTOR, roleNamesCopy); | ||
} | ||
|
||
private void addToRoleMap(String domain, String fileName, AccessTokenResponse accessTokenResponse) { | ||
// parse roles from access token | ||
final String token = accessTokenResponse.getAccess_token(); | ||
|
||
try { | ||
AccessToken accessToken = new AccessToken(token, accessSignKeyResolver); | ||
List<String> roleNames = accessToken.getScope(); | ||
roleNameMap.put(getRolesStr(domain, roleNames), fileName); | ||
} catch (Exception e) { | ||
LOG.error("Got error to parse access token file {}, error: {}", fileName, e.getMessage()); | ||
return; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
91 changes: 91 additions & 0 deletions
91
clients/java/zts/core/src/test/java/com/yahoo/athenz/zts/AccessTokenTestFileHelper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
package com.yahoo.athenz.zts; | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.yahoo.athenz.auth.token.AccessToken; | ||
import com.yahoo.athenz.auth.util.Crypto; | ||
import io.jsonwebtoken.SignatureAlgorithm; | ||
|
||
import java.io.BufferedWriter; | ||
import java.io.File; | ||
import java.io.FileWriter; | ||
import java.io.IOException; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.nio.file.Paths; | ||
import java.security.PrivateKey; | ||
import java.security.cert.X509Certificate; | ||
import java.util.Collections; | ||
|
||
import static org.testng.Assert.fail; | ||
|
||
public class AccessTokenTestFileHelper { | ||
|
||
private static final File ecPrivateKey = new File("./src/test/resources/unit_test_ec_private.key"); | ||
private static final File tokenFile = new File("./src/test/resources/test.domain/admin"); | ||
private static final File invalidTokenFile = new File("./src/test/resources/test.domain/invalid"); | ||
|
||
private static AccessToken createAccessToken(long now) { | ||
|
||
AccessToken accessToken = new AccessToken(); | ||
accessToken.setAuthTime(now); | ||
accessToken.setScope(Collections.singletonList("admin")); | ||
accessToken.setSubject("subject"); | ||
accessToken.setUserId("userid"); | ||
accessToken.setExpiryTime(now + 3600); | ||
accessToken.setIssueTime(now); | ||
accessToken.setClientId("mtls"); | ||
accessToken.setAudience("coretech"); | ||
accessToken.setVersion(1); | ||
accessToken.setIssuer("athenz"); | ||
accessToken.setProxyPrincipal("proxy.user"); | ||
accessToken.setConfirmEntry("x5t#uri", "spiffe://athenz/sa/api"); | ||
|
||
try { | ||
Path path = Paths.get("src/test/resources/mtls_token_spec.cert"); | ||
String certStr = new String(Files.readAllBytes(path)); | ||
X509Certificate cert = Crypto.loadX509Certificate(certStr); | ||
accessToken.setConfirmX509CertHash(cert); | ||
} catch (IOException ignored) { | ||
fail(); | ||
} | ||
|
||
return accessToken; | ||
} | ||
|
||
public static void setupTokenFile() { | ||
AccessTokenResponse accessTokenResponse = new AccessTokenResponse(); | ||
long now = System.currentTimeMillis() / 1000; | ||
AccessToken accessToken = createAccessToken(now); | ||
PrivateKey privateKey = Crypto.loadPrivateKey(ecPrivateKey); | ||
String accessJws = accessToken.getSignedToken(privateKey, "eckey1", SignatureAlgorithm.ES256); | ||
|
||
accessTokenResponse.setAccess_token(accessJws); | ||
accessTokenResponse.setExpires_in(28800); | ||
accessTokenResponse.setScope("admin"); | ||
accessTokenResponse.setToken_type("Bearer"); | ||
|
||
ObjectMapper objectMapper = new ObjectMapper(); | ||
|
||
try { | ||
objectMapper.writeValue(tokenFile, accessTokenResponse); | ||
System.out.println("Write new access token " + accessTokenResponse.toString() + " to file: " + tokenFile + " successfully"); | ||
} catch (IOException e) { | ||
e.printStackTrace(); | ||
fail(); | ||
} | ||
} | ||
|
||
public static void setupInvalidTokenFile() { | ||
String str = "Invalid access token"; | ||
|
||
try { | ||
BufferedWriter writer = new BufferedWriter(new FileWriter(invalidTokenFile)); | ||
writer.write(str); | ||
writer.close(); | ||
} catch (IOException e) { | ||
e.printStackTrace(); | ||
fail(); | ||
} | ||
|
||
} | ||
} |
Oops, something went wrong.