From 3afbdd3ea3d8eb1ef339f2c0c46fb57783f4b79b Mon Sep 17 00:00:00 2001 From: Sebastian Laskawiec Date: Tue, 6 Aug 2019 11:45:56 +0200 Subject: [PATCH] KEYCLOAK-10934 PlainTextVaultProvider --- .../keycloak/vault/DefaultVaultRawSecret.java | 1 + .../vault/PlainTextVaultProvider.java | 80 + .../vault/PlainTextVaultProviderFactory.java | 77 + .../vault/VaultNotFoundException.java | 18 + .../org.keycloak.vault.VaultProviderFactory | 1 + .../PlainTextVaultProviderFactoryTest.java | 1332 +++++++++++++++++ .../vault/PlainTextVaultProviderTest.java | 134 ++ .../java/org/keycloak/vault/Scenario.java | 30 + .../org/keycloak/vault/SecretContains.java | 34 + services/src/test/resources/log4j.properties | 5 + .../vault/test__realm_underscore__key1 | 1 + .../resources/org/keycloak/vault/test_key1 | 1 + 12 files changed, 1714 insertions(+) create mode 100644 services/src/main/java/org/keycloak/vault/PlainTextVaultProvider.java create mode 100644 services/src/main/java/org/keycloak/vault/PlainTextVaultProviderFactory.java create mode 100644 services/src/main/java/org/keycloak/vault/VaultNotFoundException.java create mode 100644 services/src/main/resources/META-INF/services/org.keycloak.vault.VaultProviderFactory create mode 100644 services/src/test/java/org/keycloak/vault/PlainTextVaultProviderFactoryTest.java create mode 100644 services/src/test/java/org/keycloak/vault/PlainTextVaultProviderTest.java create mode 100644 services/src/test/java/org/keycloak/vault/Scenario.java create mode 100644 services/src/test/java/org/keycloak/vault/SecretContains.java create mode 100755 services/src/test/resources/log4j.properties create mode 100644 services/src/test/resources/org/keycloak/vault/test__realm_underscore__key1 create mode 100644 services/src/test/resources/org/keycloak/vault/test_key1 diff --git a/services/src/main/java/org/keycloak/vault/DefaultVaultRawSecret.java b/services/src/main/java/org/keycloak/vault/DefaultVaultRawSecret.java index 1c18378a28..7528b8c70b 100644 --- a/services/src/main/java/org/keycloak/vault/DefaultVaultRawSecret.java +++ b/services/src/main/java/org/keycloak/vault/DefaultVaultRawSecret.java @@ -60,5 +60,6 @@ public class DefaultVaultRawSecret implements VaultRawSecret { if (rawSecret.hasArray()) { ThreadLocalRandom.current().nextBytes(rawSecret.array()); } + rawSecret.clear(); } } diff --git a/services/src/main/java/org/keycloak/vault/PlainTextVaultProvider.java b/services/src/main/java/org/keycloak/vault/PlainTextVaultProvider.java new file mode 100644 index 0000000000..5694503793 --- /dev/null +++ b/services/src/main/java/org/keycloak/vault/PlainTextVaultProvider.java @@ -0,0 +1,80 @@ +package org.keycloak.vault; + +import org.jboss.logging.Logger; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.EnumSet; +import java.util.Optional; + +/** + * A text-based vault provider, which stores each secret in a separate file. The file name needs to match a + * vault secret id (or a key for short). A typical vault directory layout looks like this: + *
+ *     ${VAULT}/realma__key1 (contains secret for key 1)
+ *     ${VAULT}/realma__key2 (contains secret for key 2)
+ *     etc...
+ * 
+ * Note, that each key needs is prefixed by realm name. This kind of layout is used by Kubernetes by default + * (when mounting a volume into the pod). + * + * See https://kubernetes.io/docs/concepts/configuration/secret/ + * See https://github.com/keycloak/keycloak-community/blob/master/design/secure-credentials-store.md#plain-text-file-per-secret-kubernetes--openshift + * + * @author Sebastian Łaskawiec + */ +public class PlainTextVaultProvider implements VaultProvider { + + private static final Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass()); + + private final Path vaultPath; + private final String realmName; + + /** + * Creates a new {@link PlainTextVaultProvider}. + * + * @param path A path to a vault. Can not be null. + * @param realmName A realm name. Can not be null. + */ + public PlainTextVaultProvider(@Nonnull Path path, @Nonnull String realmName) { + this.vaultPath = path; + this.realmName = realmName; + logger.debugf("PlainTextVaultProvider will operate in %s directory", vaultPath.toAbsolutePath()); + } + + @Override + public VaultRawSecret obtainSecret(String vaultSecretId) { + Path secretPath = resolveSecretPath(vaultSecretId); + if (!Files.exists(secretPath)) { + return DefaultVaultRawSecret.forBuffer(Optional.empty()); + } + + try (FileChannel fileChannel = (FileChannel) Files.newByteChannel(secretPath, EnumSet.of(StandardOpenOption.READ))) { + MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size()); + return DefaultVaultRawSecret.forBuffer(Optional.of(mappedByteBuffer)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void close() { + + } + + /** + * A method that resolves the exact secret location. + * + * @param vaultSecretId Secret ID. + * @return Path for the secret. + */ + protected Path resolveSecretPath(String vaultSecretId) { + return vaultPath.resolve(realmName.replaceAll("_", "__") + "_" + vaultSecretId.replaceAll("_", "__")); + } +} diff --git a/services/src/main/java/org/keycloak/vault/PlainTextVaultProviderFactory.java b/services/src/main/java/org/keycloak/vault/PlainTextVaultProviderFactory.java new file mode 100644 index 0000000000..2a12761658 --- /dev/null +++ b/services/src/main/java/org/keycloak/vault/PlainTextVaultProviderFactory.java @@ -0,0 +1,77 @@ +package org.keycloak.vault; + +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +import java.lang.invoke.MethodHandles; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Creates and configures {@link PlainTextVaultProvider}. + * + * @author Sebastian Łaskawiec + */ +public class PlainTextVaultProviderFactory implements VaultProviderFactory { + + private static final Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass()); + + public static final String PROVIDER_ID = "plaintext"; + + private String vaultDirectory; + private boolean disabled; + private Path vaultPath; + + @Override + public VaultProvider create(KeycloakSession session) { + if (disabled || vaultDirectory == null) { + //init method not called? + throw new IllegalStateException("Can not create a vault since it's disabled or not initialized correctly"); + } + return new PlainTextVaultProvider(vaultPath, session.getContext().getRealm().getName()); + } + + @Override + public void init(Config.Scope config) { + vaultDirectory = config.get("dir"); + + Boolean disabledFromConfig = config.getBoolean("disabled"); + if (disabledFromConfig == null) { + disabled = false; + } else { + disabled = disabledFromConfig.booleanValue(); + } + + if (disabled) { + logger.debug("PlainTextVaultProviderFactory disabled"); + return; + } + + if (vaultDirectory == null) { + logger.debug("PlainTextVaultProviderFactory not configured"); + return; + } + + vaultPath = Paths.get(vaultDirectory); + if (!Files.exists(vaultPath)) { + throw new VaultNotFoundException("The " + vaultPath.toAbsolutePath().toString() + " directory doesn't exist"); + } + logger.debugf("Configured PlainTextVaultProviderFactory with directory %s", vaultPath.toString()); + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return PROVIDER_ID; + } +} diff --git a/services/src/main/java/org/keycloak/vault/VaultNotFoundException.java b/services/src/main/java/org/keycloak/vault/VaultNotFoundException.java new file mode 100644 index 0000000000..14a7ccd5e5 --- /dev/null +++ b/services/src/main/java/org/keycloak/vault/VaultNotFoundException.java @@ -0,0 +1,18 @@ +package org.keycloak.vault; + +/** + * Thrown when a vault directory doesn't exist. + * + * @author Sebastian Łaskawiec + */ +public class VaultNotFoundException extends RuntimeException { + + /** + * Constructs new exception. + * + * @param message A full text message of the exception. + */ + public VaultNotFoundException(String message) { + super(message); + } +} diff --git a/services/src/main/resources/META-INF/services/org.keycloak.vault.VaultProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.vault.VaultProviderFactory new file mode 100644 index 0000000000..d3331b2ab4 --- /dev/null +++ b/services/src/main/resources/META-INF/services/org.keycloak.vault.VaultProviderFactory @@ -0,0 +1 @@ +org.keycloak.vault.PlainTextVaultProviderFactory \ No newline at end of file diff --git a/services/src/test/java/org/keycloak/vault/PlainTextVaultProviderFactoryTest.java b/services/src/test/java/org/keycloak/vault/PlainTextVaultProviderFactoryTest.java new file mode 100644 index 0000000000..f09bb1743e --- /dev/null +++ b/services/src/test/java/org/keycloak/vault/PlainTextVaultProviderFactoryTest.java @@ -0,0 +1,1332 @@ +package org.keycloak.vault; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.keycloak.Config; +import org.keycloak.common.enums.SslRequired; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.AuthenticationFlowModel; +import org.keycloak.models.AuthenticatorConfigModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientScopeModel; +import org.keycloak.models.GroupModel; +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.OTPPolicy; +import org.keycloak.models.PasswordPolicy; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RequiredActionProviderModel; +import org.keycloak.models.RequiredCredentialModel; +import org.keycloak.models.RoleModel; +import org.keycloak.services.DefaultKeycloakSession; +import org.keycloak.services.DefaultKeycloakSessionFactory; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertNotNull; + +/** + * Tests for {@link PlainTextVaultProviderFactory}. + * + * @author Sebastian Łaskawiec + */ +public class PlainTextVaultProviderFactoryTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void shouldInitializeVaultCorrectly() { + //given + VaultConfig config = new VaultConfig(Scenario.EXISTING.getAbsolutePathAsString(), Boolean.FALSE); + PlainTextVaultProviderFactory factory = new PlainTextVaultProviderFactory(); + + KeycloakSession session = new DefaultKeycloakSession(new DefaultKeycloakSessionFactory()); + session.getContext().setRealm(new VaultRealmModel()); + + //when + factory.init(config); + VaultProvider provider = factory.create(session); + + //then + assertNotNull(provider); + } + + @Test + public void shouldInitializeCorrectlyWithNullDisabledFlag() { + //given + VaultConfig config = new VaultConfig(Scenario.EXISTING.getAbsolutePathAsString(), null); + PlainTextVaultProviderFactory factory = new PlainTextVaultProviderFactory(); + + KeycloakSession session = new DefaultKeycloakSession(new DefaultKeycloakSessionFactory()); + session.getContext().setRealm(new VaultRealmModel()); + + //when + factory.init(config); + VaultProvider provider = factory.create(session); + + //then + assertNotNull(provider); + } + + @Test + public void shouldThrowAnExceptionWhenTryingToCreateProviderOnDisabledFactory() { + //given + VaultConfig config = new VaultConfig(Scenario.EXISTING.getAbsolutePathAsString(), Boolean.TRUE); + PlainTextVaultProviderFactory factory = new PlainTextVaultProviderFactory(); + + expectedException.expect(IllegalStateException.class); + + //when + factory.init(config); + factory.create(null); + + //then - verified by the ExpectedException rule + } + + @Test + public void shouldThrowAnExceptionWhenUsingNonExistingDirectory() { + //given + VaultConfig config = new VaultConfig(Scenario.NON_EXISTING.getAbsolutePathAsString(), Boolean.FALSE); + PlainTextVaultProviderFactory factory = new PlainTextVaultProviderFactory(); + + expectedException.expect(VaultNotFoundException.class); + + //when + factory.init(config); + + //then - verified by the ExpectedException rule + } + + @Test + public void shouldThrowAnExceptionWhenWithNullDirectory() { + //given + VaultConfig config = new VaultConfig(null, Boolean.FALSE); + PlainTextVaultProviderFactory factory = new PlainTextVaultProviderFactory(); + + expectedException.expect(IllegalStateException.class); + + //when + factory.init(config); + factory.create(null); + + //then - verified by the ExpectedException rule + } + + /** + * A whitebox implementation of the Realm model, which is needed to extract realm name. + * Please use only for testing {@link PlainTextVaultProviderFactory}. + */ + private static class VaultRealmModel implements RealmModel { + + @Override + public String getId() { + return null; + } + + @Override + public RoleModel getRole(String name) { + return null; + } + + @Override + public RoleModel addRole(String name) { + return null; + } + + @Override + public RoleModel addRole(String id, String name) { + return null; + } + + @Override + public boolean removeRole(RoleModel role) { + return false; + } + + @Override + public Set getRoles() { + return null; + } + + @Override + public List getDefaultRoles() { + return null; + } + + @Override + public void addDefaultRole(String name) { + + } + + @Override + public void updateDefaultRoles(String... defaultRoles) { + + } + + @Override + public void removeDefaultRoles(String... defaultRoles) { + + } + + @Override + public String getName() { + return "test"; + } + + @Override + public void setName(String name) { + + } + + @Override + public String getDisplayName() { + return null; + } + + @Override + public void setDisplayName(String displayName) { + + } + + @Override + public String getDisplayNameHtml() { + return null; + } + + @Override + public void setDisplayNameHtml(String displayNameHtml) { + + } + + @Override + public boolean isEnabled() { + return false; + } + + @Override + public void setEnabled(boolean enabled) { + + } + + @Override + public SslRequired getSslRequired() { + return null; + } + + @Override + public void setSslRequired(SslRequired sslRequired) { + + } + + @Override + public boolean isRegistrationAllowed() { + return false; + } + + @Override + public void setRegistrationAllowed(boolean registrationAllowed) { + + } + + @Override + public boolean isRegistrationEmailAsUsername() { + return false; + } + + @Override + public void setRegistrationEmailAsUsername(boolean registrationEmailAsUsername) { + + } + + @Override + public boolean isRememberMe() { + return false; + } + + @Override + public void setRememberMe(boolean rememberMe) { + + } + + @Override + public boolean isEditUsernameAllowed() { + return false; + } + + @Override + public void setEditUsernameAllowed(boolean editUsernameAllowed) { + + } + + @Override + public boolean isUserManagedAccessAllowed() { + return false; + } + + @Override + public void setUserManagedAccessAllowed(boolean userManagedAccessAllowed) { + + } + + @Override + public void setAttribute(String name, String value) { + + } + + @Override + public void setAttribute(String name, Boolean value) { + + } + + @Override + public void setAttribute(String name, Integer value) { + + } + + @Override + public void setAttribute(String name, Long value) { + + } + + @Override + public void removeAttribute(String name) { + + } + + @Override + public String getAttribute(String name) { + return null; + } + + @Override + public Integer getAttribute(String name, Integer defaultValue) { + return null; + } + + @Override + public Long getAttribute(String name, Long defaultValue) { + return null; + } + + @Override + public Boolean getAttribute(String name, Boolean defaultValue) { + return null; + } + + @Override + public Map getAttributes() { + return null; + } + + @Override + public boolean isBruteForceProtected() { + return false; + } + + @Override + public void setBruteForceProtected(boolean value) { + + } + + @Override + public boolean isPermanentLockout() { + return false; + } + + @Override + public void setPermanentLockout(boolean val) { + + } + + @Override + public int getMaxFailureWaitSeconds() { + return 0; + } + + @Override + public void setMaxFailureWaitSeconds(int val) { + + } + + @Override + public int getWaitIncrementSeconds() { + return 0; + } + + @Override + public void setWaitIncrementSeconds(int val) { + + } + + @Override + public int getMinimumQuickLoginWaitSeconds() { + return 0; + } + + @Override + public void setMinimumQuickLoginWaitSeconds(int val) { + + } + + @Override + public long getQuickLoginCheckMilliSeconds() { + return 0; + } + + @Override + public void setQuickLoginCheckMilliSeconds(long val) { + + } + + @Override + public int getMaxDeltaTimeSeconds() { + return 0; + } + + @Override + public void setMaxDeltaTimeSeconds(int val) { + + } + + @Override + public int getFailureFactor() { + return 0; + } + + @Override + public void setFailureFactor(int failureFactor) { + + } + + @Override + public boolean isVerifyEmail() { + return false; + } + + @Override + public void setVerifyEmail(boolean verifyEmail) { + + } + + @Override + public boolean isLoginWithEmailAllowed() { + return false; + } + + @Override + public void setLoginWithEmailAllowed(boolean loginWithEmailAllowed) { + + } + + @Override + public boolean isDuplicateEmailsAllowed() { + return false; + } + + @Override + public void setDuplicateEmailsAllowed(boolean duplicateEmailsAllowed) { + + } + + @Override + public boolean isResetPasswordAllowed() { + return false; + } + + @Override + public void setResetPasswordAllowed(boolean resetPasswordAllowed) { + + } + + @Override + public String getDefaultSignatureAlgorithm() { + return null; + } + + @Override + public void setDefaultSignatureAlgorithm(String defaultSignatureAlgorithm) { + + } + + @Override + public boolean isRevokeRefreshToken() { + return false; + } + + @Override + public void setRevokeRefreshToken(boolean revokeRefreshToken) { + + } + + @Override + public int getRefreshTokenMaxReuse() { + return 0; + } + + @Override + public void setRefreshTokenMaxReuse(int revokeRefreshTokenCount) { + + } + + @Override + public int getSsoSessionIdleTimeout() { + return 0; + } + + @Override + public void setSsoSessionIdleTimeout(int seconds) { + + } + + @Override + public int getSsoSessionMaxLifespan() { + return 0; + } + + @Override + public void setSsoSessionMaxLifespan(int seconds) { + + } + + @Override + public int getSsoSessionIdleTimeoutRememberMe() { + return 0; + } + + @Override + public void setSsoSessionIdleTimeoutRememberMe(int seconds) { + + } + + @Override + public int getSsoSessionMaxLifespanRememberMe() { + return 0; + } + + @Override + public void setSsoSessionMaxLifespanRememberMe(int seconds) { + + } + + @Override + public int getOfflineSessionIdleTimeout() { + return 0; + } + + @Override + public void setOfflineSessionIdleTimeout(int seconds) { + + } + + @Override + public int getAccessTokenLifespan() { + return 0; + } + + @Override + public boolean isOfflineSessionMaxLifespanEnabled() { + return false; + } + + @Override + public void setOfflineSessionMaxLifespanEnabled(boolean offlineSessionMaxLifespanEnabled) { + + } + + @Override + public int getOfflineSessionMaxLifespan() { + return 0; + } + + @Override + public void setOfflineSessionMaxLifespan(int seconds) { + + } + + @Override + public void setAccessTokenLifespan(int seconds) { + + } + + @Override + public int getAccessTokenLifespanForImplicitFlow() { + return 0; + } + + @Override + public void setAccessTokenLifespanForImplicitFlow(int seconds) { + + } + + @Override + public int getAccessCodeLifespan() { + return 0; + } + + @Override + public void setAccessCodeLifespan(int seconds) { + + } + + @Override + public int getAccessCodeLifespanUserAction() { + return 0; + } + + @Override + public void setAccessCodeLifespanUserAction(int seconds) { + + } + + @Override + public Map getUserActionTokenLifespans() { + return null; + } + + @Override + public int getAccessCodeLifespanLogin() { + return 0; + } + + @Override + public void setAccessCodeLifespanLogin(int seconds) { + + } + + @Override + public int getActionTokenGeneratedByAdminLifespan() { + return 0; + } + + @Override + public void setActionTokenGeneratedByAdminLifespan(int seconds) { + + } + + @Override + public int getActionTokenGeneratedByUserLifespan() { + return 0; + } + + @Override + public void setActionTokenGeneratedByUserLifespan(int seconds) { + + } + + @Override + public int getActionTokenGeneratedByUserLifespan(String actionTokenType) { + return 0; + } + + @Override + public void setActionTokenGeneratedByUserLifespan(String actionTokenType, Integer seconds) { + + } + + @Override + public List getRequiredCredentials() { + return null; + } + + @Override + public void addRequiredCredential(String cred) { + + } + + @Override + public PasswordPolicy getPasswordPolicy() { + return null; + } + + @Override + public void setPasswordPolicy(PasswordPolicy policy) { + + } + + @Override + public OTPPolicy getOTPPolicy() { + return null; + } + + @Override + public void setOTPPolicy(OTPPolicy policy) { + + } + + @Override + public RoleModel getRoleById(String id) { + return null; + } + + @Override + public List getDefaultGroups() { + return null; + } + + @Override + public void addDefaultGroup(GroupModel group) { + + } + + @Override + public void removeDefaultGroup(GroupModel group) { + + } + + @Override + public List getClients() { + return null; + } + + @Override + public ClientModel addClient(String name) { + return null; + } + + @Override + public ClientModel addClient(String id, String clientId) { + return null; + } + + @Override + public boolean removeClient(String id) { + return false; + } + + @Override + public ClientModel getClientById(String id) { + return null; + } + + @Override + public ClientModel getClientByClientId(String clientId) { + return null; + } + + @Override + public void updateRequiredCredentials(Set creds) { + + } + + @Override + public Map getBrowserSecurityHeaders() { + return null; + } + + @Override + public void setBrowserSecurityHeaders(Map headers) { + + } + + @Override + public Map getSmtpConfig() { + return null; + } + + @Override + public void setSmtpConfig(Map smtpConfig) { + + } + + @Override + public AuthenticationFlowModel getBrowserFlow() { + return null; + } + + @Override + public void setBrowserFlow(AuthenticationFlowModel flow) { + + } + + @Override + public AuthenticationFlowModel getRegistrationFlow() { + return null; + } + + @Override + public void setRegistrationFlow(AuthenticationFlowModel flow) { + + } + + @Override + public AuthenticationFlowModel getDirectGrantFlow() { + return null; + } + + @Override + public void setDirectGrantFlow(AuthenticationFlowModel flow) { + + } + + @Override + public AuthenticationFlowModel getResetCredentialsFlow() { + return null; + } + + @Override + public void setResetCredentialsFlow(AuthenticationFlowModel flow) { + + } + + @Override + public AuthenticationFlowModel getClientAuthenticationFlow() { + return null; + } + + @Override + public void setClientAuthenticationFlow(AuthenticationFlowModel flow) { + + } + + @Override + public AuthenticationFlowModel getDockerAuthenticationFlow() { + return null; + } + + @Override + public void setDockerAuthenticationFlow(AuthenticationFlowModel flow) { + + } + + @Override + public List getAuthenticationFlows() { + return null; + } + + @Override + public AuthenticationFlowModel getFlowByAlias(String alias) { + return null; + } + + @Override + public AuthenticationFlowModel addAuthenticationFlow(AuthenticationFlowModel model) { + return null; + } + + @Override + public AuthenticationFlowModel getAuthenticationFlowById(String id) { + return null; + } + + @Override + public void removeAuthenticationFlow(AuthenticationFlowModel model) { + + } + + @Override + public void updateAuthenticationFlow(AuthenticationFlowModel model) { + + } + + @Override + public List getAuthenticationExecutions(String flowId) { + return null; + } + + @Override + public AuthenticationExecutionModel getAuthenticationExecutionById(String id) { + return null; + } + + @Override + public AuthenticationExecutionModel addAuthenticatorExecution(AuthenticationExecutionModel model) { + return null; + } + + @Override + public void updateAuthenticatorExecution(AuthenticationExecutionModel model) { + + } + + @Override + public void removeAuthenticatorExecution(AuthenticationExecutionModel model) { + + } + + @Override + public List getAuthenticatorConfigs() { + return null; + } + + @Override + public AuthenticatorConfigModel addAuthenticatorConfig(AuthenticatorConfigModel model) { + return null; + } + + @Override + public void updateAuthenticatorConfig(AuthenticatorConfigModel model) { + + } + + @Override + public void removeAuthenticatorConfig(AuthenticatorConfigModel model) { + + } + + @Override + public AuthenticatorConfigModel getAuthenticatorConfigById(String id) { + return null; + } + + @Override + public AuthenticatorConfigModel getAuthenticatorConfigByAlias(String alias) { + return null; + } + + @Override + public List getRequiredActionProviders() { + return null; + } + + @Override + public RequiredActionProviderModel addRequiredActionProvider(RequiredActionProviderModel model) { + return null; + } + + @Override + public void updateRequiredActionProvider(RequiredActionProviderModel model) { + + } + + @Override + public void removeRequiredActionProvider(RequiredActionProviderModel model) { + + } + + @Override + public RequiredActionProviderModel getRequiredActionProviderById(String id) { + return null; + } + + @Override + public RequiredActionProviderModel getRequiredActionProviderByAlias(String alias) { + return null; + } + + @Override + public List getIdentityProviders() { + return null; + } + + @Override + public IdentityProviderModel getIdentityProviderByAlias(String alias) { + return null; + } + + @Override + public void addIdentityProvider(IdentityProviderModel identityProvider) { + + } + + @Override + public void removeIdentityProviderByAlias(String alias) { + + } + + @Override + public void updateIdentityProvider(IdentityProviderModel identityProvider) { + + } + + @Override + public Set getIdentityProviderMappers() { + return null; + } + + @Override + public Set getIdentityProviderMappersByAlias(String brokerAlias) { + return null; + } + + @Override + public IdentityProviderMapperModel addIdentityProviderMapper(IdentityProviderMapperModel model) { + return null; + } + + @Override + public void removeIdentityProviderMapper(IdentityProviderMapperModel mapping) { + + } + + @Override + public void updateIdentityProviderMapper(IdentityProviderMapperModel mapping) { + + } + + @Override + public IdentityProviderMapperModel getIdentityProviderMapperById(String id) { + return null; + } + + @Override + public IdentityProviderMapperModel getIdentityProviderMapperByName(String brokerAlias, String name) { + return null; + } + + @Override + public ComponentModel addComponentModel(ComponentModel model) { + return null; + } + + @Override + public ComponentModel importComponentModel(ComponentModel model) { + return null; + } + + @Override + public void updateComponent(ComponentModel component) { + + } + + @Override + public void removeComponent(ComponentModel component) { + + } + + @Override + public void removeComponents(String parentId) { + + } + + @Override + public List getComponents(String parentId, String providerType) { + return null; + } + + @Override + public List getComponents(String parentId) { + return null; + } + + @Override + public List getComponents() { + return null; + } + + @Override + public ComponentModel getComponent(String id) { + return null; + } + + @Override + public String getLoginTheme() { + return null; + } + + @Override + public void setLoginTheme(String name) { + + } + + @Override + public String getAccountTheme() { + return null; + } + + @Override + public void setAccountTheme(String name) { + + } + + @Override + public String getAdminTheme() { + return null; + } + + @Override + public void setAdminTheme(String name) { + + } + + @Override + public String getEmailTheme() { + return null; + } + + @Override + public void setEmailTheme(String name) { + + } + + @Override + public int getNotBefore() { + return 0; + } + + @Override + public void setNotBefore(int notBefore) { + + } + + @Override + public boolean isEventsEnabled() { + return false; + } + + @Override + public void setEventsEnabled(boolean enabled) { + + } + + @Override + public long getEventsExpiration() { + return 0; + } + + @Override + public void setEventsExpiration(long expiration) { + + } + + @Override + public Set getEventsListeners() { + return null; + } + + @Override + public void setEventsListeners(Set listeners) { + + } + + @Override + public Set getEnabledEventTypes() { + return null; + } + + @Override + public void setEnabledEventTypes(Set enabledEventTypes) { + + } + + @Override + public boolean isAdminEventsEnabled() { + return false; + } + + @Override + public void setAdminEventsEnabled(boolean enabled) { + + } + + @Override + public boolean isAdminEventsDetailsEnabled() { + return false; + } + + @Override + public void setAdminEventsDetailsEnabled(boolean enabled) { + + } + + @Override + public ClientModel getMasterAdminClient() { + return null; + } + + @Override + public void setMasterAdminClient(ClientModel client) { + + } + + @Override + public boolean isIdentityFederationEnabled() { + return false; + } + + @Override + public boolean isInternationalizationEnabled() { + return false; + } + + @Override + public void setInternationalizationEnabled(boolean enabled) { + + } + + @Override + public Set getSupportedLocales() { + return null; + } + + @Override + public void setSupportedLocales(Set locales) { + + } + + @Override + public String getDefaultLocale() { + return null; + } + + @Override + public void setDefaultLocale(String locale) { + + } + + @Override + public GroupModel createGroup(String name) { + return null; + } + + @Override + public GroupModel createGroup(String id, String name) { + return null; + } + + @Override + public GroupModel getGroupById(String id) { + return null; + } + + @Override + public List getGroups() { + return null; + } + + @Override + public Long getGroupsCount(Boolean onlyTopGroups) { + return null; + } + + @Override + public Long getGroupsCountByNameContaining(String search) { + return null; + } + + @Override + public List getTopLevelGroups() { + return null; + } + + @Override + public List getTopLevelGroups(Integer first, Integer max) { + return null; + } + + @Override + public List searchForGroupByName(String search, Integer first, Integer max) { + return null; + } + + @Override + public boolean removeGroup(GroupModel group) { + return false; + } + + @Override + public void moveGroup(GroupModel group, GroupModel toParent) { + + } + + @Override + public List getClientScopes() { + return null; + } + + @Override + public ClientScopeModel addClientScope(String name) { + return null; + } + + @Override + public ClientScopeModel addClientScope(String id, String name) { + return null; + } + + @Override + public boolean removeClientScope(String id) { + return false; + } + + @Override + public ClientScopeModel getClientScopeById(String id) { + return null; + } + + @Override + public void addDefaultClientScope(ClientScopeModel clientScope, boolean defaultScope) { + + } + + @Override + public void removeDefaultClientScope(ClientScopeModel clientScope) { + + } + + @Override + public List getDefaultClientScopes(boolean defaultScope) { + return null; + } + } + + /** + * A whitebox implementation of the config. Please use only for testing {@link PlainTextVaultProviderFactory}. + */ + private static class VaultConfig implements Config.Scope { + + private String vaultDirectory; + private Boolean disabled; + + public VaultConfig(String vaultDirectory, Boolean disabled) { + this.vaultDirectory = vaultDirectory; + this.disabled = disabled; + } + + @Override + public String get(String key) { + return vaultDirectory; + } + + @Override + public String get(String key, String defaultValue) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public String[] getArray(String key) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public Integer getInt(String key) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public Integer getInt(String key, Integer defaultValue) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public Long getLong(String key) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public Long getLong(String key, Long defaultValue) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public Boolean getBoolean(String key) { + return disabled; + } + + @Override + public Boolean getBoolean(String key, Boolean defaultValue) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public Config.Scope scope(String... scope) { + throw new UnsupportedOperationException("not implemented"); + } + } + +} \ No newline at end of file diff --git a/services/src/test/java/org/keycloak/vault/PlainTextVaultProviderTest.java b/services/src/test/java/org/keycloak/vault/PlainTextVaultProviderTest.java new file mode 100644 index 0000000000..d514108b82 --- /dev/null +++ b/services/src/test/java/org/keycloak/vault/PlainTextVaultProviderTest.java @@ -0,0 +1,134 @@ +package org.keycloak.vault; + +import org.junit.Test; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.keycloak.vault.SecretContains.secretContains; + +/** + * Tests for {@link PlainTextVaultProvider}. + * + * @author Sebastian Łaskawiec + */ +public class PlainTextVaultProviderTest { + + @Test + public void shouldObtainSecret() throws Exception { + //given + PlainTextVaultProvider provider = new PlainTextVaultProvider(Scenario.EXISTING.getPath(), "test"); + + //when + VaultRawSecret secret1 = provider.obtainSecret("key1"); + + //then + assertNotNull(secret1); + assertNotNull(secret1.getRawSecret().get()); + assertThat(secret1, secretContains("secret1")); + } + + @Test + public void shouldReplaceUnderscoreWithTwoUnderscores() throws Exception { + //given + PlainTextVaultProvider provider = new PlainTextVaultProvider(Scenario.EXISTING.getPath(), "test_realm"); + + //when + VaultRawSecret secret1 = provider.obtainSecret("underscore_key1"); + + //then + assertNotNull(secret1); + assertNotNull(secret1.getRawSecret().get()); + assertThat(secret1, secretContains("underscore_secret1")); + } + + @Test + public void shouldReturnEmptyOptionalOnMissingSecret() throws Exception { + //given + PlainTextVaultProvider provider = new PlainTextVaultProvider(Scenario.EXISTING.getPath(), "test"); + + //when + VaultRawSecret secret = provider.obtainSecret("non-existing-key"); + + //then + assertNotNull(secret); + assertFalse(secret.getRawSecret().isPresent()); + } + + @Test + public void shouldOperateOnNonExistingVaultDirectory() throws Exception { + //given + PlainTextVaultProvider provider = new PlainTextVaultProvider(Scenario.NON_EXISTING.getPath(), "test"); + + //when + VaultRawSecret secret = provider.obtainSecret("non-existing-key"); + + //then + assertNotNull(secret); + assertFalse(secret.getRawSecret().isPresent()); + } + + @Test + public void shouldReflectChangesInASecretFile() throws Exception { + //given + Path temporarySecretFile = Files.createTempFile("vault", null); + Path vaultDirectory = temporarySecretFile.getParent(); + String secretName = temporarySecretFile.getFileName().toString(); + + PlainTextVaultProvider provider = new PlainTextVaultProvider(vaultDirectory, "ignored") { + @Override + protected Path resolveSecretPath(String vaultSecretId) { + return vaultDirectory.resolve(vaultSecretId); + } + }; + + //when + String secret1AsString = null; + String secret2AsString = null; + + Files.write(temporarySecretFile, "secret1".getBytes()); + try (VaultRawSecret secret1 = provider.obtainSecret(secretName)) { + secret1AsString = StandardCharsets.UTF_8.decode(secret1.getRawSecret().get()).toString(); + } + + Files.write(temporarySecretFile, "secret2".getBytes()); + try (VaultRawSecret secret2 = provider.obtainSecret(secretName)) { + secret2AsString = StandardCharsets.UTF_8.decode(secret2.getRawSecret().get()).toString(); + } + + //then + assertEquals("secret1", secret1AsString); + assertEquals("secret2", secret2AsString); + } + + @Test + public void shouldNotOverrideFileWhenDestroyingASecret() throws Exception { + //given + Path temporarySecretFile = Files.createTempFile("vault", null); + Path vaultDirectory = temporarySecretFile.getParent(); + String secretName = temporarySecretFile.getFileName().toString(); + + PlainTextVaultProvider provider = new PlainTextVaultProvider(vaultDirectory, "ignored") { + @Override + protected Path resolveSecretPath(String vaultSecretId) { + return vaultDirectory.resolve(vaultSecretId); + } + }; + + Files.write(temporarySecretFile, "secret".getBytes()); + + //when + VaultRawSecret secretAfterFirstRead = provider.obtainSecret(secretName); + secretAfterFirstRead.close(); + VaultRawSecret secretAfterSecondRead = provider.obtainSecret(secretName); + + //then + assertThat(secretAfterFirstRead, secretContains("secret")); + assertThat(secretAfterSecondRead, secretContains("secret")); + } +} \ No newline at end of file diff --git a/services/src/test/java/org/keycloak/vault/Scenario.java b/services/src/test/java/org/keycloak/vault/Scenario.java new file mode 100644 index 0000000000..05eee6c581 --- /dev/null +++ b/services/src/test/java/org/keycloak/vault/Scenario.java @@ -0,0 +1,30 @@ +package org.keycloak.vault; + +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * A small helper class to navigate to proper vault directory. + * + * @author Sebastian Łaskawiec + */ +enum Scenario { + EXISTING("src/test/resources/org/keycloak/vault"), + NON_EXISTING("src/test/resources/org/keycloak/vault/non-existing"), + WRITABLE_IN_RUNTIME("target/test-classes"); + + Path path; + + Scenario(String path) { + this.path = Paths.get(path); + } + + public Path getPath() { + return path; + } + + public String getAbsolutePathAsString() { + return path.toAbsolutePath().toString(); + } + +} diff --git a/services/src/test/java/org/keycloak/vault/SecretContains.java b/services/src/test/java/org/keycloak/vault/SecretContains.java new file mode 100644 index 0000000000..e3718388c4 --- /dev/null +++ b/services/src/test/java/org/keycloak/vault/SecretContains.java @@ -0,0 +1,34 @@ +package org.keycloak.vault; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +import java.nio.charset.StandardCharsets; + +/** + * Checks if {@link VaultRawSecret} is equal to a String. + */ +public class SecretContains extends TypeSafeMatcher { + + private String thisVaultAsString; + + public SecretContains(String thisVaultAsString) { + this.thisVaultAsString = thisVaultAsString; + } + + @Override + protected boolean matchesSafely(VaultRawSecret secret) { + String convertedSecret = StandardCharsets.UTF_8.decode(secret.getRawSecret().get()).toString(); + return thisVaultAsString.equals(convertedSecret); + } + + @Override + public void describeTo(Description description) { + description.appendText("is equal to " + thisVaultAsString); + } + + public static Matcher secretContains(String thisVaultAsString) { + return new SecretContains(thisVaultAsString); + } +} diff --git a/services/src/test/resources/log4j.properties b/services/src/test/resources/log4j.properties new file mode 100755 index 0000000000..0032431e6f --- /dev/null +++ b/services/src/test/resources/log4j.properties @@ -0,0 +1,5 @@ +log4j.rootLogger=info, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p %t [%c] %m%n \ No newline at end of file diff --git a/services/src/test/resources/org/keycloak/vault/test__realm_underscore__key1 b/services/src/test/resources/org/keycloak/vault/test__realm_underscore__key1 new file mode 100644 index 0000000000..9257074139 --- /dev/null +++ b/services/src/test/resources/org/keycloak/vault/test__realm_underscore__key1 @@ -0,0 +1 @@ +underscore_secret1 \ No newline at end of file diff --git a/services/src/test/resources/org/keycloak/vault/test_key1 b/services/src/test/resources/org/keycloak/vault/test_key1 new file mode 100644 index 0000000000..cb277dcf3e --- /dev/null +++ b/services/src/test/resources/org/keycloak/vault/test_key1 @@ -0,0 +1 @@ +secret1 \ No newline at end of file