Skip to content

Commit

Permalink
Merge branch 'master' into simple-icons-names
Browse files Browse the repository at this point in the history
  • Loading branch information
RedSparr0w authored Jul 10, 2019
2 parents 1357031 + ce0ddf9 commit 8e3ae53
Show file tree
Hide file tree
Showing 217 changed files with 2,509 additions and 1,957 deletions.
8 changes: 0 additions & 8 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,14 +163,6 @@ package_steps: &package_steps
NODE_VERSION: v10
name: Run package tests on Node 10

- run:
<<: *run_package_tests
environment:
mocha_reporter: mocha-junit-reporter
MOCHA_FILE: junit/gh-badges/v11/results.xml
NODE_VERSION: v11
name: Run package tests on Node 11

- run:
<<: *run_package_tests
environment:
Expand Down
1 change: 1 addition & 0 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ overrides:
'category',
'isDeprecated',
'route',
'auth',
'examples',
'_cacheLength',
'defaultBadgeData',
Expand Down
2 changes: 0 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
FROM node:8-alpine

RUN apk add --no-cache gettext imagemagick librsvg git

RUN mkdir -p /usr/src/app
RUN mkdir /usr/src/app/private
WORKDIR /usr/src/app
Expand Down
2 changes: 2 additions & 0 deletions config/custom-environment-variables.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public:

redirectUri: 'REDIRECT_URI'

rasterUrl: 'RASTER_URL'

cors:
allowedOrigin:
__name: 'ALLOWED_ORIGIN'
Expand Down
2 changes: 2 additions & 0 deletions config/shields-io-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public:

redirectUrl: 'https://shields.io/'

rasterUrl: 'https://raster.shields.io'

private:
# These are not really private; they should be moved to `public`.
shields_ips: ['192.99.59.72', '51.254.114.150', '149.56.96.133']
4 changes: 3 additions & 1 deletion config/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ public:

rateLimit: false

redirectUrl: 'http://badge-server.example.com'
redirectUrl: 'http://frontend.example.test'

rasterUrl: 'http://raster.example.test'

handleInternalErrors: false
11 changes: 11 additions & 0 deletions core/badge-urls/make-badge-url.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'

const { URL } = require('url')
const queryString = require('query-string')
const pathToRegexp = require('path-to-regexp')

