Skip to content

Commit

Permalink
Auto-merge for PR #835 via VersionBot
Browse files Browse the repository at this point in the history
Initial support for api keys in the CLI
  • Loading branch information
resin-io-versionbot[bot] committed Mar 29, 2018
2 parents b4526e9 + 0829d3c commit 989df9b
Show file tree
Hide file tree
Showing 11 changed files with 72 additions and 61 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions doc/cli.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -276,7 +276,7 @@ Examples:

#### --token, -t <token>

auth token
session token or API key (experimental)

#### --web, -w

Expand Down
2 changes: 1 addition & 1 deletion gulpfile.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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', [
Expand Down
10 changes: 5 additions & 5 deletions lib/actions/auth.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -40,7 +40,7 @@ exports.login =
options: [
{
signature: 'token'
description: 'auth token'
description: 'session token or API key (experimental)'
parameter: 'token'
alias: 't'
}
Expand Down Expand Up @@ -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')
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion lib/actions/build.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions lib/app.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
6 changes: 3 additions & 3 deletions lib/auth/server.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down
27 changes: 16 additions & 11 deletions lib/auth/utils.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -42,29 +42,34 @@ 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
#
# @description
# 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.token.get().then (currentToken) ->
resin.auth.loginWithToken(sessionToken)
.return(sessionToken)
return resin.auth.getToken()
.catchReturn(undefined)
.then (currentToken) ->
resin.auth.loginWithToken(token)
.return(token)
.then(resin.auth.isLoggedIn)
.tap (isLoggedIn) ->
return if isLoggedIn
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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"
},
Expand Down
12 changes: 6 additions & 6 deletions tests/auth/server.spec.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
56 changes: 28 additions & 28 deletions tests/auth/utils.spec.coffee
Original file line number Diff line number Diff line change
@@ -1,64 +1,64 @@
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()', ->

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()', ->
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', ->
Expand All @@ -71,31 +71,31 @@ 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', ->

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)
return utils.loginIfTokenValid(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) ->
utils.isTokenValid(tokens.johndoe.token).then ->
m.chai.expect(resin.token.get()).to.eventually.not.exist
.nodeify(done)
it 'should stay without a token', ->
utils.loginIfTokenValid(tokens.johndoe.token).then ->
resin.auth.isLoggedIn()
.then (isLoggedIn) ->
m.chai.expect(isLoggedIn).to.equal(false)

describe 'given the token does authenticate with the server', ->

Expand All @@ -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

0 comments on commit 989df9b

Please sign in to comment.