JCLOUDS-1414: OpenStack Keystone V3 - different auth "domains" support

This commit is contained in:
Alix Lourme 2018-05-07 18:06:31 +02:00 committed by Ignasi Barrera
parent b144d9f473
commit 93a805ca57
11 changed files with 368 additions and 82 deletions

View File

@ -30,7 +30,9 @@ public abstract class TenantOrDomainAndCredentials<T> {
@Nullable public abstract String tenantOrDomainId();
@Nullable public abstract String tenantOrDomainName();
@Nullable public abstract String scope();
public abstract T credentials();
@Nullable public abstract String projectDomainName();
@Nullable public abstract String projectDomainId();
public abstract T credentials();
TenantOrDomainAndCredentials() {
@ -45,6 +47,8 @@ public abstract class TenantOrDomainAndCredentials<T> {
public abstract Builder<T> tenantOrDomainId(String tenantId);
public abstract Builder<T> tenantOrDomainName(String tenantName);
public abstract Builder<T> scope(String scope);
public abstract Builder<T> projectDomainName(String projectDomainName);
public abstract Builder<T> projectDomainId(String projectDomainId);
public abstract Builder<T> credentials(T credentials);
public abstract TenantOrDomainAndCredentials<T> build();

View File

@ -17,6 +17,8 @@
package org.jclouds.openstack.keystone.auth.functions;
import static com.google.common.base.Preconditions.checkState;
import static org.jclouds.openstack.keystone.config.KeystoneProperties.PROJECT_DOMAIN_ID;
import static org.jclouds.openstack.keystone.config.KeystoneProperties.PROJECT_DOMAIN_NAME;
import static org.jclouds.openstack.keystone.config.KeystoneProperties.REQUIRES_TENANT;
import static org.jclouds.openstack.keystone.config.KeystoneProperties.SCOPE;
import static org.jclouds.openstack.keystone.config.KeystoneProperties.TENANT_ID;
@ -51,15 +53,25 @@ public abstract class BaseAuthenticator<C> implements Function<Credentials, Auth
@Inject(optional = true)
@Named(REQUIRES_TENANT)
protected boolean requiresTenant;
@Inject(optional = true)
@Named(SCOPE)
protected String scope = Scope.UNSCOPED;
@Inject(optional = true)
@Named(PROJECT_DOMAIN_NAME)
protected String projectDomainName;
@Inject(optional = true)
@Named(PROJECT_DOMAIN_ID)
protected String projectDomainId;
@PostConstruct
public void checkPropertiesAreCompatible() {
checkState(defaultTenantName == null || defaultTenantId == null, "you cannot specify both %s and %s",
TENANT_NAME, TENANT_ID);
checkState(defaultTenantName == null || defaultTenantId == null, "you cannot specify both %s and %s", TENANT_NAME,
TENANT_ID);
checkState(projectDomainName == null || projectDomainId == null, "you cannot specify both %s and %s",
PROJECT_DOMAIN_NAME, PROJECT_DOMAIN_ID);
}
@Override
@ -74,21 +86,21 @@ public abstract class BaseAuthenticator<C> implements Function<Credentials, Auth
}
if (defaultTenantId == null && tenantName == null && requiresTenant) {
throw new IllegalArgumentException(
String.format(
"current configuration is set to [%s]. Unless you set [%s] or [%s], you must prefix your identity with 'tenantName:'",
REQUIRES_TENANT, TENANT_NAME, TENANT_ID));
throw new IllegalArgumentException(String.format(
"current configuration is set to [%s]. Unless you set [%s] or [%s], you must prefix your identity with 'tenantName:'",
REQUIRES_TENANT, TENANT_NAME, TENANT_ID));
}
C creds = createCredentials(usernameOrAccessKey, passwordOrSecretKeyOrToken);
TenantOrDomainAndCredentials<C> credsWithTenant = TenantOrDomainAndCredentials.<C> builder().tenantOrDomainId(defaultTenantId)
.tenantOrDomainName(tenantName).scope(scope).credentials(creds).build();
TenantOrDomainAndCredentials<C> credsWithTenant = TenantOrDomainAndCredentials.<C> builder()
.tenantOrDomainId(defaultTenantId).tenantOrDomainName(tenantName).scope(scope)
.projectDomainName(projectDomainName).projectDomainId(projectDomainId).credentials(creds).build();
return authenticate(credsWithTenant);
}
public abstract C createCredentials(String identity, String credential);
public abstract AuthInfo authenticate(TenantOrDomainAndCredentials<C> credentials);
}

