diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/LdapAuthentication.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/LdapAuthentication.java new file mode 100644 index 00000000..f75587ca --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/LdapAuthentication.java @@ -0,0 +1,79 @@ +/* + * Copyright 2021 the original author or authors. + * + * 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 + * + * https://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 org.springframework.vault.authentication; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.Assert; +import org.springframework.vault.VaultException; +import org.springframework.vault.client.VaultResponses; +import org.springframework.vault.support.VaultResponse; +import org.springframework.vault.support.VaultToken; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestOperations; + +import static java.util.Collections.singletonMap; +import static org.springframework.vault.authentication.AuthenticationUtil.getLoginPath; + +/** + * LDAP implementation of {@link ClientAuthentication}. + * + * @author Mikhael Sokolov + * @see LdapAuthenticationOptions + * @see RestOperations + * @see LDAP + * @since 2.4 + */ +public class LdapAuthentication implements ClientAuthentication, AuthenticationStepsFactory { + + private static final Log logger = LogFactory.getLog(LdapAuthentication.class); + + private final LdapAuthenticationOptions options; + + private final RestOperations restOperations; + + public LdapAuthentication(LdapAuthenticationOptions options, RestOperations restOperations) { + Assert.notNull(options, "LdapAuthenticationOptions must not be null"); + Assert.notNull(restOperations, "RestOperations must not be null"); + + this.options = options; + this.restOperations = restOperations; + } + + @Override + public VaultToken login() throws VaultException { + return createTokenUsingLdapAuthentication(); + } + + @Override + public AuthenticationSteps getAuthenticationSteps() { + return AuthenticationSteps + .fromSupplier(() -> singletonMap("password", options.getPassword())) + .login(String.format("%s/%s", getLoginPath(options.getPath()), options.getUsername())); + } + + private VaultToken createTokenUsingLdapAuthentication() { + try { + VaultResponse response = restOperations.postForObject(String.format("%s/%s", getLoginPath(options.getPath()), options.getUsername()), singletonMap("password", options.getPassword()), VaultResponse.class); + + logger.debug("Login successful using LDAP credentials"); + + return LoginTokenUtil.from(response.getAuth()); + } catch (HttpStatusCodeException e) { + throw new VaultException(String.format("Cannot login using LDAP: %s", VaultResponses.getError(e.getResponseBodyAsString())), e); + } + } +} \ No newline at end of file diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/LdapAuthenticationOptions.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/LdapAuthenticationOptions.java new file mode 100644 index 00000000..d54ba0d4 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/LdapAuthenticationOptions.java @@ -0,0 +1,101 @@ +/* + * Copyright 2021 the original author or authors. + * + * 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 + * + * https://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 org.springframework.vault.authentication; + +import org.springframework.util.Assert; + +import javax.annotation.Nullable; + +/** + * @author Mikhael Sokolov + */ +public class LdapAuthenticationOptions { + + public static final String DEFAULT_LDAP_AUTHENTICATION_PATH = "ldap"; + + /** + * Path of the ldap authentication backend mount. + */ + private final String path; + + /** + * Username of the ldap authentication backend mount. + */ + private final String username; + + /** + * Password of the ldap authentication backend mount. + */ + private final CharSequence password; + + private LdapAuthenticationOptions(String username, CharSequence password, String path) { + this.username = username; + this.password = password; + this.path = path; + } + + public static LdapAuthenticationOptionsBuilder builder() { + return new LdapAuthenticationOptionsBuilder(); + } + + public String getUsername() { + return username; + } + + public CharSequence getPassword() { + return password; + } + + public String getPath() { + return path; + } + + public static class LdapAuthenticationOptionsBuilder { + + @Nullable + private String username; + + @Nullable + private CharSequence password; + + private String path = DEFAULT_LDAP_AUTHENTICATION_PATH; + + LdapAuthenticationOptionsBuilder() { + } + + public LdapAuthenticationOptionsBuilder username(String username) { + this.username = username; + return this; + } + + public LdapAuthenticationOptionsBuilder password(CharSequence password) { + this.password = password; + return this; + } + + public LdapAuthenticationOptionsBuilder path(String path) { + this.path = path; + return this; + } + + public LdapAuthenticationOptions build() { + Assert.notNull(this.username, "Username must not be null"); + Assert.notNull(this.password, "Password must not be null"); + + return new LdapAuthenticationOptions(username, password, path); + } + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/UsernamePasswordAuthentication.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/UsernamePasswordAuthentication.java new file mode 100644 index 00000000..121478b8 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/UsernamePasswordAuthentication.java @@ -0,0 +1,79 @@ +/* + * Copyright 2021 the original author or authors. + * + * 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 + * + * https://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 org.springframework.vault.authentication; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.Assert; +import org.springframework.vault.VaultException; +import org.springframework.vault.client.VaultResponses; +import org.springframework.vault.support.VaultResponse; +import org.springframework.vault.support.VaultToken; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestOperations; + +import static java.util.Collections.singletonMap; +import static org.springframework.vault.authentication.AuthenticationUtil.getLoginPath; + +/** + * Username and password implementation of {@link ClientAuthentication}. + * + * @author Mikhael Sokolov + * @see UsernamePasswordAuthenticationOptions + * @see RestOperations + * @see Username & password + * @since 2.4 + */ +public class UsernamePasswordAuthentication implements ClientAuthentication, AuthenticationStepsFactory { + + private static final Log logger = LogFactory.getLog(UsernamePasswordAuthentication.class); + + private final UsernamePasswordAuthenticationOptions options; + + private final RestOperations restOperations; + + public UsernamePasswordAuthentication(UsernamePasswordAuthenticationOptions options, RestOperations restOperations) { + Assert.notNull(options, "UsernamePasswordAuthenticationOptions must not be null"); + Assert.notNull(restOperations, "RestOperations must not be null"); + + this.options = options; + this.restOperations = restOperations; + } + + @Override + public VaultToken login() throws VaultException { + return createTokenUsingUsernamePasswordAuthentication(); + } + + @Override + public AuthenticationSteps getAuthenticationSteps() { + return AuthenticationSteps + .fromSupplier(() -> singletonMap("password", options.getPassword())) + .login(String.format("%s/%s", getLoginPath(options.getPath()), options.getUsername())); + } + + private VaultToken createTokenUsingUsernamePasswordAuthentication() { + try { + VaultResponse response = restOperations.postForObject(String.format("%s/%s", getLoginPath(options.getPath()), options.getUsername()), singletonMap("password", options.getPassword()), VaultResponse.class); + + logger.debug("Login successful using username and password credentials"); + + return LoginTokenUtil.from(response.getAuth()); + } catch (HttpStatusCodeException e) { + throw new VaultException(String.format("Cannot login using username and password: %s", VaultResponses.getError(e.getResponseBodyAsString())), e); + } + } +} \ No newline at end of file diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/UsernamePasswordAuthenticationOptions.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/UsernamePasswordAuthenticationOptions.java new file mode 100644 index 00000000..519ab5d9 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/UsernamePasswordAuthenticationOptions.java @@ -0,0 +1,101 @@ +/* + * Copyright 2021 the original author or authors. + * + * 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 + * + * https://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 org.springframework.vault.authentication; + +import org.springframework.util.Assert; + +import javax.annotation.Nullable; + +/** + * @author Mikhael Sokolov + */ +public class UsernamePasswordAuthenticationOptions { + + public static final String DEFAULT_USERPASS_AUTHENTICATION_PATH = "userpass"; + + /** + * Path of the userpass authentication backend mount. + */ + private final String path; + + /** + * Username of the userpass authentication backend mount. + */ + private final String username; + + /** + * Password of the userpass authentication backend mount. + */ + private final CharSequence password; + + private UsernamePasswordAuthenticationOptions(String username, CharSequence password, String path) { + this.username = username; + this.password = password; + this.path = path; + } + + public String getUsername() { + return username; + } + + public CharSequence getPassword() { + return password; + } + + public String getPath() { + return path; + } + + public static UsernamePasswordAuthenticationBuilder builder() { + return new UsernamePasswordAuthenticationBuilder(); + } + + public static class UsernamePasswordAuthenticationBuilder { + + @Nullable + private String username; + + @Nullable + private CharSequence password; + + private String path = DEFAULT_USERPASS_AUTHENTICATION_PATH; + + UsernamePasswordAuthenticationBuilder() { + } + + public UsernamePasswordAuthenticationBuilder username(String username) { + this.username = username; + return this; + } + + public UsernamePasswordAuthenticationBuilder password(CharSequence password) { + this.password = password; + return this; + } + + public UsernamePasswordAuthenticationBuilder path(String path) { + this.path = path; + return this; + } + + public UsernamePasswordAuthenticationOptions build() { + Assert.notNull(this.username, "Username must not be null"); + Assert.notNull(this.password, "Password must not be null"); + + return new UsernamePasswordAuthenticationOptions(username, password, path); + } + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/LdapAuthenticationIntegrationTestBase.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/LdapAuthenticationIntegrationTestBase.java new file mode 100644 index 00000000..e751cdcd --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/LdapAuthenticationIntegrationTestBase.java @@ -0,0 +1,50 @@ +/* + * Copyright 2017-2021 the original author or authors. + * + * 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 + * + * https://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 org.springframework.vault.authentication; + +import org.junit.jupiter.api.BeforeEach; +import org.springframework.vault.support.Policy; +import org.springframework.vault.util.IntegrationTestSupport; + +import java.util.Map; + +import static java.util.Collections.singletonMap; +import static org.springframework.vault.authentication.LdapAuthenticationOptions.DEFAULT_LDAP_AUTHENTICATION_PATH; +import static org.springframework.vault.support.Policy.BuiltinCapabilities.*; + +/** + * Integration test base class for {@link LdapAuthentication} tests. + * + * @author Mikhael Sokolov + */ +public abstract class LdapAuthenticationIntegrationTestBase extends IntegrationTestSupport { + + static final Policy POLICY = Policy.of(Policy.Rule.builder().path("/*").capabilities(READ, CREATE, UPDATE).build()); + + protected final String username = "admin"; + protected final String password = "qwerty"; + + @BeforeEach + public void before() { + + if (!prepare().hasAuth(DEFAULT_LDAP_AUTHENTICATION_PATH)) { + prepare().mountAuth(DEFAULT_LDAP_AUTHENTICATION_PATH); + } + + prepare().getVaultOperations().opsForSys().createOrUpdatePolicy(DEFAULT_LDAP_AUTHENTICATION_PATH, POLICY); + prepare().getVaultOperations().doWithSession(restOperations -> restOperations.postForEntity(String.format("auth/%s/users/%s", DEFAULT_LDAP_AUTHENTICATION_PATH, username), singletonMap("password", password), Map.class)); + } +} \ No newline at end of file diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/LdapAuthenticationIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/LdapAuthenticationIntegrationTests.java new file mode 100644 index 00000000..d4c4417a --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/LdapAuthenticationIntegrationTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016-2021 the original author or authors. + * + * 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 + * + * https://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 org.springframework.vault.authentication; + +import org.junit.jupiter.api.Test; +import org.springframework.vault.client.VaultClients; +import org.springframework.vault.support.VaultToken; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link LdapAuthentication}. + * + * @author Mikhael Sokolov + */ +class LdapAuthenticationIntegrationTests extends LdapAuthenticationIntegrationTestBase { + + @Test + void shouldLoginSuccessfully() { + + RestTemplate restTemplate = VaultClients.createRestTemplate(); + LdapAuthenticationOptions options = LdapAuthenticationOptions.builder() + .username(username) + .password(password) + .build(); + + LdapAuthentication authentication = new LdapAuthentication(options, restTemplate); + VaultToken login = authentication.login(); + + assertThat(login.getToken()).isNotEmpty(); + } +} \ No newline at end of file diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/UsernamePasswordAuthenticationIntegrationTestBase.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/UsernamePasswordAuthenticationIntegrationTestBase.java new file mode 100644 index 00000000..50780ab1 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/UsernamePasswordAuthenticationIntegrationTestBase.java @@ -0,0 +1,50 @@ +/* + * Copyright 2017-2021 the original author or authors. + * + * 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 + * + * https://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 org.springframework.vault.authentication; + +import org.junit.jupiter.api.BeforeEach; +import org.springframework.vault.support.Policy; +import org.springframework.vault.util.IntegrationTestSupport; + +import java.util.Map; + +import static java.util.Collections.singletonMap; +import static org.springframework.vault.authentication.UsernamePasswordAuthenticationOptions.DEFAULT_USERPASS_AUTHENTICATION_PATH; +import static org.springframework.vault.support.Policy.BuiltinCapabilities.*; + +/** + * Integration test base class for {@link UsernamePasswordAuthentication} tests. + * + * @author Mikhael Sokolov + */ +public abstract class UsernamePasswordAuthenticationIntegrationTestBase extends IntegrationTestSupport { + + static final Policy POLICY = Policy.of(Policy.Rule.builder().path("/*").capabilities(READ, CREATE, UPDATE).build()); + + protected final String username = "admin"; + protected final String password = "qwerty"; + + @BeforeEach + public void before() { + + if (!prepare().hasAuth(DEFAULT_USERPASS_AUTHENTICATION_PATH)) { + prepare().mountAuth(DEFAULT_USERPASS_AUTHENTICATION_PATH); + } + + prepare().getVaultOperations().opsForSys().createOrUpdatePolicy(DEFAULT_USERPASS_AUTHENTICATION_PATH, POLICY); + prepare().getVaultOperations().doWithSession(restOperations -> restOperations.postForEntity(String.format("auth/%s/users/%s", DEFAULT_USERPASS_AUTHENTICATION_PATH, username), singletonMap("password", password), Map.class)); + } +} \ No newline at end of file diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/UsernamePasswordAuthenticationIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/UsernamePasswordAuthenticationIntegrationTests.java new file mode 100644 index 00000000..4c12a4c0 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/UsernamePasswordAuthenticationIntegrationTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016-2021 the original author or authors. + * + * 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 + * + * https://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 org.springframework.vault.authentication; + +import org.junit.jupiter.api.Test; +import org.springframework.vault.client.VaultClients; +import org.springframework.vault.support.VaultToken; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link UsernamePasswordAuthentication}. + * + * @author Mikhael Sokolov + */ +class UsernamePasswordAuthenticationIntegrationTests extends UsernamePasswordAuthenticationIntegrationTestBase { + + @Test + void shouldLoginSuccessfully() { + + RestTemplate restTemplate = VaultClients.createRestTemplate(); + UsernamePasswordAuthenticationOptions options = UsernamePasswordAuthenticationOptions.builder() + .username(username) + .password(password) + .build(); + + UsernamePasswordAuthentication authentication = new UsernamePasswordAuthentication(options, restTemplate); + VaultToken login = authentication.login(); + + assertThat(login.getToken()).isNotEmpty(); + } +} \ No newline at end of file