Support names and ids for users, projects and domains in Keystone

In Keystone API it's possible to provide both ids and names for
users, projects, user domains and project domains. This commit
adds support for this functionality.

Change-Id: I3268bd82cc92a150927c98e0827ebd105d91f5e3
This commit is contained in:
Vitaly Kramskikh 2016-10-04 14:58:51 +03:00
parent 70a98fb10c
commit 3956dbe65b
3 changed files with 224 additions and 51 deletions

View File

@ -70,62 +70,77 @@ export default class Keystone extends AbstractService {
/**
* Issue a token from the provided credentials. Credentials will be read from the
* configuration, unless they have been explicitly provided. Note that both the userDomainName
* and the projectDomainName are only required if the user/project names are given, rather
* than the explicit user/domain ID's.
* configuration, unless they have been explicitly provided.
*
* NOTE: This method is only applicable if the password auth plugin on keystone is enabled.
* Other auth methods will have to be provided by third-party developers.
*
* @param {String} username An optional user name or ID.
* @param {String} password An optional password.
* @param {String} projectName An optional project name or ID.
* @param {String} userDomainName Domain name for the user, not required if a user id is given.
* @param {String} projectDomainName Domain name for the project, not required with project ID.
* @param {Object} credentials Optional credentials.
* @param {String} credentials.user_id An optional user ID.
* @param {String} credentials.username An optional user name.
* @param {String} credentials.password An optional password.
* @param {String} credentials.user_domain_id An optional user domain ID.
* Not required if a user ID is given.
* @param {String} credentials.user_domain_name An optional user domain name.
* Not required if a user ID is given.
* @param {String} credentials.project_id An optional project ID.
* @param {String} credentials.project_name An optional project name.
* @param {String} credentials.project_domain_id An optional project domain ID.
* Not required if a project ID is given.
* @param {String} credentials.project_domain_name An optional project domain name.
* Not required if a project ID is given.
* @returns {Promise.<T>} A promise which will resolve with a valid token.
*/
tokenIssue(username = this._safeConfigGet('auth.username'),
password = this._safeConfigGet('auth.password'),
projectName = this._safeConfigGet('auth.project_name'),
userDomainName = this._safeConfigGet('auth.user_domain_id'),
projectDomainName = this._safeConfigGet('auth.project_domain_id')) {
tokenIssue({
user_id: userId = this._safeConfigGet('auth.user_id'),
username = this._safeConfigGet('auth.username'),
password = this._safeConfigGet('auth.password'),
user_domain_id: userDomainId = this._safeConfigGet('auth.user_domain_id'),
user_domain_name: userDomainName = this._safeConfigGet('auth.user_domain_name'),
project_id: projectId = this._safeConfigGet('auth.project_id'),
project_name: projectName = this._safeConfigGet('auth.project_name'),
project_domain_id: projectDomainId = this._safeConfigGet('auth.project_domain_id'),
project_domain_name: projectDomainName = this._safeConfigGet('auth.project_domain_name')
} = {}) {
let project;
let user = {password};
if (userId) {
user.id = userId;
} else if (username) {
user.name = username;
if (userDomainId) {
user.domain = {id: userDomainId};
} else if (userDomainName) {
user.domain = {name: userDomainName};
} else {
user.domain = {id: 'default'};
}
}
if (projectId) {
project = {id: projectId};
} else if (projectName) {
project = {name: projectName};
if (projectDomainId) {
project.domain = {id: projectDomainId};
} else if (projectDomainName) {
project.domain = {name: projectDomainName};
} else {
project.domain = {id: 'default'};
}
}
const body = {
auth: {
identity: {
methods: ['password'],
password: {
user: {
name: username,
password: password
}
}
}
password: {user}
},
scope: project ? {project} : 'unscoped'
}
};
if (userDomainName) {
body.auth.identity.password.user.domain = {
id: userDomainName
};
}
if (!projectName) {
body.auth.scope = "unscoped";
} else {
body.auth.scope = {
project: {
name: projectName
}
};
if (projectDomainName) {
body.auth.scope.project.domain = {
id: projectDomainName
};
}
}
return this
.serviceEndpoint()
.then((url) => this.http.httpPost(`${url}auth/tokens`, body))

View File

@ -94,12 +94,7 @@ describe("Keystone", () => {
it("should permit passing your own user, password, and project.", (done) => {
keystone
.tokenIssue(
adminConfig.auth.username,
adminConfig.auth.password,
adminConfig.auth.project_name,
adminConfig.auth.user_domain_id,
adminConfig.auth.project_domain_id)
.tokenIssue(adminConfig.auth)
.then((token) => {
expect(token).not.toBeNull();
done();
@ -109,9 +104,38 @@ describe("Keystone", () => {
);
});
it("should throw an exception if invalid credentials are provided.", (done) => {
it("should throw an exception if invalid username and password are provided.", (done) => {
keystone
.tokenIssue('foo', 'bar', 'lolProject', 'notADomain', 'notADomain')
.tokenIssue({
username: 'foo',
password: 'bar'
})
.then((token) => done.fail(token))
.catch((error) => {
expect(error).not.toBeNull();
done();
});
});
it("should throw an exception if invalid project is provided.", (done) => {
keystone
.tokenIssue({
project_id: 'foo',
project_name: 'bar'
})
.then((token) => done.fail(token))
.catch((error) => {
expect(error).not.toBeNull();
done();
});
});
it("should throw an exception if invalid user domain is provided.", (done) => {
keystone
.tokenIssue({
user_domain_id: 'foo',
user_domain_name: 'bar'
})
.then((token) => done.fail(token))
.catch((error) => {
expect(error).not.toBeNull();

View File

@ -70,8 +70,10 @@ describe('Keystone', () => {
describe("tokenIssue()", () => {
it("should 'just work' by using provided credentials from the config.", (done) => {
let mockOptions = mockData.tokenIssue();
fetchMock.mock(mockData.root());
fetchMock.mock(mockData.tokenIssue());
fetchMock.mock(mockOptions);
const keystone = new Keystone(mockData.config);
keystone
.tokenIssue()
@ -82,6 +84,138 @@ describe('Keystone', () => {
.catch((error) => done.fail(error));
});
it("should support authentication with a user ID", (done) => {
let mockOptions = mockData.tokenIssue();
fetchMock.mock(mockData.root());
fetchMock.mock(mockOptions);
const userId = 'userId';
const keystone = new Keystone(mockData.config);
keystone
.tokenIssue({
user_id: userId
})
.then(() => {
const requestBody = JSON.parse(fetchMock.lastCall(mockOptions.matcher)[1].body);
expect(requestBody.auth.identity.password.user.id).toEqual(userId);
done();
})
.catch((error) => done.fail(error));
});
it("should support authentication with a username and a user domain ID", (done) => {
let mockOptions = mockData.tokenIssue();
fetchMock.mock(mockData.root());
fetchMock.mock(mockOptions);
const username = 'username';
const userDomainId = 'userDomainId';
const keystone = new Keystone(mockData.config);
keystone
.tokenIssue({
username: username,
user_domain_id: userDomainId
})
.then(() => {
const requestBody = JSON.parse(fetchMock.lastCall(mockOptions.matcher)[1].body);
expect(requestBody.auth.identity.password.user.name).toEqual(username);
expect(requestBody.auth.identity.password.user.domain.id).toEqual(userDomainId);
done();
})
.catch((error) => done.fail(error));
});
it("should support authentication with a username and a user domain name", (done) => {
let mockOptions = mockData.tokenIssue();
fetchMock.mock(mockData.root());
fetchMock.mock(mockOptions);
const username = 'username';
const userDomainName = 'userDomainName';
const keystone = new Keystone(mockData.config);
keystone
.tokenIssue({
username: username,
user_domain_name: userDomainName
})
.then(() => {
const requestBody = JSON.parse(fetchMock.lastCall(mockOptions.matcher)[1].body);
expect(requestBody.auth.identity.password.user.name).toEqual(username);
expect(requestBody.auth.identity.password.user.domain.name).toEqual(userDomainName);
done();
})
.catch((error) => done.fail(error));
});
it("should support authentication with a project ID", (done) => {
let mockOptions = mockData.tokenIssue();
fetchMock.mock(mockData.root());
fetchMock.mock(mockOptions);
const projectId = 'projectId';
const keystone = new Keystone(mockData.config);
keystone
.tokenIssue({
project_id: projectId,
})
.then(() => {
const requestBody = JSON.parse(fetchMock.lastCall(mockOptions.matcher)[1].body);
expect(requestBody.auth.scope.project.id).toEqual(projectId);
done();
})
.catch((error) => done.fail(error));
});
it("should support authentication with a project name and a project domain ID", (done) => {
let mockOptions = mockData.tokenIssue();
fetchMock.mock(mockData.root());
fetchMock.mock(mockOptions);
const projectName = 'projectName';
const projectDomainId = 'projectDomainId';
const keystone = new Keystone(mockData.config);
keystone
.tokenIssue({
project_name: projectName,
project_domain_id: projectDomainId
})
.then(() => {
const requestBody = JSON.parse(fetchMock.lastCall(mockOptions.matcher)[1].body);
expect(requestBody.auth.scope.project.name).toEqual(projectName);
expect(requestBody.auth.scope.project.domain.id).toEqual(projectDomainId);
done();
})
.catch((error) => done.fail(error));
});
it("should support authentication with a project name and a project domain name", (done) => {
let mockOptions = mockData.tokenIssue();
fetchMock.mock(mockData.root());
fetchMock.mock(mockOptions);
const projectName = 'projectName';
const projectDomainName = 'projectDomainName';
const keystone = new Keystone(mockData.config);
keystone
.tokenIssue({
project_name: projectName,
project_domain_name: projectDomainName
})
.then(() => {
const requestBody = JSON.parse(fetchMock.lastCall(mockOptions.matcher)[1].body);
expect(requestBody.auth.scope.project.name).toEqual(projectName);
expect(requestBody.auth.scope.project.domain.name).toEqual(projectDomainName);
done();
})
.catch((error) => done.fail(error));
});
it("Should not cache its results", (done) => {
let mockOptions = mockData.tokenIssue();
fetchMock.mock(mockData.root());