Expand Down Expand Up @@ -134,11 +135,21 @@ function dynamicBadgeUrl({
return `${baseUrl}/badge/dynamic/${datatype}.${format}?${outQueryString}`
}

function rasterRedirectUrl({ rasterUrl }, badgeUrl) {
// Ensure we're always using the `rasterUrl` by using just the path from
// the request URL.
const { pathname, search } = new URL(badgeUrl, 'https://bogus.test')
const result = new URL(pathname, rasterUrl)
result.search = search
return result
}

module.exports = {
badgeUrlFromPath,
badgeUrlFromPattern,
encodeField,
staticBadgeUrl,
queryStringStaticBadgeUrl,
dynamicBadgeUrl,
rasterRedirectUrl,
}
43 changes: 43 additions & 0 deletions core/base-service/auth-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict'

class AuthHelper {
constructor({ userKey, passKey, isRequired = false }, privateConfig) {
if (!userKey && !passKey) {
throw Error('Expected userKey or passKey to be set')
}

this._userKey = userKey
this._passKey = passKey
this.user = userKey ? privateConfig[userKey] : undefined
this.pass = passKey ? privateConfig[passKey] : undefined
this.isRequired = isRequired
}

get isConfigured() {
return (
(this._userKey ? Boolean(this.user) : true) &&
(this._passKey ? Boolean(this.pass) : true)
)
}

get isValid() {
if (this.isRequired) {
return this.isConfigured
} else {
const configIsEmpty = !this.user && !this.pass
return this.isConfigured || configIsEmpty
}
}

get basicAuth() {
const { user, pass } = this
return this.isConfigured ? { user, pass } : undefined
}

get bearerAuthHeader() {
const { pass } = this
return this.isConfigured ? { Authorization: `Bearer ${pass}` } : undefined
}
}

module.exports = { AuthHelper }
96 changes: 96 additions & 0 deletions core/base-service/auth-helper.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
'use strict'

const { expect } = require('chai')
const { test, given, forCases } = require('sazerac')
const { AuthHelper } = require('./auth-helper')

describe('AuthHelper', function() {
it('throws without userKey or passKey', function() {
expect(() => new AuthHelper({}, {})).to.throw(
Error,
'Expected userKey or passKey to be set'
)
})

describe('isValid', function() {
function validate(config, privateConfig) {
return new AuthHelper(config, privateConfig).isValid
}
test(validate, () => {
forCases([
// Fully configured user + pass.
given(
{ userKey: 'myci_user', passKey: 'myci_pass', isRequired: true },
{ myci_user: 'admin', myci_pass: 'abc123' }
),
given(
{ userKey: 'myci_user', passKey: 'myci_pass' },
{ myci_user: 'admin', myci_pass: 'abc123' }
),
// Fully configured user or pass.
given(
{ userKey: 'myci_user', isRequired: true },
{ myci_user: 'admin' }
),
given(
{ passKey: 'myci_pass', isRequired: true },
{ myci_pass: 'abc123' }
),
given({ userKey: 'myci_user' }, { myci_user: 'admin' }),
given({ passKey: 'myci_pass' }, { myci_pass: 'abc123' }),
// Empty config.
given({ userKey: 'myci_user', passKey: 'myci_pass' }, {}),
given({ userKey: 'myci_user' }, {}),
given({ passKey: 'myci_pass' }, {}),
]).expect(true)

forCases([
// Partly configured.
given(
{ userKey: 'myci_user', passKey: 'myci_pass', isRequired: true },
{ myci_user: 'admin' }
),
given(
{ userKey: 'myci_user', passKey: 'myci_pass' },
{ myci_user: 'admin' }
),
// Missing required config.
given(
{ userKey: 'myci_user', passKey: 'myci_pass', isRequired: true },
{}
),
given({ userKey: 'myci_user', isRequired: true }, {}),
given({ passKey: 'myci_pass', isRequired: true }, {}),
]).expect(false)
})
})

describe('basicAuth', function() {
function validate(config, privateConfig) {
return new AuthHelper(config, privateConfig).basicAuth
}
test(validate, () => {
forCases([
given(
{ userKey: 'myci_user', passKey: 'myci_pass', isRequired: true },
{ myci_user: 'admin', myci_pass: 'abc123' }
),
given(
{ userKey: 'myci_user', passKey: 'myci_pass' },
{ myci_user: 'admin', myci_pass: 'abc123' }
),
]).expect({ user: 'admin', pass: 'abc123' })
given({ userKey: 'myci_user' }, { myci_user: 'admin' }).expect({
user: 'admin',
pass: undefined,
})
given({ passKey: 'myci_pass' }, { myci_pass: 'abc123' }).expect({
user: undefined,
pass: 'abc123',
})
given({ userKey: 'myci_user', passKey: 'myci_pass' }, {}).expect(
undefined
)
})
})
})
51 changes: 48 additions & 3 deletions core/base-service/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ const decamelize = require('decamelize')
// See available emoji at http://emoji.muan.co/
const emojic = require('emojic')
const Joi = require('@hapi/joi')
const { AuthHelper } = require('./auth-helper')
const { assertValidCategory } = require('./categories')
const checkErrorResponse = require('./check-error-response')
const coalesceBadge = require('./coalesce-badge')
const {
NotFound,
InvalidResponse,
Inaccessible,
ImproperlyConfigured,
InvalidParameter,
Deprecated,
} = require('./errors')
Expand Down Expand Up @@ -132,6 +134,33 @@ module.exports = class BaseService {
throw new Error(`Route not defined for ${this.name}`)
}

/**
* Configuration for the authentication helper that prepares credentials
* for upstream requests.
*
* @abstract
* @return {object} auth
* @return {string} auth.userKey
* (Optional) The key from `privateConfig` to use as the username.
* @return {string} auth.passKey
* (Optional) The key from `privateConfig` to use as the password.
* If auth is configured, either `userKey` or `passKey` is required.
* @return {string} auth.isRequired
* (Optional) If `true`, the service will return `NotFound` unless the
* configured credentials are present.
*
* See also the config schema in `./server.js` and `doc/server-secrets.md`.
*
* To use the configured auth in the handler or fetch method, pass the
* credentials to the request. For example:
* `{ options: { auth: this.authHelper.basicAuth } }`
* `{ options: { headers: this.authHelper.bearerAuthHeader } }`
* `{ options: { qs: { token: this.authHelper.pass } } }`
*/
static get auth() {
return undefined
}

/**
* Example URLs for this service. These should use the format
* specified in `route`, and can be used to demonstrate how to use badges for
Expand Down Expand Up @@ -238,8 +267,9 @@ module.exports = class BaseService {
return result
}

constructor({ sendAndCacheRequest }, { handleInternalErrors }) {
constructor({ sendAndCacheRequest, authHelper }, { handleInternalErrors }) {
this._requestFetcher = sendAndCacheRequest
this.authHelper = authHelper
this._handleInternalErrors = handleInternalErrors
}

Expand Down Expand Up @@ -303,6 +333,7 @@ module.exports = class BaseService {
color: 'red',
}
} else if (
error instanceof ImproperlyConfigured ||
error instanceof InvalidResponse ||
error instanceof Inaccessible ||
error instanceof Deprecated
Expand Down Expand Up @@ -353,12 +384,26 @@ module.exports = class BaseService {
trace.logTrace('inbound', emojic.ticket, 'Named params', namedParams)
trace.logTrace('inbound', emojic.crayon, 'Query params', queryParams)

const serviceInstance = new this(context, config)
// Like the service instance, the auth helper could be reused for each request.
// However, moving its instantiation to `register()` makes `invoke()` harder
// to test.
const authHelper = this.auth
? new AuthHelper(this.auth, config.private)
: undefined

const serviceInstance = new this({ ...context, authHelper }, config)

let serviceError
if (authHelper && !authHelper.isValid) {
const prettyMessage = authHelper.isRequired
? 'credentials have not been configured'
: 'credentials are misconfigured'
serviceError = new ImproperlyConfigured({ prettyMessage })
}

const { queryParamSchema } = this.route
let transformedQueryParams
if (queryParamSchema) {
if (!serviceError && queryParamSchema) {
try {
transformedQueryParams = validate(
{
Expand Down
43 changes: 41 additions & 2 deletions core/base-service/base.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class DummyService extends BaseService {
}

describe('BaseService', function() {
const defaultConfig = { handleInternalErrors: false }
const defaultConfig = { handleInternalErrors: false, private: {} }

it('Invokes the handler as expected', async function() {
expect(
Expand Down Expand Up @@ -316,7 +316,7 @@ describe('BaseService', function() {
})

describe('ScoutCamp integration', function() {
const expectedRouteRegex = /^\/foo\/([^/]+?)\.(svg|png|gif|jpg|json)$/
const expectedRouteRegex = /^\/foo\/([^/]+?)\.(svg|json)$/

let mockCamp
let mockHandleRequest
Expand Down Expand Up @@ -482,4 +482,43 @@ describe('BaseService', function() {
}
})
})

describe('auth', function() {
class AuthService extends DummyService {
static get auth() {
return {
passKey: 'myci_pass',
isRequired: true,
}
}

async handle() {
return {
message: `The CI password is ${this.authHelper.pass}`,
}
}
}

it('when auth is configured properly, invoke() sets authHelper', async function() {
expect(
await AuthService.invoke(
{},
{ defaultConfig, private: { myci_pass: 'abc123' } },
{ namedParamA: 'bar.bar.bar' }
)
).to.deep.equal({ message: 'The CI password is abc123' })
})

it('when auth is not configured properly, invoke() returns inacessible', async function() {
expect(
await AuthService.invoke({}, defaultConfig, {
namedParamA: 'bar.bar.bar',
})
).to.deep.equal({
color: 'lightgray',
isError: true,
message: 'credentials have not been configured',
})
})
})
})
Loading

0 comments on commit 8e3ae53

Please sign in to comment.