From 3956dbe65bb3df9880b2f25ac2bfd418c60a7042 Mon Sep 17 00:00:00 2001 From: Vitaly Kramskikh Date: Tue, 4 Oct 2016 14:58:51 +0300 Subject: [PATCH] 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 --- src/keystone.js | 99 +++++++++++++---------- test/functional/keystoneTest.js | 40 ++++++++-- test/unit/keystoneTest.js | 136 +++++++++++++++++++++++++++++++- 3 files changed, 224 insertions(+), 51 deletions(-) diff --git a/src/keystone.js b/src/keystone.js index 5f8a7af..8790097 100644 --- a/src/keystone.js +++ b/src/keystone.js @@ -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.} 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)) diff --git a/test/functional/keystoneTest.js b/test/functional/keystoneTest.js index aa6d77c..11a5e92 100644 --- a/test/functional/keystoneTest.js +++ b/test/functional/keystoneTest.js @@ -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(); diff --git a/test/unit/keystoneTest.js b/test/unit/keystoneTest.js index d10fb65..e5cc610 100644 --- a/test/unit/keystoneTest.js +++ b/test/unit/keystoneTest.js @@ -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());