View File

@ -65,7 +65,7 @@ public final class KeystoneProperties {
* @see <a href="http://wiki.openstack.org/CLIAuth">openstack docs</a>
*/
public static final String REQUIRES_TENANT = "jclouds.keystone.requires-tenant";
/**
* set this property to specify for scoped authentication.
* <p>
@ -80,13 +80,41 @@ public final class KeystoneProperties {
@SinceApiVersion("3")
public static final String SCOPE = "jclouds.keystone.scope";
/**
* Set this property to specify the domain name of project (tenant)
* scope.<br/>
* Required property when authentication {@link #SCOPE} is 'project:' and
* project (tenant) domain is different than the user domain (Otherwise, the
* domain used is the same as the user). <br/>
* Cannot be used simultaneously with {@link #PROJECT_DOMAIN_ID}
*
* @see <a href=
* "https://docs.openstack.org/keystone/latest/api_curl_examples.html#project-scoped">openstack
* docs : Identity service (Keystone)</a>
*/
public static final String PROJECT_DOMAIN_NAME = "jclouds.keystone.project-domain-name";
/**
* Set this property to specify the domain id of project (tenant) scope.<br/>
* Required property when authentication {@link #SCOPE} is 'project:' and
* project (tenant) domain is different than the user domain (Otherwise, the
* domain used is the same as the user). <br/>
* Cannot be used simultaneously with {@link #PROJECT_DOMAIN_NAME}
*
*
* @see <a href=
* "https://docs.openstack.org/keystone/latest/api_curl_examples.html#project-scoped">openstack
* docs : Identity service (Keystone)</a>
*/
public static final String PROJECT_DOMAIN_ID = "jclouds.keystone.project-domain-id";
/**
* type of the keystone service. ex. {@code compute}
*
* @see ServiceType
*/
public static final String SERVICE_TYPE = "jclouds.keystone.service-type";
/**
* Version of keystone to be used by services. Default: 3.
*/

View File

@ -38,6 +38,7 @@ import org.jclouds.openstack.keystone.v3.domain.Auth.DomainScope;
import org.jclouds.openstack.keystone.v3.domain.Auth.Id;
import org.jclouds.openstack.keystone.v3.domain.Auth.Name;
import org.jclouds.openstack.keystone.v3.domain.Auth.ProjectIdScope;
import org.jclouds.openstack.keystone.v3.domain.Auth.ProjectIdScope.ProjectId;
import org.jclouds.openstack.keystone.v3.domain.Auth.ProjectScope;
import org.jclouds.openstack.keystone.v3.domain.Auth.ProjectScope.ProjectName;
import org.jclouds.rest.MapBinder;
@ -50,8 +51,7 @@ import com.google.common.collect.ImmutableSet;
public abstract class BindAuthToJsonPayload<T> extends BindToJsonPayload implements MapBinder {
private static final Set<String> SCOPE_PREFIXES = ImmutableSet
.of(PROJECT, PROJECT_ID, DOMAIN, DOMAIN_ID);
private static final Set<String> SCOPE_PREFIXES = ImmutableSet.of(PROJECT, PROJECT_ID, DOMAIN, DOMAIN_ID);
protected BindAuthToJsonPayload(Json jsonBinder) {
super(jsonBinder);
@ -93,15 +93,32 @@ public abstract class BindAuthToJsonPayload<T> extends BindToJsonPayload impleme
checkArgument(SCOPE_PREFIXES.contains(parts[0]), "Scope prefix should be: %s", SCOPE_PREFIXES);
if (PROJECT.equals(parts[0])) {
Object domainScope = credentials.tenantOrDomainId() != null ? Id.create(credentials.tenantOrDomainId()) : Name
.create(credentials.tenantOrDomainName());
return ProjectScope.create(ProjectName.create(parts[1], domainScope));
return ProjectScope.create(ProjectName.create(parts[1], parseProjectDomain(credentials, true)));
} else if (PROJECT_ID.equals(parts[0])) {
return ProjectIdScope.create(Id.create(parts[1]));
// tenant (name/id) was never used as domain for project-id; so try to
// keep backward compatibility
return ProjectIdScope.create(ProjectId.create(parts[1], parseProjectDomain(credentials, false)));
} else if (DOMAIN.equals(parts[0])) {
return DomainScope.create(Name.create(parts[1]));
} else {
return DomainIdScope.create(Id.create(parts[1]));
}
}
private Object parseProjectDomain(TenantOrDomainAndCredentials<T> credentials, boolean useTenantAsDefaultDomain) {
// Before 'projectDomainName'/'projectDomainId' support,
// 'tenantOrDomainId' was used as domain (id) for project-scoped by name,
// but not by id, so 'useTenantAsDefaultDomain' flag allows to manage that
Object domainScope = null;
if (useTenantAsDefaultDomain && credentials.tenantOrDomainId() != null) {
domainScope = Id.create(credentials.tenantOrDomainId());
} else if (credentials.projectDomainName() != null) {
domainScope = Name.create(credentials.projectDomainName());
} else if (credentials.projectDomainId() != null) {
domainScope = Id.create(credentials.projectDomainId());
} else if (useTenantAsDefaultDomain) {
domainScope = Name.create(credentials.tenantOrDomainName());
}
return domainScope;
}
}

View File

@ -26,18 +26,24 @@ import com.google.auto.value.AutoValue;
@AutoValue
public abstract class Auth {
public abstract Identity identity();
@Nullable public abstract Object scope();
@Nullable
public abstract Object scope();
@SerializedNames({ "identity", "scope" })
public static Auth create(Identity identity, Object scope) {
return new AutoValue_Auth(identity, scope);
}
@AutoValue
public abstract static class Identity {
public abstract List<String> methods();
@Nullable public abstract Id token();
@Nullable public abstract PasswordAuth password();
@Nullable
public abstract Id token();
@Nullable
public abstract PasswordAuth password();
@SerializedNames({ "methods", "token", "password" })
public static Identity create(List<String> methods, Id token, PasswordAuth password) {
@ -56,7 +62,9 @@ public abstract class Auth {
@AutoValue
public abstract static class UserAuth {
public abstract String name();
public abstract DomainAuth domain();
public abstract String password();
@SerializedNames({ "name", "domain", "password" })
@ -66,7 +74,8 @@ public abstract class Auth {
@AutoValue
public abstract static class DomainAuth {
@Nullable public abstract String name();
@Nullable
public abstract String name();
@SerializedNames({ "name" })
public static DomainAuth create(String name) {
@ -76,7 +85,7 @@ public abstract class Auth {
}
}
}
@AutoValue
public abstract static class Id {
public abstract String id();
@ -86,17 +95,18 @@ public abstract class Auth {
return new AutoValue_Auth_Id(id);
}
}
@AutoValue
public abstract static class Name {
@Nullable public abstract String name();
@Nullable
public abstract String name();
@SerializedNames({ "name" })
public static Name create(String name) {
return new AutoValue_Auth_Name(name);
}
}
public static class Scope {
public static final String PROJECT = "project";
public static final String PROJECT_ID = "projectId";
@ -108,22 +118,24 @@ public abstract class Auth {
@AutoValue
public abstract static class ProjectScope {
public abstract ProjectName project();
@SerializedNames({ Scope.PROJECT })
public static ProjectScope create(ProjectName project) {
return new AutoValue_Auth_ProjectScope(project);
}
@AutoValue
public abstract static class ProjectName {
public abstract String name();
@Nullable public abstract Object domain();
@Nullable
public abstract Object domain();
@SerializedNames({ "name", Scope.DOMAIN })
public static ProjectName create(String name, Object domain) {
return new AutoValue_Auth_ProjectScope_ProjectName(name, domain);
}
public static ProjectName create(String name, Name domain) {
return new AutoValue_Auth_ProjectScope_ProjectName(name, domain);
}
@ -133,17 +145,38 @@ public abstract class Auth {
}
}
}
@AutoValue
public abstract static class ProjectIdScope {
public abstract Id project();
public abstract ProjectId project();
@SerializedNames({ Scope.PROJECT })
public static ProjectIdScope create(Id id) {
return new AutoValue_Auth_ProjectIdScope(id);
public static ProjectIdScope create(ProjectId project) {
return new AutoValue_Auth_ProjectIdScope(project);
}
@AutoValue
public abstract static class ProjectId {
public abstract String id();
@Nullable
public abstract Object domain();
@SerializedNames({ "id", Scope.DOMAIN })
public static ProjectId create(String id, Object domain) {
return new AutoValue_Auth_ProjectIdScope_ProjectId(id, domain);
}
public static ProjectId create(String id, Name domain) {
return new AutoValue_Auth_ProjectIdScope_ProjectId(id, domain);
}
public static ProjectId create(String id, Id domain) {
return new AutoValue_Auth_ProjectIdScope_ProjectId(id, domain);
}
}
}
@AutoValue
public abstract static class DomainIdScope {
public abstract Id domain();
@ -153,7 +186,7 @@ public abstract class Auth {
return new AutoValue_Auth_DomainIdScope(id);
}
}
@AutoValue
public abstract static class DomainScope {
public abstract Name domain();

View File

@ -19,6 +19,7 @@ package org.jclouds.openstack.keystone.v3.auth;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
import org.jclouds.openstack.keystone.auth.domain.ApiAccessKeyCredentials;
import org.jclouds.openstack.keystone.auth.domain.AuthInfo;
import org.jclouds.openstack.keystone.auth.domain.PasswordCredentials;
import org.jclouds.openstack.keystone.auth.domain.TenantOrDomainAndCredentials;
@ -31,71 +32,132 @@ import org.testng.annotations.Test;
public class V3AuthenticationApiMockTest extends BaseV3KeystoneApiMockTest {
public void testAuthenticatePassword() throws InterruptedException {
server.enqueue(jsonResponse("/v3/token.json"));
TenantOrDomainAndCredentials<PasswordCredentials> credentials = TenantOrDomainAndCredentials.<PasswordCredentials> builder()
.tenantOrDomainName("domain")
.scope("unscoped")
TenantOrDomainAndCredentials<PasswordCredentials> credentials = TenantOrDomainAndCredentials
.<PasswordCredentials> builder().tenantOrDomainName("domain").scope("unscoped")
.credentials(PasswordCredentials.builder().username("identity").password("credential").build()).build();
AuthInfo authInfo = authenticationApi.authenticatePassword(credentials);
assertTrue(authInfo instanceof Token);
assertEquals(authInfo, tokenFromResource("/v3/token.json"));
assertEquals(server.getRequestCount(), 1);
assertSent(server, "POST", "/auth/tokens", stringFromResource("/v3/auth-password.json"));
checkTokenResult(credentials, "/v3/auth-password.json");
}
public void testAuthenticatePasswordScoped() throws InterruptedException {
server.enqueue(jsonResponse("/v3/token.json"));
TenantOrDomainAndCredentials<PasswordCredentials> credentials = TenantOrDomainAndCredentials.<PasswordCredentials> builder()
.tenantOrDomainName("domain")
.scope("projectId:1234567890")
TenantOrDomainAndCredentials<PasswordCredentials> credentials = TenantOrDomainAndCredentials
.<PasswordCredentials> builder().tenantOrDomainName("domain").scope("projectId:1234567890")
.credentials(PasswordCredentials.builder().username("identity").password("credential").build()).build();
AuthInfo authInfo = authenticationApi.authenticatePassword(credentials);
assertTrue(authInfo instanceof Token);
assertEquals(authInfo, tokenFromResource("/v3/token.json"));
checkTokenResult(credentials, "/v3/auth-password-scoped.json");
}
assertEquals(server.getRequestCount(), 1);
assertSent(server, "POST", "/auth/tokens", stringFromResource("/v3/auth-password-scoped.json"));
public void testAuthenticatePasswordProjectScopedIdDomainBackwardsCompat() throws InterruptedException {
// See JCLOUDS-1414, before add of KeystoneProperties.PROJECT_DOMAIN,
// TENANT_ID was not used as domain for project-scoped with id
// => Unit test only for backward compatibility (is the same as
// 'testAuthenticatePasswordScoped' with TENANT-ID in addition)
TenantOrDomainAndCredentials<PasswordCredentials> credentials = TenantOrDomainAndCredentials
.<PasswordCredentials> builder().tenantOrDomainName("domain").scope("projectId:1234567890")
.tenantOrDomainId("somethingShouldNotBeUsed")
.credentials(PasswordCredentials.builder().username("identity").password("credential").build()).build();
checkTokenResult(credentials, "/v3/auth-password-scoped.json");
}
public void testAuthenticatePasswordProjectScopedNameDomainBackwardsCompat() throws InterruptedException {
// See JCLOUDS-1414, before add of KeystoneProperties.PROJECT_DOMAIN,
// domain-id of project-scoped could be filled with TENANT_ID
// => Unit test only for backward compatibility
TenantOrDomainAndCredentials<PasswordCredentials> credentials = TenantOrDomainAndCredentials
.<PasswordCredentials> builder().tenantOrDomainName("domain").scope("project:my-project")
.tenantOrDomainId("default")
.credentials(PasswordCredentials.builder().username("identity").password("credential").build()).build();
checkTokenResult(credentials, "/v3/auth-password-project-scoped-name-domain-backwards-compat.json");
}
public void testAuthenticatePasswordProjectScopedIdDomainId() throws InterruptedException {
TenantOrDomainAndCredentials<PasswordCredentials> credentials = TenantOrDomainAndCredentials
.<PasswordCredentials> builder().tenantOrDomainName("domain").scope("projectId:42-project-42")
.projectDomainId("42-domain-42")
.credentials(PasswordCredentials.builder().username("identity").password("credential").build()).build();
checkTokenResult(credentials, "/v3/auth-password-project-scoped-id-domain-id.json");
}
public void testAuthenticatePasswordProjectScopedIdDomainName() throws InterruptedException {
TenantOrDomainAndCredentials<PasswordCredentials> credentials = TenantOrDomainAndCredentials
.<PasswordCredentials> builder().tenantOrDomainName("domain").scope("projectId:42")
.projectDomainName("default")
.credentials(PasswordCredentials.builder().username("identity").password("credential").build()).build();
checkTokenResult(credentials, "/v3/auth-password-project-scoped-id-domain-name.json");
}
public void testAuthenticatePasswordProjectScopedNameDomainId() throws InterruptedException {
TenantOrDomainAndCredentials<PasswordCredentials> credentials = TenantOrDomainAndCredentials
.<PasswordCredentials> builder().tenantOrDomainName("domain").scope("project:my-project")
.projectDomainId("42")
.credentials(PasswordCredentials.builder().username("identity").password("credential").build()).build();
checkTokenResult(credentials, "/v3/auth-password-project-scoped-name-domain-id.json");
}
public void testAuthenticatePasswordProjectScopedNameDomainName() throws InterruptedException {
TenantOrDomainAndCredentials<PasswordCredentials> credentials = TenantOrDomainAndCredentials
.<PasswordCredentials> builder().tenantOrDomainName("domain").scope("project:my-project")
.projectDomainName("default")
.credentials(PasswordCredentials.builder().username("identity").password("credential").build()).build();
checkTokenResult(credentials, "/v3/auth-password-project-scoped-name-domain-name.json");
}
public void testAuthenticateToken() throws InterruptedException {
server.enqueue(jsonResponse("/v3/token.json"));
TenantOrDomainAndCredentials<TokenCredentials> credentials = TenantOrDomainAndCredentials.<TokenCredentials> builder()
.tenantOrDomainName("domain")
.scope("unscoped")
TenantOrDomainAndCredentials<TokenCredentials> credentials = TenantOrDomainAndCredentials
.<TokenCredentials> builder().tenantOrDomainName("domain").scope("unscoped")
.credentials(TokenCredentials.builder().id("token").build()).build();
AuthInfo authInfo = authenticationApi.authenticateToken(credentials);
assertTrue(authInfo instanceof Token);
assertEquals(authInfo, tokenFromResource("/v3/token.json"));
assertEquals(server.getRequestCount(), 1);
assertSent(server, "POST", "/auth/tokens", stringFromResource("/v3/auth-token.json"));
checkTokenResult(credentials, "/v3/auth-token.json");
}
public void testAuthenticateTokenScoped() throws InterruptedException {
TenantOrDomainAndCredentials<TokenCredentials> credentials = TenantOrDomainAndCredentials
.<TokenCredentials> builder().tenantOrDomainName("domain").scope("domain:mydomain")
.credentials(TokenCredentials.builder().id("token").build()).build();
checkTokenResult(credentials, "/v3/auth-token-scoped.json");
}
@SuppressWarnings("unchecked")
private void checkTokenResult(TenantOrDomainAndCredentials<?> credentials, String json) throws InterruptedException {
server.enqueue(jsonResponse("/v3/token.json"));
TenantOrDomainAndCredentials<TokenCredentials> credentials = TenantOrDomainAndCredentials.<TokenCredentials> builder()
.tenantOrDomainName("domain")
.scope("domain:mydomain")
.credentials(TokenCredentials.builder().id("token").build()).build();
AuthInfo authInfo = authenticationApi.authenticateToken(credentials);
AuthInfo authInfo = null;
if (credentials.credentials() instanceof PasswordCredentials) {
authInfo = authenticationApi
.authenticatePassword((TenantOrDomainAndCredentials<PasswordCredentials>) credentials);
} else if (credentials.credentials() instanceof TokenCredentials) {
authInfo = authenticationApi.authenticateToken((TenantOrDomainAndCredentials<TokenCredentials>) credentials);
} else if (credentials.credentials() instanceof ApiAccessKeyCredentials) {
authInfo = authenticationApi
.authenticateAccessKey((TenantOrDomainAndCredentials<ApiAccessKeyCredentials>) credentials);
} else {
throw new IllegalArgumentException(String.format("Unsupported authentication method with class: %s",
credentials.credentials().getClass().getName()));
}
assertTrue(authInfo instanceof Token);
assertEquals(authInfo, tokenFromResource("/v3/token.json"));
assertEquals(server.getRequestCount(), 1);
assertSent(server, "POST", "/auth/tokens", stringFromResource("/v3/auth-token-scoped.json"));
assertSent(server, "POST", "/auth/tokens", stringFromResource(json));
}
}

View File

@ -0,0 +1,26 @@
{
"auth": {
"identity": {
"methods": [
"password"
],
"password": {
"user": {
"name": "identity",
"domain": {
"name": "domain"
},
"password": "credential"
}
}
},
"scope": {
"project": {
"id": "42-project-42",
"domain": {
"id": "42-domain-42"
}
}
}
}
}

View File

@ -0,0 +1,26 @@
{
"auth": {
"identity": {
"methods": [
"password"
],
"password": {
"user": {
"name": "identity",
"domain": {
"name": "domain"
},
"password": "credential"
}
}
},
"scope": {
"project": {
"id": "42",
"domain": {
"name": "default"
}
}
}
}
}

View File

@ -0,0 +1,26 @@
{
"auth": {
"identity": {
"methods": [
"password"
],
"password": {
"user": {
"name": "identity",
"domain": {
"name": "domain"
},
"password": "credential"
}
}
},
"scope": {
"project": {
"name": "my-project",
"domain": {
"id": "default"
}
}
}
}
}

View File

@ -0,0 +1,26 @@
{
"auth": {
"identity": {
"methods": [
"password"
],
"password": {
"user": {
"name": "identity",
"domain": {
"name": "domain"
},
"password": "credential"
}
}
},
"scope": {
"project": {
"name": "my-project",
"domain": {
"id": "42"
}
}
}
}
}

View File

@ -0,0 +1,26 @@
{
"auth": {
"identity": {
"methods": [
"password"
],
"password": {
"user": {
"name": "identity",
"domain": {
"name": "domain"
},
"password": "credential"
}
}
},
"scope": {
"project": {
"name": "my-project",
"domain": {
"name": "default"
}
}
}
}
}