diff --git a/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java b/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java index 004472d..6a71cd2 100644 --- a/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java +++ b/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java @@ -19,8 +19,7 @@ package de.stklcode.jvault.connector; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import de.stklcode.jvault.connector.exception.*; -import de.stklcode.jvault.connector.model.AuthBackend; -import de.stklcode.jvault.connector.model.Token; +import de.stklcode.jvault.connector.model.*; import de.stklcode.jvault.connector.model.response.*; import de.stklcode.jvault.connector.model.response.embedded.AuthMethod; import org.apache.http.HttpResponse; @@ -44,24 +43,25 @@ import java.util.stream.Collectors; /** * Vault Connector implementatin using Vault's HTTP API. * - * @author Stefan Kalscheuer - * @since 0.1 + * @author Stefan Kalscheuer + * @since 0.1 */ public class HTTPVaultConnector implements VaultConnector { - private static final String PATH_PREFIX = "/v1/"; - private static final String PATH_SEAL_STATUS = "sys/seal-status"; - private static final String PATH_SEAL = "sys/seal"; - private static final String PATH_UNSEAL = "sys/unseal"; - private static final String PATH_INIT = "sys/init"; - private static final String PATH_AUTH = "sys/auth"; - private static final String PATH_TOKEN = "auth/token"; - private static final String PATH_LOOKUP = "/lookup"; - private static final String PATH_CREATE = "/create"; + private static final String PATH_PREFIX = "/v1/"; + private static final String PATH_SEAL_STATUS = "sys/seal-status"; + private static final String PATH_SEAL = "sys/seal"; + private static final String PATH_UNSEAL = "sys/unseal"; + private static final String PATH_INIT = "sys/init"; + private static final String PATH_AUTH = "sys/auth"; + private static final String PATH_TOKEN = "auth/token"; + private static final String PATH_LOOKUP = "/lookup"; + private static final String PATH_CREATE = "/create"; private static final String PATH_CREATE_ORPHAN = "/create-orphan"; private static final String PATH_AUTH_USERPASS = "auth/userpass/login/"; - private static final String PATH_AUTH_APPID = "auth/app-id/"; - private static final String PATH_SECRET = "secret"; - private static final String PATH_REVOKE = "sys/revoke/"; + private static final String PATH_AUTH_APPID = "auth/app-id/"; + private static final String PATH_AUTH_APPROLE = "auth/approle/"; + private static final String PATH_SECRET = "secret"; + private static final String PATH_REVOKE = "sys/revoke/"; private final ObjectMapper jsonMapper; @@ -74,8 +74,8 @@ public class HTTPVaultConnector implements VaultConnector { /** * Create connector using hostname and schema. * - * @param hostname The hostname - * @param useTLS If TRUE, use HTTPS, otherwise HTTP + * @param hostname The hostname + * @param useTLS If TRUE, use HTTPS, otherwise HTTP */ public HTTPVaultConnector(String hostname, boolean useTLS) { this(hostname, useTLS, null); @@ -84,9 +84,9 @@ public class HTTPVaultConnector implements VaultConnector { /** * Create connector using hostname, schema and port. * - * @param hostname The hostname - * @param useTLS If TRUE, use HTTPS, otherwise HTTP - * @param port The port + * @param hostname The hostname + * @param useTLS If TRUE, use HTTPS, otherwise HTTP + * @param port The port */ public HTTPVaultConnector(String hostname, boolean useTLS, Integer port) { this(hostname, useTLS, port, PATH_PREFIX); @@ -95,10 +95,10 @@ public class HTTPVaultConnector implements VaultConnector { /** * Create connector using hostname, schame, port and path. * - * @param hostname The hostname - * @param useTLS If TRUE, use HTTPS, otherwise HTTP - * @param port The port - * @param prefix HTTP API prefix (default: /v1/" + * @param hostname The hostname + * @param useTLS If TRUE, use HTTPS, otherwise HTTP + * @param port The port + * @param prefix HTTP API prefix (default: /v1/" */ public HTTPVaultConnector(String hostname, boolean useTLS, Integer port, String prefix) { this(((useTLS) ? "https" : "http") + @@ -110,7 +110,7 @@ public class HTTPVaultConnector implements VaultConnector { /** * Create connector using full URL. * - * @param baseURL The URL + * @param baseURL The URL */ public HTTPVaultConnector(String baseURL) { this.baseURL = baseURL; @@ -207,31 +207,40 @@ public class HTTPVaultConnector implements VaultConnector { @Override public AuthResponse authUserPass(final String username, final String password) throws VaultConnectorException { - Map payload = new HashMap<>(); + final Map payload = new HashMap<>(); payload.put("password", password); - try { - /* Get response */ - String response = requestPost(PATH_AUTH_USERPASS + username, payload); - /* Parse response */ - AuthResponse upr = jsonMapper.readValue(response, AuthResponse.class); - /* verify response */ - this.token = upr.getAuth().getClientToken(); - this.tokenTTL = System.currentTimeMillis() + upr.getAuth().getLeaseDuration() * 1000L; - this.authorized = true; - return upr; - } catch (IOException e) { - throw new InvalidResponseException("Unable to parse response", e); - } + return queryAuth(PATH_AUTH_USERPASS + username, payload); } @Override public AuthResponse authAppId(final String appID, final String userID) throws VaultConnectorException { - Map payload = new HashMap<>(); + final Map payload = new HashMap<>(); payload.put("app_id", appID); payload.put("user_id", userID); + return queryAuth(PATH_AUTH_APPID + "login", payload); + } + + @Override + public AuthResponse authAppRole(final String roleID, final String secretID) throws VaultConnectorException { + final Map payload = new HashMap<>(); + payload.put("role_id", roleID); + if (secretID != null) + payload.put("secret_id", secretID); + return queryAuth(PATH_AUTH_APPROLE + "login", payload); + } + + /** + * Query authorization request to given backend + * + * @param path The path to request + * @param payload Payload (credentials) + * @return The AuthResponse + * @throws VaultConnectorException on errors + */ + private AuthResponse queryAuth(final String path, final Map payload) throws VaultConnectorException { try { /* Get response */ - String response = requestPost(PATH_AUTH_APPID + "login", payload); + String response = requestPost(path, payload); /* Parse response */ AuthResponse auth = jsonMapper.readValue(response, AuthResponse.class); /* verify response */ @@ -273,6 +282,162 @@ public class HTTPVaultConnector implements VaultConnector { return true; } + @Override + public boolean createAppRole(final AppRole role) throws VaultConnectorException { + if (!isAuthorized()) + throw new AuthorizationRequiredException(); + /* Get response */ + String response = requestPost(PATH_AUTH_APPROLE + "role/" + role.getName(), role); + /* Response should be code 204 without content */ + if (!response.equals("")) + throw new InvalidResponseException("Received response where non was expected."); + + /* Set custom ID if provided */ + return !(role.getId() != null && !role.getId().isEmpty()) || setAppRoleID(role.getName(), role.getId()); + } + + @Override + public AppRoleResponse lookupAppRole(final String roleName) throws VaultConnectorException { + if (!isAuthorized()) + throw new AuthorizationRequiredException(); + /* Request HTTP response and parse Secret */ + try { + String response = requestGet(PATH_AUTH_APPROLE + "role/" + roleName, new HashMap<>()); + return jsonMapper.readValue(response, AppRoleResponse.class); + } catch (IOException e) { + throw new InvalidResponseException("Unable to parse response", e); + } catch (URISyntaxException ignored) { + /* this should never occur and may leak sensible information */ + throw new InvalidRequestException("Invalid URI format."); + } + } + + @Override + public boolean deleteAppRole(String roleName) throws VaultConnectorException { + if (!isAuthorized()) + throw new AuthorizationRequiredException(); + + /* Request HTTP response and expect empty result */ + String response = requestDelete(PATH_AUTH_APPROLE + "role/" + roleName); + + /* Response should be code 204 without content */ + if (!response.equals("")) + throw new InvalidResponseException("Received response where non was expected."); + + return true; + } + + @Override + public String getAppRoleID(final String roleName) throws VaultConnectorException { + if (!isAuthorized()) + throw new AuthorizationRequiredException(); + /* Request HTTP response and parse Secret */ + try { + String response = requestGet(PATH_AUTH_APPROLE + "role/" + roleName + "/role-id", new HashMap<>()); + return jsonMapper.readValue(response, RawDataResponse.class).getData().get("role_id").toString(); + } catch (IOException e) { + throw new InvalidResponseException("Unable to parse response", e); + } catch (URISyntaxException ignored) { + /* this should never occur and may leak sensible information */ + throw new InvalidRequestException("Invalid URI format."); + } + } + + @Override + public boolean setAppRoleID(final String roleName, final String roleID) throws VaultConnectorException { + if (!isAuthorized()) + throw new AuthorizationRequiredException(); + /* Request HTTP response and parse Secret */ + Map payload = new HashMap<>(); + payload.put("role_id", roleID); + String response = requestPost(PATH_AUTH_APPROLE + "role/" + roleName + "/role-id", payload); + /* Response should be code 204 without content */ + if (!response.equals("")) + throw new InvalidResponseException("Received response where non was expected."); + return true; + } + + @Override + public AppRoleSecretResponse createAppRoleSecret(final String roleName, final AppRoleSecret secret) throws VaultConnectorException { + if (!isAuthorized()) + throw new AuthorizationRequiredException(); + /* Get response */ + String response; + if (secret.getId() != null && !secret.getId().isEmpty()) + response = requestPost(PATH_AUTH_APPROLE + "role/" + roleName + "/custom-secret-id", secret); + else + response = requestPost(PATH_AUTH_APPROLE + "role/" + roleName + "/secret-id", secret); + + try { + /* Extract the secret ID from response */ + return jsonMapper.readValue(response, AppRoleSecretResponse.class); + } catch (IOException e) { + throw new InvalidResponseException("Unable to parse response."); + } + } + + @Override + public AppRoleSecretResponse lookupAppRoleSecret(final String roleName, final String secretID) throws VaultConnectorException { + if (!isAuthorized()) + throw new AuthorizationRequiredException(); + /* Request HTTP response and parse Secret */ + try { + String response = requestPost(PATH_AUTH_APPROLE + "role/" + roleName + "/secret-id/lookup", new AppRoleSecret(secretID)); + return jsonMapper.readValue(response, AppRoleSecretResponse.class); + } catch (IOException e) { + throw new InvalidResponseException("Unable to parse response", e); + } + } + + @Override + public boolean destroyAppRoleSecret(final String roleName, final String secretID) throws VaultConnectorException { + if (!isAuthorized()) + throw new AuthorizationRequiredException(); + + /* Request HTTP response and expect empty result */ + String response = requestPost(PATH_AUTH_APPROLE + "role/" + roleName + "/secret-id/destroy", new AppRoleSecret(secretID)); + + /* Response should be code 204 without content */ + if (!response.equals("")) + throw new InvalidResponseException("Received response where non was expected."); + + return true; + } + + @Override + public List listAppRoles() throws VaultConnectorException { + if (!isAuthorized()) + throw new AuthorizationRequiredException(); + + try { + String response = requestGet(PATH_AUTH_APPROLE + "role?list=true", new HashMap<>()); + SecretListResponse secrets = jsonMapper.readValue(response, SecretListResponse.class); + return secrets.getKeys(); + } catch (IOException e) { + throw new InvalidResponseException("Unable to parse response", e); + } catch (URISyntaxException ignored) { + /* this should never occur and may leak sensible information */ + throw new InvalidRequestException("Invalid URI format."); + } + } + + @Override + public List listAppRoleSecretss(final String roleName) throws VaultConnectorException { + if (!isAuthorized()) + throw new AuthorizationRequiredException(); + + try { + String response = requestGet(PATH_AUTH_APPROLE + "role/" + roleName + "/secret-id?list=true", new HashMap<>()); + SecretListResponse secrets = jsonMapper.readValue(response, SecretListResponse.class); + return secrets.getKeys(); + } catch (IOException e) { + throw new InvalidResponseException("Unable to parse response", e); + } catch (URISyntaxException ignored) { + /* this should never occur and may leak sensible information */ + throw new InvalidRequestException("Invalid URI format."); + } + } + @Override public SecretResponse readSecret(final String key) throws VaultConnectorException { if (!isAuthorized()) @@ -400,10 +565,10 @@ public class HTTPVaultConnector implements VaultConnector { /** * Execute HTTP request using POST method. * - * @param path URL path (relative to base) - * @param payload Map of payload values (will be converted to JSON) - * @return HTTP response - * @throws VaultConnectorException on connection error + * @param path URL path (relative to base) + * @param payload Map of payload values (will be converted to JSON) + * @return HTTP response + * @throws VaultConnectorException on connection error */ private String requestPost(final String path, final Object payload) throws VaultConnectorException { /* Initialize post */ @@ -428,10 +593,10 @@ public class HTTPVaultConnector implements VaultConnector { /** * Execute HTTP request using PUT method. * - * @param path URL path (relative to base) - * @param payload Map of payload values (will be converted to JSON) - * @return HTTP response - * @throws VaultConnectorException on connection error + * @param path URL path (relative to base) + * @param payload Map of payload values (will be converted to JSON) + * @return HTTP response + * @throws VaultConnectorException on connection error */ private String requestPut(final String path, final Map payload) throws VaultConnectorException { /* Initialize put */ @@ -455,9 +620,9 @@ public class HTTPVaultConnector implements VaultConnector { /** * Execute HTTP request using DELETE method. * - * @param path URL path (relative to base) - * @return HTTP response - * @throws VaultConnectorException on connection error + * @param path URL path (relative to base) + * @return HTTP response + * @throws VaultConnectorException on connection error */ private String requestDelete(final String path) throws VaultConnectorException { /* Initialize delete */ @@ -472,10 +637,10 @@ public class HTTPVaultConnector implements VaultConnector { /** * Execute HTTP request using GET method. * - * @param path URL path (relative to base) - * @param payload Map of payload values (will be converted to JSON) + * @param path URL path (relative to base) + * @param payload Map of payload values (will be converted to JSON) * @return HTTP response - * @throws VaultConnectorException on connection error + * @throws VaultConnectorException on connection error */ private String requestGet(final String path, final Map payload) throws VaultConnectorException, URISyntaxException { /* Add parameters to URI */ @@ -495,9 +660,9 @@ public class HTTPVaultConnector implements VaultConnector { /** * Execute prepared HTTP request and return result. * - * @param base Prepares Request - * @return HTTP response - * @throws VaultConnectorException on connection error + * @param base Prepares Request + * @return HTTP response + * @throws VaultConnectorException on connection error */ private String request(HttpRequestBase base) throws VaultConnectorException { /* Set JSON Header */ @@ -512,9 +677,10 @@ public class HTTPVaultConnector implements VaultConnector { switch (response.getStatusLine().getStatusCode()) { case 200: - try(BufferedReader br = new BufferedReader(new InputStreamReader(response.getEntity().getContent()))) { + try (BufferedReader br = new BufferedReader(new InputStreamReader(response.getEntity().getContent()))) { return br.lines().collect(Collectors.joining("\n")); - } catch (IOException ignored) { } + } catch (IOException ignored) { + } case 204: return ""; case 403: @@ -537,8 +703,7 @@ public class HTTPVaultConnector implements VaultConnector { } } catch (IOException e) { throw new InvalidResponseException("Unable to read response", e); - } - finally { + } finally { if (response != null && response.getEntity() != null) try { EntityUtils.consume(response.getEntity()); diff --git a/src/main/java/de/stklcode/jvault/connector/VaultConnector.java b/src/main/java/de/stklcode/jvault/connector/VaultConnector.java index 960b6e0..c7d2fd9 100644 --- a/src/main/java/de/stklcode/jvault/connector/VaultConnector.java +++ b/src/main/java/de/stklcode/jvault/connector/VaultConnector.java @@ -17,10 +17,10 @@ package de.stklcode.jvault.connector; import de.stklcode.jvault.connector.exception.VaultConnectorException; -import de.stklcode.jvault.connector.model.AuthBackend; -import de.stklcode.jvault.connector.model.Token; +import de.stklcode.jvault.connector.model.*; import de.stklcode.jvault.connector.model.response.*; +import java.util.ArrayList; import java.util.List; /** @@ -110,9 +110,34 @@ public interface VaultConnector { * @param userID The User ID * @return TRUE on success * @throws VaultConnectorException on error + * @deprecated As of Vault 0.6.1 App-ID is superseded by AppRole. Consider using {@link #authAppRole} instead. */ + @Deprecated AuthResponse authAppId(final String appID, final String userID) throws VaultConnectorException; + /** + * Authorize to Vault using AppRole method without secret ID. + * + * @param roleID The role ID + * @return TRUE on success + * @throws VaultConnectorException on error + * @since 0.4.0 + */ + default AuthResponse authAppRole(final String roleID) throws VaultConnectorException { + return authAppRole(roleID, null); + } + + /** + * Authorize to Vault using AppRole method. + * + * @param roleID The role ID + * @param secretID The secret ID + * @return TRUE on success + * @throws VaultConnectorException on error + * @since 0.4.0 + */ + AuthResponse authAppRole(final String roleID, final String secretID) throws VaultConnectorException; + /** * Register new App-ID with policy. * @@ -121,9 +146,188 @@ public interface VaultConnector { * @param displayName Arbitrary name to display * @return TRUE on success * @throws VaultConnectorException on error + * @deprecated As of Vault 0.6.1 App-ID is superseded by AppRole. Consider using {@link #createAppRole} instead. */ + @Deprecated boolean registerAppId(final String appID, final String policy, final String displayName) throws VaultConnectorException; + /** + * Register a new AppRole role from given metamodel. + * + * @param role The role + * @return TRUE on success + * @throws VaultConnectorException on error + * @since 0.4.0 + */ + boolean createAppRole(final AppRole role) throws VaultConnectorException; + + /** + * Register new AppRole role with default policy. + * + * @param roleName The role name + * @return TRUE on success + * @throws VaultConnectorException on error + * @since 0.4.0 + */ + default boolean createAppRole(final String roleName) throws VaultConnectorException { + return createAppRole(roleName, new ArrayList<>()); + } + + /** + * Register new AppRole role with policies. + * + * @param roleName The role name + * @param policies The policies to associate with + * @return TRUE on success + * @throws VaultConnectorException on error + * @since 0.4.0 + */ + default boolean createAppRole(final String roleName, final List policies) throws VaultConnectorException { + return createAppRole(roleName, policies, null); + } + + /** + * Register new AppRole role with default policy and custom ID. + * + * @param roleName The role name + * @param roleID A custom role ID + * @return TRUE on success + * @throws VaultConnectorException on error + * @since 0.4.0 + */ + default boolean createAppRole(final String roleName, final String roleID) throws VaultConnectorException { + return createAppRole(roleName, new ArrayList<>(), roleID); + } + + /** + * Register new AppRole role with policies and custom ID. + * + * @param roleName The role name + * @param policies The policies to associate with + * @param roleID A custom role ID + * @return TRUE on success + * @throws VaultConnectorException on error + * @since 0.4.0 + */ + default boolean createAppRole(final String roleName, final List policies, final String roleID) throws VaultConnectorException { + return createAppRole(new AppRoleBuilder(roleName).withPolicies(policies).withId(roleID).build()); + } + + /** + * Delete AppRole role from Vault. + * + * @param roleName The role anme + * @return TRUE on succevss + * @throws VaultConnectorException on error + */ + boolean deleteAppRole(final String roleName) throws VaultConnectorException; + + /** + * Lookup an AppRole role. + * + * @param roleName The role name + * @return Result of the lookup + * @throws VaultConnectorException on error + * @since 0.4.0 + */ + AppRoleResponse lookupAppRole(final String roleName) throws VaultConnectorException; + + /** + * Retrieve ID for an AppRole role. + * + * @param roleName The role name + * @return The role ID + * @throws VaultConnectorException on error + * @since 0.4.0 + */ + String getAppRoleID(final String roleName) throws VaultConnectorException; + + /** + * Set custom ID for an AppRole role. + * + * @param roleName The role name + * @param roleID The role ID + * @return TRUE on success + * @throws VaultConnectorException on error + * @since 0.4.0 + */ + boolean setAppRoleID(final String roleName, final String roleID) throws VaultConnectorException; + + /** + * Register new random generated AppRole secret. + * + * @param roleName The role name + * @return The secret ID + * @throws VaultConnectorException on error + * @since 0.4.0 + */ + default AppRoleSecretResponse createAppRoleSecret(final String roleName) throws VaultConnectorException { + return createAppRoleSecret(roleName, new AppRoleSecret()); + } + + /** + * Register new AppRole secret with custom ID. + * + * @param roleName The role name + * @param secretID A custom secret ID + * @return The secret ID + * @throws VaultConnectorException on error + * @since 0.4.0 + */ + default AppRoleSecretResponse createAppRoleSecret(final String roleName, final String secretID) throws VaultConnectorException { + return createAppRoleSecret(roleName, new AppRoleSecret(secretID)); + } + + /** + * Register new AppRole secret with custom ID. + * + * @param roleName The role name + * @param secret The secret meta object + * @return The secret ID + * @throws VaultConnectorException on error + * @since 0.4.0 + */ + AppRoleSecretResponse createAppRoleSecret(final String roleName, final AppRoleSecret secret) throws VaultConnectorException; + + /** + * Lookup an AppRole secret. + * + * @param roleName The role name + * @param secretID The secret ID + * @return Result of the lookup + * @throws VaultConnectorException on error + * @since 0.4.0 + */ + AppRoleSecretResponse lookupAppRoleSecret(final String roleName, final String secretID) throws VaultConnectorException; + + /** + * Destroy an AppRole secret. + * + * @param roleName The role name + * @param secretID The secret meta object + * @return The secret ID + * @throws VaultConnectorException on error + * @since 0.4.0 + */ + boolean destroyAppRoleSecret(final String roleName, final String secretID) throws VaultConnectorException; + + /** + * List existing (accessible) AppRole roles. + * + * @return List of roles + * @throws VaultConnectorException on error + */ + List listAppRoles() throws VaultConnectorException; + + /** + * List existing (accessible) secret IDs for AppRole role. + * + * @param roleName The role name + * @return List of roles + * @throws VaultConnectorException on error + */ + List listAppRoleSecretss(final String roleName) throws VaultConnectorException; + /** * Register User-ID with App-ID * @@ -131,7 +335,9 @@ public interface VaultConnector { * @param userID The User-ID * @return TRUE on success * @throws VaultConnectorException on error + * @deprecated As of Vault 0.6.1 App-ID is superseded by AppRole. Consider using {@link #createAppRoleSecret} instead. */ + @Deprecated boolean registerUserId(final String appID, final String userID) throws VaultConnectorException; /** @@ -143,7 +349,9 @@ public interface VaultConnector { * @param userID The User-ID * @return TRUE on success * @throws VaultConnectorException on error + * @deprecated As of Vault 0.6.1 App-ID is superseded by AppRole. */ + @Deprecated default boolean registerAppUserId(final String appID, final String policy, final String displayName, final String userID) throws VaultConnectorException { return registerAppId(appID, policy, userID) && registerUserId(appID, userID); } diff --git a/src/main/java/de/stklcode/jvault/connector/model/AppRole.java b/src/main/java/de/stklcode/jvault/connector/model/AppRole.java new file mode 100644 index 0000000..50e3300 --- /dev/null +++ b/src/main/java/de/stklcode/jvault/connector/model/AppRole.java @@ -0,0 +1,147 @@ +/* + * Copyright 2016 Stefan Kalscheuer + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.stklcode.jvault.connector.model; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.util.List; + +/** + * Vault AppRole role metamodel. + * + * @author Stefan Kalscheuer + * @since 0.4.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class AppRole { + @JsonProperty("role_name") + private String name; + + @JsonProperty("role_id") + @JsonInclude(JsonInclude.Include.NON_NULL) + private String id; + + @JsonProperty("bind_secret_id") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Boolean bindSecretId; + + private List boundCidrList; + + private List policies; + + @JsonProperty("secret_id_num_uses") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Integer secretIdNumUses; + + @JsonProperty("secret_id_ttl") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Integer secretIdTtl; + + @JsonProperty("token_ttl") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Integer tokenTtl; + + @JsonProperty("token_max_ttl") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Integer tokenMaxTtl; + + @JsonProperty("period") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Integer period; + + public AppRole() { + + } + + public AppRole(String name, String id, Boolean bindSecretId, List boundCidrList, List policies, Integer secretIdNumUses, Integer secretIdTtl, Integer tokenTtl, Integer tokenMaxTtl, Integer period) { + this.name = name; + this.id = id; + this.bindSecretId = bindSecretId; + this.boundCidrList = boundCidrList; + this.policies = policies; + this.secretIdNumUses = secretIdNumUses; + this.secretIdTtl = secretIdTtl; + this.tokenTtl = tokenTtl; + this.tokenMaxTtl = tokenMaxTtl; + this.period = period; + } + + public String getName() { + return name; + } + + public String getId() { + return id; + } + + public Boolean getBindSecretId() { + return bindSecretId; + } + + public List getBoundCidrList() { + return boundCidrList; + } + + @JsonSetter("bound_cidr_list") + public void setBoundCidrList(List boundCidrList) { + this.boundCidrList = boundCidrList; + } + + @JsonGetter("bound_cidr_list") + public String getBoundCidrListString() { + if (boundCidrList == null || boundCidrList.isEmpty()) + return ""; + return String.join(",", boundCidrList); + } + + public List getPolicies() { + return policies; + } + + @JsonSetter("policies") + public void setPolicies(List policies) { + this.policies = policies; + } + + @JsonGetter("policies") + public String getPoliciesString() { + if (policies == null || policies.isEmpty()) + return ""; + return String.join(",", policies); + } + + public Integer getSecretIdNumUses() { + return secretIdNumUses; + } + + public Integer getSecretIdTtl() { + return secretIdTtl; + } + + public Integer getTokenTtl() { + return tokenTtl; + } + + public Integer getTokenMaxTtl() { + return tokenMaxTtl; + } + + public Integer getPeriod() { + return period; + } +} diff --git a/src/main/java/de/stklcode/jvault/connector/model/AppRoleBuilder.java b/src/main/java/de/stklcode/jvault/connector/model/AppRoleBuilder.java new file mode 100644 index 0000000..fd4102c --- /dev/null +++ b/src/main/java/de/stklcode/jvault/connector/model/AppRoleBuilder.java @@ -0,0 +1,209 @@ +/* + * Copyright 2016 Stefan Kalscheuer + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.stklcode.jvault.connector.model; + +import java.util.ArrayList; +import java.util.List; + +/** + * A builder for vault AppRole roles.. + * + * @author Stefan Kalscheuer + * @since 0.4.0 + */ +public class AppRoleBuilder { + private String name; + private String id; + private Boolean bindSecretId; + private List boundCidrList; + private List policies; + private Integer secretIdNumUses; + private Integer secretIdTtl; + private Integer tokenTtl; + private Integer tokenMaxTtl; + private Integer period; + + public AppRoleBuilder(String name) { + this.name = name; + } + + /** + * Add custom role ID (optional) + * + * @param id the ID + * @return self + */ + public AppRoleBuilder withId(final String id) { + this.id = id; + return this; + } + + /** + * Set if role is bound to secret ID + * + * @param bindSecretId the display name + * @return self + */ + public AppRoleBuilder withBindSecretID(final Boolean bindSecretId) { + this.bindSecretId = bindSecretId; + return this; + } + + /** + * Bind role to secret ID. + * Convenience method for {@link #withBindSecretID(Boolean)} + * + * @return self + */ + public AppRoleBuilder withBindSecretID() { + return withBindSecretID(true); + } + + /** + * Do not bind role to secret ID. + * Convenience method for {@link #withBindSecretID(Boolean)} + * + * @return self + */ + public AppRoleBuilder withoutBindSecretID() { + return withBindSecretID(false); + } + + /** + * Set bound CIDR blocks. + * + * @param boundCidrList List of CIDR blocks which can perform login + * @return self + */ + public AppRoleBuilder withBoundCidrList(final List boundCidrList) { + this.boundCidrList = boundCidrList; + return this; + } + + /** + * Add a CIDR block to list of bound blocks. + * + * @param cidrBlock the CIDR block + * @return self + */ + public AppRoleBuilder withCidrBlock(final String cidrBlock) { + if (boundCidrList == null) + boundCidrList = new ArrayList<>(); + boundCidrList.add(cidrBlock); + return this; + } + + /** + * Add given policies + * + * @param policies the policies + * @return self + */ + public AppRoleBuilder withPolicies(final List policies) { + if (this.policies == null) + this.policies = new ArrayList<>(); + this.policies.addAll(policies); + return this; + } + + /** + * Add a single policy. + * + * @param policy the policy + * @return self + */ + public AppRoleBuilder withPolicy(final String policy) { + if (this.policies == null) + this.policies = new ArrayList<>(); + policies.add(policy); + return this; + } + + /** + * Set number of uses for sectet IDs. + * + * @param secredIdNumUses the number of uses + * @return self + */ + public AppRoleBuilder withSecretIdNumUses(final Integer secredIdNumUses) { + this.secretIdNumUses = secredIdNumUses; + return this; + } + + /** + * Set default sectet ID TTL in seconds. + * + * @param secredIdTtl the TTL + * @return self + */ + public AppRoleBuilder withSecretIdTtl(final Integer secredIdTtl) { + this.secretIdTtl = secredIdTtl; + return this; + } + + /** + * Set default token TTL in seconds. + * + * @param tokenTtl the TTL + * @return self + */ + public AppRoleBuilder withTokenTtl(final Integer tokenTtl) { + this.tokenTtl = tokenTtl; + return this; + } + + /** + * Set maximum token TTL in seconds. + * + * @param tokenMaxTtl the TTL + * @return self + */ + public AppRoleBuilder withTokenMaxTtl(final Integer tokenMaxTtl) { + this.tokenMaxTtl = tokenMaxTtl; + return this; + } + + /** + * Set renewal period for generated token in seconds. + * + * @param period period in seconds + * @return self + */ + public AppRoleBuilder withPeriod(final Integer period) { + this.period = period; + return this; + } + + + /** + * Build the AppRole role based on given parameters. + * + * @return the role + */ + public AppRole build() { + return new AppRole(name, + id, + bindSecretId, + boundCidrList, + policies, + secretIdNumUses, + secretIdTtl, + tokenTtl, + tokenMaxTtl, + period); + } +} diff --git a/src/main/java/de/stklcode/jvault/connector/model/AppRoleSecret.java b/src/main/java/de/stklcode/jvault/connector/model/AppRoleSecret.java new file mode 100644 index 0000000..17f4e4e --- /dev/null +++ b/src/main/java/de/stklcode/jvault/connector/model/AppRoleSecret.java @@ -0,0 +1,121 @@ +/* + * Copyright 2016 Stefan Kalscheuer + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.stklcode.jvault.connector.model; + +import com.fasterxml.jackson.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * Vault AppRole role metamodel. + * + * @author Stefan Kalscheuer + * @since 0.4.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class AppRoleSecret { + @JsonProperty("secret_id") + @JsonInclude(JsonInclude.Include.NON_NULL) + private String id; + + @JsonProperty(value = "secret_id_accessor", access = JsonProperty.Access.WRITE_ONLY) + private String accessor; + + @JsonProperty("metadata") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Map metadata; + + private List cidrList; + + @JsonProperty(value = "creation_time", access = JsonProperty.Access.WRITE_ONLY) + private String creationTime; + + @JsonProperty(value = "expiration_time", access = JsonProperty.Access.WRITE_ONLY) + private String expirationTime; + + @JsonProperty(value = "last_updated_time", access = JsonProperty.Access.WRITE_ONLY) + private String lastUpdatedTime; + + @JsonProperty(value = "secret_id_num_uses", access = JsonProperty.Access.WRITE_ONLY) + private Integer numUses; + + @JsonProperty(value = "secret_id_ttl", access = JsonProperty.Access.WRITE_ONLY) + private Integer ttl; + + public AppRoleSecret() { + + } + + public AppRoleSecret(String id) { + this.id = id; + } + + public AppRoleSecret(String id, Map metadata, List cidrList) { + this.id = id; + this.metadata = metadata; + this.cidrList = cidrList; + } + + public String getId() { + return id; + } + + public String getAccessor() { + return accessor; + } + + public Map getMetadata() { + return metadata; + } + + public List getCidrList() { + return cidrList; + } + + @JsonSetter("cidr_list") + public void setCidrList(List cidrList) { + this.cidrList = cidrList; + } + + @JsonGetter("cidr_list") + public String getCidrListString() { + if (cidrList == null || cidrList.isEmpty()) + return ""; + return String.join(",", cidrList); + } + + public String getCreationTime() { + return creationTime; + } + + public String getExpirationTime() { + return expirationTime; + } + + public String getLastUpdatedTime() { + return lastUpdatedTime; + } + + public Integer getNumUses() { + return numUses; + } + + public Integer getTtl() { + return ttl; + } +} diff --git a/src/main/java/de/stklcode/jvault/connector/model/AuthBackend.java b/src/main/java/de/stklcode/jvault/connector/model/AuthBackend.java index 00ec431..1f0da4b 100644 --- a/src/main/java/de/stklcode/jvault/connector/model/AuthBackend.java +++ b/src/main/java/de/stklcode/jvault/connector/model/AuthBackend.java @@ -25,6 +25,7 @@ package de.stklcode.jvault.connector.model; public enum AuthBackend { TOKEN("token"), APPID("app-id"), + APPROLE("approle"), USERPASS("userpass"), UNKNOWN(""); diff --git a/src/main/java/de/stklcode/jvault/connector/model/response/AppRoleResponse.java b/src/main/java/de/stklcode/jvault/connector/model/response/AppRoleResponse.java new file mode 100644 index 0000000..bc79446 --- /dev/null +++ b/src/main/java/de/stklcode/jvault/connector/model/response/AppRoleResponse.java @@ -0,0 +1,56 @@ +/* + * Copyright 2016 Stefan Kalscheuer + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.stklcode.jvault.connector.model.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.stklcode.jvault.connector.exception.InvalidResponseException; +import de.stklcode.jvault.connector.model.AppRole; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Vault response for AppRole lookup. + * + * @author Stefan Kalscheuer + * @since 0.4.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class AppRoleResponse extends VaultDataResponse { + private AppRole role; + + @Override + public void setData(Map data) throws InvalidResponseException { + ObjectMapper mapper = new ObjectMapper(); + try { + /* null empty strings on list objects */ + Map filteredData = new HashMap<>(); + data.forEach((k,v) -> { if (!(v instanceof String && ((String) v).isEmpty())) filteredData.put(k,v); }); + this.role = mapper.readValue(mapper.writeValueAsString(filteredData), AppRole.class); + } catch (IOException e) { + e.printStackTrace(); + throw new InvalidResponseException(); + } + } + + public AppRole getRole() { + return role; + } +} \ No newline at end of file diff --git a/src/main/java/de/stklcode/jvault/connector/model/response/AppRoleSecretResponse.java b/src/main/java/de/stklcode/jvault/connector/model/response/AppRoleSecretResponse.java new file mode 100644 index 0000000..8951d66 --- /dev/null +++ b/src/main/java/de/stklcode/jvault/connector/model/response/AppRoleSecretResponse.java @@ -0,0 +1,56 @@ +/* + * Copyright 2016 Stefan Kalscheuer + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.stklcode.jvault.connector.model.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.stklcode.jvault.connector.exception.InvalidResponseException; +import de.stklcode.jvault.connector.model.AppRole; +import de.stklcode.jvault.connector.model.AppRoleSecret; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * Vault response for AppRole lookup. + * + * @author Stefan Kalscheuer + * @since 0.4.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class AppRoleSecretResponse extends VaultDataResponse { + private AppRoleSecret secret; + + @Override + public void setData(Map data) throws InvalidResponseException { + ObjectMapper mapper = new ObjectMapper(); + try { + /* null empty strings on list objects */ + Map filteredData = new HashMap<>(); + data.forEach((k,v) -> { if (!(v instanceof String && ((String) v).isEmpty())) filteredData.put(k,v); }); + this.secret = mapper.readValue(mapper.writeValueAsString(filteredData), AppRoleSecret.class); + } catch (IOException e) { + e.printStackTrace(); + throw new InvalidResponseException(); + } + } + + public AppRoleSecret getSecret() { + return secret; + } +} \ No newline at end of file diff --git a/src/main/java/de/stklcode/jvault/connector/model/response/RawDataResponse.java b/src/main/java/de/stklcode/jvault/connector/model/response/RawDataResponse.java new file mode 100644 index 0000000..0bf05e7 --- /dev/null +++ b/src/main/java/de/stklcode/jvault/connector/model/response/RawDataResponse.java @@ -0,0 +1,41 @@ +/* + * Copyright 2016 Stefan Kalscheuer + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.stklcode.jvault.connector.model.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.Map; + +/** + * Simple Vault data response. + * + * @author Stefan Kalscheuer + * @since 0.4.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class RawDataResponse extends VaultDataResponse { + private Map data; + + @Override + public void setData(Map data) { + this.data = data; + } + + public Map getData() { + return data; + } +} diff --git a/src/test/java/de/stklcode/jvault/connector/HTTPVaultConnectorTest.java b/src/test/java/de/stklcode/jvault/connector/HTTPVaultConnectorTest.java index 82be7d8..51c21be 100644 --- a/src/test/java/de/stklcode/jvault/connector/HTTPVaultConnectorTest.java +++ b/src/test/java/de/stklcode/jvault/connector/HTTPVaultConnectorTest.java @@ -17,8 +17,7 @@ package de.stklcode.jvault.connector; import de.stklcode.jvault.connector.exception.InvalidResponseException; -import de.stklcode.jvault.connector.model.Token; -import de.stklcode.jvault.connector.model.TokenBuilder; +import de.stklcode.jvault.connector.model.*; import de.stklcode.jvault.connector.model.response.*; import de.stklcode.jvault.connector.test.Credentials; import de.stklcode.jvault.connector.test.VaultConfiguration; @@ -26,7 +25,6 @@ import de.stklcode.jvault.connector.exception.InvalidRequestException; import de.stklcode.jvault.connector.exception.PermissionDeniedException; import de.stklcode.jvault.connector.exception.VaultConnectorException; import de.stklcode.jvault.connector.factory.VaultConnectorFactory; -import de.stklcode.jvault.connector.model.AuthBackend; import org.junit.*; import org.junit.rules.TemporaryFolder; @@ -35,9 +33,7 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.net.ServerSocket; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.util.*; import static org.hamcrest.Matchers.*; import static org.hamcrest.core.Is.is; @@ -57,6 +53,10 @@ public class HTTPVaultConnectorTest { private static String PASS_VALID = "validPass"; private static String APP_ID = "152AEA38-85FB-47A8-9CBD-612D645BFACA"; private static String USER_ID = "5ADF8218-D7FB-4089-9E38-287465DBF37E"; + private static String APPROLE_ROLE_NAME = "testrole1"; // role with secret ID + private static String APPROLE_ROLE = "627b6400-90c3-a239-49a9-af65a448ca10"; + private static String APPROLE_SECRET = "154fe52a-6df2-b4e9-2dbd-d3c5e6539f9b"; + private static String APPROLE_ROLE2 = "35b7bf43-9644-588a-e68f-2e8313bb23b7"; // role with CIDR subnet private static String SECRET_PATH = "userstore"; private static String SECRET_KEY = "foo"; private static String SECRET_KEY_JSON = "json"; @@ -118,8 +118,8 @@ public class HTTPVaultConnectorTest { } catch (VaultConnectorException e) { fail("Could not list supported auth backends: " + e.getMessage()); } - assertThat(supportedBackends.size(), is(3)); - assertThat(supportedBackends, hasItems(AuthBackend.TOKEN, AuthBackend.USERPASS, AuthBackend.APPID)); + assertThat(supportedBackends, hasSize(4)); + assertThat(supportedBackends, hasItems(AuthBackend.TOKEN, AuthBackend.USERPASS, AuthBackend.APPID, AuthBackend.APPROLE)); } /** @@ -203,6 +203,218 @@ public class HTTPVaultConnectorTest { } } + /** + * App-ID authentication roundtrip. + */ + @Test + public void authAppRole() { + assumeFalse(connector.isAuthorized()); + + /* Authenticate with correct credentials */ + try { + AuthResponse res = connector.authAppRole(APPROLE_ROLE, APPROLE_SECRET); + assertThat("Authorization flag not set after AppRole login.", connector.isAuthorized(), is(true)); + } catch (VaultConnectorException e) { + fail("Failed to authenticate using AppRole: " + e.getMessage()); + } + + /* Authenticate with valid secret ID against unknown role */ + try { + AuthResponse res = connector.authAppRole("foo", APPROLE_SECRET); + fail("Successfully logged in with unknown role"); + } catch (VaultConnectorException e) { + assertThat(e, is(instanceOf(InvalidResponseException.class))); + } + + /* Authenticate without wrong secret ID */ + try { + AuthResponse res = connector.authAppRole(APPROLE_ROLE, "foo"); + fail("Successfully logged in without secret ID"); + } catch (VaultConnectorException e) { + assertThat(e, is(instanceOf(InvalidResponseException.class))); + } + + /* Authenticate without secret ID */ + try { + AuthResponse res = connector.authAppRole(APPROLE_ROLE); + fail("Successfully logged in without secret ID"); + } catch (VaultConnectorException e) { + assertThat(e, is(instanceOf(InvalidResponseException.class))); + } + + /* Authenticate with secret ID on role with CIDR whitelist */ + try { + AuthResponse res = connector.authAppRole(APPROLE_ROLE2, APPROLE_SECRET); + assertThat("Authorization flag not set after AppRole login.", connector.isAuthorized(), is(true)); + } catch (VaultConnectorException e) { + fail("Failed to log in without secret ID"); + } + } + + /** + * Test creation of a new AppRole. + */ + @Test + public void createAppRoleTest() { + authRoot(); + assumeTrue(connector.isAuthorized()); + + String roleName = "TestRole"; + + /* Create role model */ + AppRole role = new AppRoleBuilder(roleName).build(); + + /* Create role */ + try { + boolean res = connector.createAppRole(role); + assertThat("No result given.", res, is(notNullValue())); + } catch (VaultConnectorException e) { + fail("Role creation failed."); + } + + /* Lookup role */ + try { + AppRoleResponse res = connector.lookupAppRole(roleName); + assertThat("Role lookup returned no role.", res.getRole(), is(notNullValue())); + } catch (VaultConnectorException e) { + fail("Role lookup failed."); + } + + /* Lookup role ID */ + try { + String res = connector.getAppRoleID(roleName); + assertThat("Role ID lookup returned empty ID.", res, is(not(emptyString()))); + } catch (VaultConnectorException e) { + fail("Role ID lookup failed."); + } + + /* Set custom role ID */ + String roleID = "custom-role-id"; + try { + connector.setAppRoleID(roleName, roleID); + } catch (VaultConnectorException e) { + fail("Setting custom role ID failed."); + } + + /* Verify role ID */ + try { + String res = connector.getAppRoleID(roleName); + assertThat("Role ID lookup returned wrong ID.", res, is(roleID)); + } catch (VaultConnectorException e) { + fail("Role ID lookup failed."); + } + + /* Create role by name */ + roleName = "RoleByName"; + try { + connector.createAppRole(roleName); + } catch (VaultConnectorException e) { + fail("Creation of role by name failed."); + } + try { + AppRoleResponse res = connector.lookupAppRole(roleName); + assertThat("Role lookuo returned not value", res.getRole(), is(notNullValue())); + } catch (VaultConnectorException e) { + fail("Creation of role by name failed."); + } + + /* Create role by name with custom ID */ + roleName = "RoleByName"; + roleID = "RolyByNameID"; + try { + connector.createAppRole(roleName, roleID); + } catch (VaultConnectorException e) { + fail("Creation of role by name failed."); + } + try { + AppRoleResponse res = connector.lookupAppRole(roleName); + assertThat("Role lookuo returned not value", res.getRole(), is(notNullValue())); + } catch (VaultConnectorException e) { + fail("Creation of role by name failed."); + } + + try { + String res = connector.getAppRoleID(roleName); + assertThat("Role lookuo returned wrong ID", res, is(roleID)); + } catch (VaultConnectorException e) { + fail("Creation of role by name failed."); + } + + /* Create role by name with policies */ + try { + connector.createAppRole(roleName, Collections.singletonList("testpolicy")); + } catch (VaultConnectorException e) { + fail("Creation of role by name failed."); + } + try { + AppRoleResponse res = connector.lookupAppRole(roleName); + assertThat("Role lookuo returned wrong policy count", res.getRole().getPolicies(), hasSize(2)); + assertThat("Role lookuo returned wrong policies", res.getRole().getPolicies(), hasItem("testpolicy")); + } catch (VaultConnectorException e) { + fail("Creation of role by name failed."); + } + + /* Delete role */ + try { + connector.deleteAppRole(roleName); + } catch (VaultConnectorException e) { + fail("Deletion of role failed."); + } + try { + connector.lookupAppRole(roleName); + fail("Deleted role could be looked up."); + } catch (VaultConnectorException e) { + assertThat(e, is(instanceOf(InvalidResponseException.class))); + } + } + + /** + * Test creation of AppRole secrets. + */ + @Test + public void createAppRoleSecretTest() { + authRoot(); + assumeTrue(connector.isAuthorized()); + + /* Create default (random) secret for existing role */ + try { + AppRoleSecretResponse res = connector.createAppRoleSecret(APPROLE_ROLE_NAME); + assertThat("No secret returned", res.getSecret(), is(notNullValue())); + } catch (VaultConnectorException e) { + fail("AppRole secret creation failed."); + } + + /* Create secret with custom ID */ + String secretID = "customSecretId"; + try { + AppRoleSecretResponse res = connector.createAppRoleSecret(APPROLE_ROLE_NAME, secretID); + assertThat("Unexpected secret ID returned", res.getSecret().getId(), is(secretID)); + } catch (VaultConnectorException e) { + fail("AppRole secret creation failed."); + } + + /* Lookup secret */ + try { + AppRoleSecretResponse res = connector.lookupAppRoleSecret(APPROLE_ROLE_NAME, secretID); + assertThat("No secret information returned", res.getSecret(), is(notNullValue())); + } catch (VaultConnectorException e) { + fail("AppRole secret lookup failed."); + } + + /* Destroy secret */ + try { + connector.destroyAppRoleSecret(APPROLE_ROLE_NAME, secretID); + } catch (VaultConnectorException e) { + fail("AppRole secret destruction failed."); + } + try { + AppRoleSecretResponse res = connector.lookupAppRoleSecret(APPROLE_ROLE_NAME, secretID); + fail("Destroyed AppRole secret successfully read."); + } catch (VaultConnectorException e) { + assertThat(e, is(instanceOf(InvalidResponseException.class))); + } + } + /** * Test reading of secrets. */ diff --git a/src/test/java/de/stklcode/jvault/connector/model/AppRoleBuilderTest.java b/src/test/java/de/stklcode/jvault/connector/model/AppRoleBuilderTest.java new file mode 100644 index 0000000..3399390 --- /dev/null +++ b/src/test/java/de/stklcode/jvault/connector/model/AppRoleBuilderTest.java @@ -0,0 +1,148 @@ +/* + * Copyright 2016 Stefan Kalscheuer + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.stklcode.jvault.connector.model; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.*; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; + +/** + * JUnit Test for AppRole Builder. + * + * @author Stefan Kalscheuer + * @since 0.4.0 + */ +public class AppRoleBuilderTest { + + + private static final String NAME = "TestRole"; + private static final String ID = "test-id"; + private static final Boolean BIND_SECRET_ID = true; + private static final List BOUND_CIDR_LIST = new ArrayList<>(); + private static final String CIDR_1 = "192.168.1.0/24"; + private static final String CIDR_2 = "172.16.0.0/16"; + private static final List POLICIES = new ArrayList<>(); + private static final String POLICY = "policy"; + private static final String POLICY_2 = "policy2"; + private static final Integer SECRET_ID_NUM_USES = 10; + private static final Integer SECRET_ID_TTL = 7200; + private static final Integer TOKEN_TTL = 4800; + private static final Integer TOKEN_MAX_TTL = 9600; + private static final Integer PERIOD = 1234; + private static final String JSON_MIN = "{\"role_name\":\"" + NAME + "\"}"; + private static final String JSON_FULL = String.format("{\"role_name\":\"%s\",\"secret_id\":\"%s\",\"bind_secret_id\":%s,\"bound_cidr_list\":[\"%s\"],\"policies\":[\"%s\"],\"secret_id_num_uses\":%d,\"secret_id_ttl\":%d,\"token_ttl\":%d,\"token_max_ttl\":%d,\"period\":%d}", + NAME, ID, BIND_SECRET_ID, CIDR_1, POLICY, SECRET_ID_NUM_USES, SECRET_ID_TTL, TOKEN_TTL, TOKEN_MAX_TTL, PERIOD); + + @BeforeClass + public static void init() { + BOUND_CIDR_LIST.add(CIDR_1); + POLICIES.add(POLICY); + } + + /** + * Build role with only a name. + */ + @Test + public void buildDefaultTest() throws JsonProcessingException { + AppRole role = new AppRoleBuilder(NAME).build(); + assertThat(role.getId(), is(nullValue())); + assertThat(role.getBindSecretId(), is(nullValue())); + assertThat(role.getBoundCidrList(), is(nullValue())); + assertThat(role.getPolicies(), is(nullValue())); + assertThat(role.getSecretIdNumUses(), is(nullValue())); + assertThat(role.getSecretIdTtl(), is(nullValue())); + assertThat(role.getTokenTtl(), is(nullValue())); + assertThat(role.getTokenMaxTtl(), is(nullValue())); + assertThat(role.getPeriod(), is(nullValue())); + + /* optional fields should be ignored, so JSON string should only contain role_name */ + assertThat(new ObjectMapper().writeValueAsString(role), is(JSON_MIN)); + } + + /** + * Build token without all parameters set. + */ + @Test + public void buildFullTest() throws JsonProcessingException { + AppRole role = new AppRoleBuilder(NAME) + .withId(ID) + .withBindSecretID(BIND_SECRET_ID) + .withBoundCidrList(BOUND_CIDR_LIST) + .withPolicies(POLICIES) + .withSecretIdNumUses(SECRET_ID_NUM_USES) + .withSecretIdTtl(SECRET_ID_TTL) + .withTokenTtl(TOKEN_TTL) + .withTokenMaxTtl(TOKEN_MAX_TTL) + .withPeriod(PERIOD) + .build(); + assertThat(role.getName(), is(NAME)); + assertThat(role.getId(), is(ID)); + assertThat(role.getBindSecretId(), is(BIND_SECRET_ID)); + assertThat(role.getBoundCidrList(), is(BOUND_CIDR_LIST)); + assertThat(role.getPolicies(), is(POLICIES)); + assertThat(role.getSecretIdNumUses(), is(SECRET_ID_NUM_USES)); + assertThat(role.getSecretIdTtl(), is(SECRET_ID_TTL)); + assertThat(role.getTokenTtl(), is(TOKEN_TTL)); + assertThat(role.getTokenMaxTtl(), is(TOKEN_MAX_TTL)); + assertThat(role.getPeriod(), is(PERIOD)); + + /* Verify that all parameters are included in JSON string */ + assertThat(new ObjectMapper().writeValueAsString(role), is(JSON_FULL)); + } + + /** + * Test convenience methods + */ + @Test + public void convenienceMethodsTest() { + /* bind_secret_id */ + AppRole role = new AppRoleBuilder(NAME).build(); + assertThat(role.getBindSecretId(), is(nullValue())); + role = new AppRoleBuilder(NAME).withBindSecretID().build(); + assertThat(role.getBindSecretId(), is(true)); + role = new AppRoleBuilder(NAME).withoutBindSecretID().build(); + assertThat(role.getBindSecretId(), is(false)); + + /* Add single CIDR subnet */ + role = new AppRoleBuilder(NAME).withCidrBlock(CIDR_2).build(); + assertThat(role.getBoundCidrList(), hasSize(1)); + assertThat(role.getBoundCidrList(), contains(CIDR_2)); + role = new AppRoleBuilder(NAME) + .withPolicies(BOUND_CIDR_LIST) + .withPolicy(CIDR_1) + .build(); + assertThat(role.getBoundCidrList(), hasSize(2)); + assertThat(role.getBoundCidrList(), contains(CIDR_1, CIDR_2)); + + /* Add single policy */ + role = new AppRoleBuilder(NAME).withPolicy(POLICY_2).build(); + assertThat(role.getPolicies(), hasSize(1)); + assertThat(role.getPolicies(), contains(POLICY_2)); + role = new AppRoleBuilder(NAME) + .withPolicies(POLICIES) + .withPolicy(POLICY_2) + .build(); + assertThat(role.getPolicies(), hasSize(2)); + assertThat(role.getPolicies(), contains(POLICY, POLICY_2)); + } +} diff --git a/src/test/resources/data_dir/auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/_salt b/src/test/resources/data_dir/auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/_salt new file mode 100644 index 0000000..91515d5 --- /dev/null +++ b/src/test/resources/data_dir/auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/_salt @@ -0,0 +1 @@ +{"Key":"auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/salt","Value":"AAAAAQKDLmmb/XlhfVJ45oKGyYwneS9s3tcQUenB8bTcxuDmAMUWnwG8oNNJFs0mSCF9Yv1KOq3Twxj4qPp05viFnP0z"} diff --git a/src/test/resources/data_dir/auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/accessor/_da42ddc9a483efd8ddeae4ab38428f73d42ad7f6320705f333555fed8593cbe2 b/src/test/resources/data_dir/auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/accessor/_da42ddc9a483efd8ddeae4ab38428f73d42ad7f6320705f333555fed8593cbe2 new file mode 100644 index 0000000..0996eca --- /dev/null +++ b/src/test/resources/data_dir/auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/accessor/_da42ddc9a483efd8ddeae4ab38428f73d42ad7f6320705f333555fed8593cbe2 @@ -0,0 +1 @@ +{"Key":"auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/accessor/da42ddc9a483efd8ddeae4ab38428f73d42ad7f6320705f333555fed8593cbe2","Value":"AAAAAQLCu78fbRRgGWG++5XDCfaO/8NTg7LMAJL7aCsrn6c1WHJ5yrAAmWmSs1euhNd7yKUd0lQ0aknCKdPAZFBlAsqgOdnN8JLFe/H9lISaWdU6lRIfgTH9whEXWT0VK25FcS4r5yVe3Qoxg0DfT8FhjuzOa70="} diff --git a/src/test/resources/data_dir/auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/role/_testrole1 b/src/test/resources/data_dir/auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/role/_testrole1 new file mode 100644 index 0000000..532122d --- /dev/null +++ b/src/test/resources/data_dir/auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/role/_testrole1 @@ -0,0 +1 @@ +{"Key":"auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/role/testrole1","Value":"AAAAAQLyV03lH8m3IYxoZKLf+/suZ+2wwKAyIHqrR3QeJZK+68wslLXy0XZ35bPrdc3jzAFhTizqILlgTBHVccdM/pydtTtbsvGHQlWstLaC79GUTM32gS/jwSrbwfa9j0q/Yrdo2LSa9IM5lw2tmYy+xR9c3ZKcm+VADZMZy3+6UmbQ1t0lniZ4uuVmqu2gl3y0732UtdMSxJepPWMjfvVq5+tynhgvEZNGgZCPc9lsV1fcBVFswtBUeATNnSJPmTnxQflXyhitPOpEM+5L+gnEsSNsyinRjv5cSbIHCP5yDzvpiWtwZ5Q0psVRSh/WJppBHcovwbJsTLK/tZ1wtFl1OgU9NLONEpgDJYiDyU0ACeFJ7r+DhjIDrQkr+WITnfBBwI+65wpOPYboqGgd4qZy84PE2s/VhWS5hjpxgpM="} diff --git a/src/test/resources/data_dir/auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/role/_testrole2 b/src/test/resources/data_dir/auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/role/_testrole2 new file mode 100644 index 0000000..2189ec6 --- /dev/null +++ b/src/test/resources/data_dir/auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/role/_testrole2 @@ -0,0 +1 @@ +{"Key":"auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/role/testrole2","Value":"AAAAAQIA7g8ifdb9dcRQtIagNGpu2Miv6Dy4jBif1J9OZd26AgFDL6eZTrDr3FfmUQQUs/izDlfI9FDB+UJZO6P2B6vkTchwSg0JdOD8lHjtuoCSDKrIPmzallXHFGwnMnzFY80JzNlzUEfbzciExXthpUjlvBoMlHydZPtAn3pL2NkJdwW1dDRARGB9RoWguqYVgCMkOVdpLFYMVSN1nyHvlt2sm3IdwsXxlE9kH1HGiIEKWYX1U0l8uM0NJTZPFo8Km09u9sz/yzS9B+cyIKXaom7h7S53yRyGP7rFZObl3INMloJyJn7+XxpqiZYAiK31tToZ9k4Y3Eez1ZyCj+oujM6MDwnnzlAkOm3nptIySOk9+iEehr6rG5fpt3WTVSEC3f+1Q+4S"} diff --git a/src/test/resources/data_dir/auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/role_id/_b2b271423a16ba322c3f87616230f8ced5e89bc8d1a32f0ce91c3d3b5f264a8a b/src/test/resources/data_dir/auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/role_id/_b2b271423a16ba322c3f87616230f8ced5e89bc8d1a32f0ce91c3d3b5f264a8a new file mode 100644 index 0000000..c1bae62 --- /dev/null +++ b/src/test/resources/data_dir/auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/role_id/_b2b271423a16ba322c3f87616230f8ced5e89bc8d1a32f0ce91c3d3b5f264a8a @@ -0,0 +1 @@ +{"Key":"auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/role_id/b2b271423a16ba322c3f87616230f8ced5e89bc8d1a32f0ce91c3d3b5f264a8a","Value":"AAAAAQJhm8OnoLuGdSqb3GhF36ALFfIdoRHQ0SMaC1CAuhlfgzuPcyZFMgHr7IL1UepjItfW"} diff --git a/src/test/resources/data_dir/auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/role_id/_fb5542bdc4127acea06e585384296c607d18d139be530ac52f850b703b22bcb4 b/src/test/resources/data_dir/auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/role_id/_fb5542bdc4127acea06e585384296c607d18d139be530ac52f850b703b22bcb4 new file mode 100644 index 0000000..295bf84 --- /dev/null +++ b/src/test/resources/data_dir/auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/role_id/_fb5542bdc4127acea06e585384296c607d18d139be530ac52f850b703b22bcb4 @@ -0,0 +1 @@ +{"Key":"auth/ac4e0527-a7b2-1b40-1148-dc0dfaf01990/role_id/fb5542bdc4127acea06e585384296c607d18d139be530ac52f850b703b22bcb4","Value":"AAAAAQJMBpriwrK36PPIVHKh4hNEU66EXyp5npyEF3JxUD0BTQW/vQC6hrnDnSF9F59Xh1Ut"} diff --git a/src/test/resources/data_dir/core/_auth b/src/test/resources/data_dir/core/_auth index bf42fd1..479c32c 100644 --- a/src/test/resources/data_dir/core/_auth +++ b/src/test/resources/data_dir/core/_auth @@ -1 +1 @@ -{"Key":"core/auth","Value":"AAAAAQKA3+V0TsgRYXO8NxjN1y6nBjUL9B2eOJe4Cfi56B3qmzGx1mhyn1SRkyWBbRIjIOp/a8d52P5ukK3AU3FpOC8w8W/aYLZVZONhMn4tnzvN9+FDYp5Kx8FqNBunDgOwygkMsgd0fnrAgxefUU1z4iY5hlXl/45qwWE6HXbrCJ5uBciHZajmKbCZPe9oc/i0Lw0hZXW598kJV6OmzGesWqYmkbyohv5d1vTB1nuDNG5MvxtWoNIcF5u5+x487zg0FQ6womZOyR7xQKxXyIYescIjAmSjJ6Xlr0rj41NSCzMIhP6fkDHI+YiPAcXDHynjMRV4rky9PG5PSJQgp1jYUUjo/3crc1ssBMSPTEecdc9xOyS903o2/fvc9aFm0CBOtlrlbLzzEztIrHPwJAkCoyqgpk/LcJgV6DRM8xmy+MDTNBFnLhzTeKyK2z7c932JYHTNO8YiIzmc8xAOY46YMEofGu0dVi6QTtfxIs3934NoO5p+EM0RcBnLme8FVWr+QsyL64SFJ3sF7t9WuwRccLTpJaQm5r7YsUFSOZSczrMkWAPIyJFprCA678HVgpVUUOeA1vC96am7+IybvOD8n7Hwu8PCKoXUStsJya+WR1gZ3zjd6zN9byibve6MXxMvYpFp4zv4l2whavvPzO5sCM6xookFvhUVZPvcTrQIhxSoTLabJClss4VJKZoV6HK4m4JZcgKXAVuHBf4WHDV+vFPnc5IITneMR1I2GYJPaOlhDlB2MO0WbCBWDc+EhuelmuiUdZFT3u4zshaODpPzvVzWwbWwIQyFfL7dM3ZCiIGS9Bo6pAj6b8zsjg=="} +{"Key":"core/auth","Value":"AAAAAQKy/0yKRpmA5LiZxPlXV+5jBS7lsbsXcACqT/BmXs0nsEP0oHLb4423GWadgYKg1+Vow5qNcLgc6JufLVkEz4dnAcTU1plaCYhRxSGrO/rlAIsKTgwFkKYnV2YRmiSjxKT8hnWw1SHmxappAhZYDMvVVrE9BYGmrCXSmeopjIPLYWgO7UB9/jda7bzMQx9mDVDRUBhRRi4vXUYgo3Dwm1Dznh2yv20ts7O5BVBtpncXB/RovRRhvh2bn/H+RfE0NuZRhQnZ9S6ATF3kGmJX33OiTmBqHBaS8cvPPSZHWmEcXnM8bHEjYxUNZB6+RattgGYVfGtujKWHfwfwJ96VlfIXbQCoZ7bIa1czNa+xhuoOKvaV70crYT4drFnnu4RoV3+HqZl7qsmS8o4MJINoCO6All6GfK7lsRivVzPIZ9M4H6towxfBvEcDvTDHZlL5pOVp7yPP2PSTs0wHM/e9Gg2x9j7nNSDw2KWKTMrxvxHb53dZ3TaaTB388oylLoaNUTG3PlnKAwB4yMp7OpapWH1qX0oo1k3M+XzOX3krHD9UImCTN5DnNZ7jKf8MJ/0aoDi37lNLvQ+0dEXrhsnsRodb2mnCVbp0UhQpPp+Y3yLnB9zBYVAD8Dnt0hOgsxz3WcFOwNX2zeT7OgGnZlTP+NtKKVfXHYjh18JS7mypA7hkh1scBBLqwMyXpzWkKwnBrJYMM1Le1ht2erJvJ4hmLdISidQ8K7ULcS2Gjht+W83ITqC2fO4NIL8QwP9gk6QyIdC8BSHy0mOlZTkT0m5rnVnvl8CV2BbX0lUKTFSmEz6Z0L4nS0/obBRIrWgKgDw3jqnq6vcvmIH4xaOTfRWUeNMQiFnXEsj6tZvRSdpYmAv9jDbdWEBCA2gFkA8wxZ+MSgRIZE8ETUt//5gcvbLncBadS+d7DcqicXUje6jHCQbPg/j9zK38/cKGIszbqVavb89tf0PEQDJB4HOPiPs5kij3T8GtxgFW1DiT23TvtubQF0XsMnv9N3avwTeJoVtBwMTrO1mIam5EHqmvtftnTQycPg9MURmw54fpUevv/Og="} diff --git a/src/test/resources/data_dir/core/cluster/local/_info b/src/test/resources/data_dir/core/cluster/local/_info new file mode 100644 index 0000000..f50d389 --- /dev/null +++ b/src/test/resources/data_dir/core/cluster/local/_info @@ -0,0 +1 @@ +{"Key":"core/cluster/local/info","Value":"AAAAAQLM3HpjYNukfnGPRBV4yhHkaDqTMag0rk3fDmRcUt5pzDH0yh/2ZRuF7czOYsfV6146nxmUfekMrH457GtCMPIppE4x0WkikQRxiA5fSw9lYT8yeErVL1o7ETNo3AHPib2ldxBdX8ik3jY="} diff --git a/src/test/resources/data_dir/sys/policy/_response-wrapping b/src/test/resources/data_dir/sys/policy/_response-wrapping index c489342..362e3a2 100644 --- a/src/test/resources/data_dir/sys/policy/_response-wrapping +++ b/src/test/resources/data_dir/sys/policy/_response-wrapping @@ -1 +1 @@ -{"Key":"sys/policy/response-wrapping","Value":"AAAAAQLDl3zy1uKv9o2NhIyl43YAtoxGChOUc4aMa7beod+3e8FkdOsZt9BIirHsqjJ+VoxQyz+HroBaNfKPsyos3WLWvz5IUZ1UHr/jLG2SjrJfCKvco85RsFytkzp3T+Z5JB2vVfm22PpBIbjq2+XpHLKIqARqTWYl7Wnql572JZOvPY0w"} +{"Key":"sys/policy/response-wrapping","Value":"AAAAAQI0PU/pu6EEHcT4HwfZjzScyW8DLBzmGDanjLuWqGEtlLcKgRLZh7/c/CRWwbRXy2d3GUB1Bo3YVzpUuDDlNY3NaipcORS3zzCHep5uO/DFUJ3DPSlde8j1BrmSpQDHerAsJYXYEManr93puObYs1cEfP9Mt8WdC/IPhgecSw32tVGBz0SSP2qaXGwdJQva6xroMWqmwMVU/lsVi/qcV459xXiTYU/8Kp6Xbqx0p0SRR0yVdM+yNiMtYtnzoxWdptbSYLTG1mhumA=="} diff --git a/src/test/resources/data_dir/sys/token/id/_05b3023411dd89a9a27282d57d027f5312be4adc b/src/test/resources/data_dir/sys/token/id/_05b3023411dd89a9a27282d57d027f5312be4adc index 04b4b69..4ffdaa1 100644 --- a/src/test/resources/data_dir/sys/token/id/_05b3023411dd89a9a27282d57d027f5312be4adc +++ b/src/test/resources/data_dir/sys/token/id/_05b3023411dd89a9a27282d57d027f5312be4adc @@ -1 +1 @@ -{"Key":"sys/token/id/05b3023411dd89a9a27282d57d027f5312be4adc","Value":"AAAAAQIUAO6ILG2gwWjc+J2kp4n03Rp6ycNYAWBYEM0ygocB7DmIT531H5cLbqFVJF5Zw+OQie0HOVLX/zcAPWtxkTOIRXH9FIUT14T9k1IzoxEOYSrbI6ig8bFffe4cd9b0qj9UKgwakQ1GG8vfeSXZnJjVBCSsvWL46s/IGh+SEmirNTiGSE3iy8p3+zunl2s/mUP08i/We03LcLTsCBfSsHVa/CongLNKgSq4oF23LFxv3De+9j8+IQ9HKA0pAatTaCjHdU1TsAiBGsKUhujGW5oQuygkUYVIBFqFqwDOytpdcxiP/A2LAut6qvQjvfT7s7C/Cvke9ypOQAr7iSmUlAhKcXPPEN21NdBmFq4K4w=="} +{"Key":"sys/token/id/05b3023411dd89a9a27282d57d027f5312be4adc","Value":"AAAAAQKiu1H1ntud1j4D5e/ZkrSbjuiQgzXK2/b+chRAVynYtfOSrY4pz4BYwZ31OU/VFdsL66Em2FLgGQDVWi6IdM9d3ao7i+EkRf842PAgKrX99coubFB4QBVHqyKhMwfDUmzflirVgSKy4IgKDVKkR08Z6ETHOGBs4Rc9c97pwYsXJP9OE8dSass3jXVLADKCe+MWJeqv1iKTAQSWlWxa75VNXNYiTYcVQ9LYS3egvDeMOqHWBICnoQGnjaHV9Yz/GmCT5YvqwZL+ZOYvy/DwlyFfr6XIWsrbpVOELWU+SPJCvVriE8E67mbDqthh0I1Du3FoE2AJl+5bIEXQIMlezWTLJN7DdnEnTCWssOdEE6LBz8Ue3o2yFe82HS8mucJECOLIjGuEm0aLAXrKzC7RlFOvkl7q0BNu+AQbg5tAe4PUBrFf+cdMdQ81FxNOOAmrjByhnnLCT8ASEd4Ugv3N1PafZtiZQ4ks8U0ppvqdFWgjnlw="}