From 12a191600735bd45a473a1bd5b5c14338365c58d Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Fri, 23 Mar 2018 16:35:39 +0100 Subject: [PATCH 1/7] Allow (experimental!) login with API keys Change-Type: minor --- doc/cli.markdown | 4 ++-- lib/actions/auth.coffee | 10 +++++----- lib/app.coffee | 7 ++++--- lib/auth/utils.coffee | 6 ++++-- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/doc/cli.markdown b/doc/cli.markdown index c5ac9978..a0ef074a 100644 --- a/doc/cli.markdown +++ b/doc/cli.markdown @@ -262,7 +262,7 @@ from the dashboard. - Credentials: using email/password and 2FA. -- Token: using the authentication token from the preferences page. +- Token: using a session token or API key (experimental) from the preferences page. Examples: @@ -276,7 +276,7 @@ Examples: #### --token, -t <token> -auth token +session token or API key (experimental) #### --web, -w diff --git a/lib/actions/auth.coffee b/lib/actions/auth.coffee index a2460492..69529b35 100644 --- a/lib/actions/auth.coffee +++ b/lib/actions/auth.coffee @@ -27,7 +27,7 @@ exports.login = - Credentials: using email/password and 2FA. - - Token: using the authentication token from the preferences page. + - Token: using a session token or API key (experimental) from the preferences page. Examples: @@ -40,7 +40,7 @@ exports.login = options: [ { signature: 'token' - description: 'auth token' + description: 'session token or API key (experimental)' parameter: 'token' alias: 't' } @@ -73,7 +73,7 @@ exports.login = action: (params, options, done) -> _ = require('lodash') Promise = require('bluebird') - resin = require('resin-sdk-preconfigured') + resin = require('resin-sdk').fromSharedOptions() auth = require('../auth') form = require('resin-cli-form') patterns = require('../utils/patterns') @@ -84,7 +84,7 @@ exports.login = return Promise.try -> return options.token if _.isString(options.token) return form.ask - message: 'Token (from the preferences page)' + message: 'Session token or API key (experimental) from the preferences page' name: 'token' type: 'input' .then(resin.auth.loginWithToken) @@ -188,7 +188,7 @@ exports.whoami = permission: 'user' action: (params, options, done) -> Promise = require('bluebird') - resin = require('resin-sdk-preconfigured') + resin = require('resin-sdk').fromSharedOptions() visuals = require('resin-cli-visuals') Promise.props diff --git a/lib/app.coffee b/lib/app.coffee index 2ceb6421..4faf13d7 100644 --- a/lib/app.coffee +++ b/lib/app.coffee @@ -62,14 +62,15 @@ capitanoExecuteAsync = Promise.promisify(capitano.execute) # We don't yet use resin-sdk directly everywhere, but we set up shared # options correctly so we can do safely in submodules -require('resin-sdk').setSharedOptions( +ResinSdk = require('resin-sdk') +ResinSdk.setSharedOptions( apiUrl: settings.get('apiUrl') imageMakerUrl: settings.get('imageMakerUrl') dataDirectory: settings.get('dataDirectory') retries: 2 ) -# Keep using sdk-preconfigured for now, but only temporarily -resin = require('resin-sdk-preconfigured') + +resin = ResinSdk.fromSharedOptions() actions = require('./actions') errors = require('./errors') diff --git a/lib/auth/utils.coffee b/lib/auth/utils.coffee index 6dbb69c6..d3756a72 100644 --- a/lib/auth/utils.coffee +++ b/lib/auth/utils.coffee @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. ### -resin = require('resin-sdk-preconfigured') +resin = require('resin-sdk').fromSharedOptions() _ = require('lodash') url = require('url') Promise = require('bluebird') @@ -62,7 +62,9 @@ exports.isTokenValid = (sessionToken) -> if not sessionToken? or _.isEmpty(sessionToken.trim()) return Promise.resolve(false) - return resin.token.get().then (currentToken) -> + return resin.auth.getToken() + .catchReturn(undefined) + .then (currentToken) -> resin.auth.loginWithToken(sessionToken) .return(sessionToken) .then(resin.auth.isLoggedIn) From 2db1d84d3c2f2c57239f5a7b73b4c945d950c388 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Tue, 27 Mar 2018 13:53:14 +0200 Subject: [PATCH 2/7] Do not require a login for builds Fixes: #578 Change-Type: patch --- lib/actions/build.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/actions/build.coffee b/lib/actions/build.coffee index 3e491d44..d72cf63e 100644 --- a/lib/actions/build.coffee +++ b/lib/actions/build.coffee @@ -47,7 +47,6 @@ buildProject = (docker, logger, composeOpts, opts) -> module.exports = signature: 'build [source]' description: 'Build a single image or a multicontainer project locally' - permission: 'user' primary: true help: ''' Use this command to build an image or a complete multicontainer project From 0e2fb8c96c672e503561f28454fd6ec342c66fd6 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Wed, 28 Mar 2018 17:47:45 +0200 Subject: [PATCH 3/7] Promisify auth utils tests --- tests/auth/utils.spec.coffee | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/tests/auth/utils.spec.coffee b/tests/auth/utils.spec.coffee index 152b32c2..2c1ab637 100644 --- a/tests/auth/utils.spec.coffee +++ b/tests/auth/utils.spec.coffee @@ -9,39 +9,36 @@ describe 'Utils:', -> describe '.getDashboardLoginURL()', -> - it 'should eventually be a valid url', (done) -> + it 'should eventually be a valid url', -> utils.getDashboardLoginURL('https://127.0.0.1:3000/callback').then (loginUrl) -> m.chai.expect -> url.parse(loginUrl) .to.not.throw(Error) - .nodeify(done) - it 'should eventually contain an https protocol', (done) -> + + it 'should eventually contain an https protocol', -> Promise.props dashboardUrl: resin.settings.get('dashboardUrl') loginUrl: utils.getDashboardLoginURL('https://127.0.0.1:3000/callback') .then ({ dashboardUrl, loginUrl }) -> protocol = url.parse(loginUrl).protocol m.chai.expect(protocol).to.equal(url.parse(dashboardUrl).protocol) - .nodeify(done) - it 'should correctly escape a callback url without a path', (done) -> + it 'should correctly escape a callback url without a path', -> Promise.props dashboardUrl: resin.settings.get('dashboardUrl') loginUrl: utils.getDashboardLoginURL('http://127.0.0.1:3000') .then ({ dashboardUrl, loginUrl }) -> expectedUrl = "#{dashboardUrl}/login/cli/http%253A%252F%252F127.0.0.1%253A3000" m.chai.expect(loginUrl).to.equal(expectedUrl) - .nodeify(done) - it 'should correctly escape a callback url with a path', (done) -> + it 'should correctly escape a callback url with a path', -> Promise.props dashboardUrl: resin.settings.get('dashboardUrl') loginUrl: utils.getDashboardLoginURL('http://127.0.0.1:3000/callback') .then ({ dashboardUrl, loginUrl }) -> expectedUrl = "#{dashboardUrl}/login/cli/http%253A%252F%252F127.0.0.1%253A3000%252Fcallback" m.chai.expect(loginUrl).to.equal(expectedUrl) - .nodeify(done) describe '.isTokenValid()', -> @@ -76,26 +73,24 @@ describe 'Utils:', -> describe 'given there was a token already', -> - beforeEach (done) -> - resin.auth.loginWithToken(tokens.janedoe.token).nodeify(done) + beforeEach -> + resin.auth.loginWithToken(tokens.janedoe.token) - it 'should preserve the old token', (done) -> + it 'should preserve the old token', -> resin.auth.getToken().then (originalToken) -> m.chai.expect(originalToken).to.equal(tokens.janedoe.token) return utils.isTokenValid(tokens.johndoe.token) .then(resin.auth.getToken).then (currentToken) -> m.chai.expect(currentToken).to.equal(tokens.janedoe.token) - .nodeify(done) describe 'given there was no token', -> - beforeEach (done) -> - resin.auth.logout().nodeify(done) + beforeEach -> + resin.auth.logout() - it 'should stay without a token', (done) -> + it 'should stay without a token', -> utils.isTokenValid(tokens.johndoe.token).then -> m.chai.expect(resin.token.get()).to.eventually.not.exist - .nodeify(done) describe 'given the token does authenticate with the server', -> From e965c603d2641c6a6f2dd6afd2b2ff76d97978c4 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Wed, 28 Mar 2018 17:47:57 +0200 Subject: [PATCH 4/7] Use spec test reporter, so we can debug with output --- gulpfile.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gulpfile.coffee b/gulpfile.coffee index 3049237b..e8464145 100644 --- a/gulpfile.coffee +++ b/gulpfile.coffee @@ -28,7 +28,7 @@ gulp.task 'coffee', -> gulp.task 'test', -> gulp.src(OPTIONS.files.tests, read: false) .pipe(mocha({ - reporter: 'min' + reporter: 'spec' })) gulp.task 'build', [ From d3a0bfc5f69212f93f5ee8cc415a69c7f6861995 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Wed, 28 Mar 2018 17:48:20 +0200 Subject: [PATCH 5/7] Fix auth utils tests to work with new SDK --- package.json | 1 + tests/auth/utils.spec.coffee | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 49873d46..722701a5 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "publish-release": "^1.3.3", "require-npm4-to-publish": "^1.0.0", "resin-lint": "^1.5.0", + "rewire": "^3.0.2", "ts-node": "^4.0.1", "typescript": "2.4.0" }, diff --git a/tests/auth/utils.spec.coffee b/tests/auth/utils.spec.coffee index 2c1ab637..b2b6ed85 100644 --- a/tests/auth/utils.spec.coffee +++ b/tests/auth/utils.spec.coffee @@ -1,10 +1,13 @@ m = require('mochainon') url = require('url') Promise = require('bluebird') -resin = require('resin-sdk-preconfigured') -utils = require('../../build/auth/utils') + tokens = require('./tokens.json') +rewire = require('rewire') +utils = rewire('../../build/auth/utils') +resin = utils.__get__('resin') + describe 'Utils:', -> describe '.getDashboardLoginURL()', -> @@ -90,7 +93,9 @@ describe 'Utils:', -> it 'should stay without a token', -> utils.isTokenValid(tokens.johndoe.token).then -> - m.chai.expect(resin.token.get()).to.eventually.not.exist + resin.auth.isLoggedIn() + .then (isLoggedIn) -> + m.chai.expect(isLoggedIn).to.equal(false) describe 'given the token does authenticate with the server', -> From ce64889b0469c121902c150be0bf17ee9d59e1d1 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Wed, 28 Mar 2018 20:13:12 +0200 Subject: [PATCH 6/7] Clarify isTokenValid logic --- lib/auth/server.coffee | 6 +++--- lib/auth/utils.coffee | 21 ++++++++++++--------- tests/auth/server.spec.coffee | 12 ++++++------ tests/auth/utils.spec.coffee | 18 +++++++++--------- 4 files changed, 30 insertions(+), 27 deletions(-) diff --git a/lib/auth/server.coffee b/lib/auth/server.coffee index ec4fefd1..519665e9 100644 --- a/lib/auth/server.coffee +++ b/lib/auth/server.coffee @@ -77,9 +77,9 @@ exports.awaitForToken = (options) -> Promise.try -> if not token throw new Error('No token') - return utils.isTokenValid(token) - .tap (isValid) -> - if not isValid + return utils.loginIfTokenValid(token) + .tap (loggedIn) -> + if not loggedIn throw new Error('Invalid token') .then -> renderAndDone({ request, response, viewName: 'success', token }) diff --git a/lib/auth/utils.coffee b/lib/auth/utils.coffee index d3756a72..7b82b471 100644 --- a/lib/auth/utils.coffee +++ b/lib/auth/utils.coffee @@ -42,7 +42,7 @@ exports.getDashboardLoginURL = (callbackUrl) -> return url.resolve(dashboardUrl, "/login/cli/#{callbackUrl}") ###* -# @summary Check if a token is valid +# @summary Log in using a token, but only if the token is valid # @function # @protected # @@ -50,23 +50,26 @@ exports.getDashboardLoginURL = (callbackUrl) -> # This function checks that the token is not only well-structured # but that it also authenticates with the server successfully. # -# @param {String} sessionToken - token -# @fulfil {Boolean} - whether is valid or not +# If authenticated, the token is persisted, if not then the previous +# login state is restored. +# +# @param {String} token - session token or api key +# @fulfil {Boolean} - whether the login was successful or not # @returns {Promise} # -# utils.isTokenValid('...').then (isValid) -> -# if isValid +# utils.loginIfTokenValid('...').then (loggedIn) -> +# if loggedIn # console.log('Token is valid!') ### -exports.isTokenValid = (sessionToken) -> - if not sessionToken? or _.isEmpty(sessionToken.trim()) +exports.loginIfTokenValid = (token) -> + if not token? or _.isEmpty(token.trim()) return Promise.resolve(false) return resin.auth.getToken() .catchReturn(undefined) .then (currentToken) -> - resin.auth.loginWithToken(sessionToken) - .return(sessionToken) + resin.auth.loginWithToken(token) + .return(token) .then(resin.auth.isLoggedIn) .tap (isLoggedIn) -> return if isLoggedIn diff --git a/tests/auth/server.spec.coffee b/tests/auth/server.spec.coffee index 5f6fdc0a..cb597d12 100644 --- a/tests/auth/server.spec.coffee +++ b/tests/auth/server.spec.coffee @@ -51,11 +51,11 @@ describe 'Server:', -> describe 'given the token authenticates with the server', -> beforeEach -> - @utilsIsTokenValidStub = m.sinon.stub(utils, 'isTokenValid') - @utilsIsTokenValidStub.returns(Promise.resolve(true)) + @loginIfTokenValidStub = m.sinon.stub(utils, 'loginIfTokenValid') + @loginIfTokenValidStub.returns(Promise.resolve(true)) afterEach -> - @utilsIsTokenValidStub.restore() + @loginIfTokenValidStub.restore() it 'should eventually be the token', (done) -> promise = server.awaitForToken(options) @@ -74,11 +74,11 @@ describe 'Server:', -> describe 'given the token does not authenticate with the server', -> beforeEach -> - @utilsIsTokenValidStub = m.sinon.stub(utils, 'isTokenValid') - @utilsIsTokenValidStub.returns(Promise.resolve(false)) + @loginIfTokenValidStub = m.sinon.stub(utils, 'loginIfTokenValid') + @loginIfTokenValidStub.returns(Promise.resolve(false)) afterEach -> - @utilsIsTokenValidStub.restore() + @loginIfTokenValidStub.restore() it 'should be rejected', (done) -> promise = server.awaitForToken(options) diff --git a/tests/auth/utils.spec.coffee b/tests/auth/utils.spec.coffee index b2b6ed85..819f4ff7 100644 --- a/tests/auth/utils.spec.coffee +++ b/tests/auth/utils.spec.coffee @@ -43,22 +43,22 @@ describe 'Utils:', -> expectedUrl = "#{dashboardUrl}/login/cli/http%253A%252F%252F127.0.0.1%253A3000%252Fcallback" m.chai.expect(loginUrl).to.equal(expectedUrl) - describe '.isTokenValid()', -> + describe '.loginIfTokenValid()', -> it 'should eventually be false if token is undefined', -> - promise = utils.isTokenValid(undefined) + promise = utils.loginIfTokenValid(undefined) m.chai.expect(promise).to.eventually.be.false it 'should eventually be false if token is null', -> - promise = utils.isTokenValid(null) + promise = utils.loginIfTokenValid(null) m.chai.expect(promise).to.eventually.be.false it 'should eventually be false if token is an empty string', -> - promise = utils.isTokenValid('') + promise = utils.loginIfTokenValid('') m.chai.expect(promise).to.eventually.be.false it 'should eventually be false if token is a string containing only spaces', -> - promise = utils.isTokenValid(' ') + promise = utils.loginIfTokenValid(' ') m.chai.expect(promise).to.eventually.be.false describe 'given the token does not authenticate with the server', -> @@ -71,7 +71,7 @@ describe 'Utils:', -> @resinAuthIsLoggedInStub.restore() it 'should eventually be false', -> - promise = utils.isTokenValid(tokens.johndoe.token) + promise = utils.loginIfTokenValid(tokens.johndoe.token) m.chai.expect(promise).to.eventually.be.false describe 'given there was a token already', -> @@ -82,7 +82,7 @@ describe 'Utils:', -> it 'should preserve the old token', -> resin.auth.getToken().then (originalToken) -> m.chai.expect(originalToken).to.equal(tokens.janedoe.token) - return utils.isTokenValid(tokens.johndoe.token) + return utils.loginIfTokenValid(tokens.johndoe.token) .then(resin.auth.getToken).then (currentToken) -> m.chai.expect(currentToken).to.equal(tokens.janedoe.token) @@ -92,7 +92,7 @@ describe 'Utils:', -> resin.auth.logout() it 'should stay without a token', -> - utils.isTokenValid(tokens.johndoe.token).then -> + utils.loginIfTokenValid(tokens.johndoe.token).then -> resin.auth.isLoggedIn() .then (isLoggedIn) -> m.chai.expect(isLoggedIn).to.equal(false) @@ -107,5 +107,5 @@ describe 'Utils:', -> @resinAuthIsLoggedInStub.restore() it 'should eventually be true', -> - promise = utils.isTokenValid(tokens.johndoe.token) + promise = utils.loginIfTokenValid(tokens.johndoe.token) m.chai.expect(promise).to.eventually.be.true From 0829d3c1765c29f0e7d40001def3aad89b39887b Mon Sep 17 00:00:00 2001 From: "resin-io-versionbot[bot]" Date: Thu, 29 Mar 2018 10:09:08 +0000 Subject: [PATCH 7/7] v7.2.0 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffacc50c..f36a1d97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY! This project adheres to [Semantic Versioning](http://semver.org/). +## v7.2.0 - 2018-03-29 + +* Do not require a login for builds #835 [Tim Perry] +* Allow (experimental!) login with API keys #835 [Tim Perry] + ## v7.1.6 - 2018-03-29 * Fix build emulation for multi-stage builds #838 [Tim Perry] diff --git a/package.json b/package.json index 722701a5..63ca791e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "resin-cli", - "version": "7.1.6", + "version": "7.2.0", "description": "The official resin.io CLI tool", "main": "./build/actions/index.js", "homepage": "https://github.com/resin-io/resin-cli",