remove [github] admin routes (#7105)
This commit is contained in:
@@ -93,7 +93,6 @@ private:
|
|||||||
obs_pass: 'OBS_PASS'
|
obs_pass: 'OBS_PASS'
|
||||||
redis_url: 'REDIS_URL'
|
redis_url: 'REDIS_URL'
|
||||||
sentry_dsn: 'SENTRY_DSN'
|
sentry_dsn: 'SENTRY_DSN'
|
||||||
shields_secret: 'SHIELDS_SECRET'
|
|
||||||
sl_insight_userUuid: 'SL_INSIGHT_USER_UUID'
|
sl_insight_userUuid: 'SL_INSIGHT_USER_UUID'
|
||||||
sl_insight_apiToken: 'SL_INSIGHT_API_TOKEN'
|
sl_insight_apiToken: 'SL_INSIGHT_API_TOKEN'
|
||||||
sonarqube_token: 'SONARQUBE_TOKEN'
|
sonarqube_token: 'SONARQUBE_TOKEN'
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
function constEq(a, b) {
|
|
||||||
if (a.length !== b.length) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
let zero = 0
|
|
||||||
for (let i = 0; i < a.length; i++) {
|
|
||||||
zero |= a.charCodeAt(i) ^ b.charCodeAt(i)
|
|
||||||
}
|
|
||||||
return zero === 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeSecretIsValid(shieldsSecret) {
|
|
||||||
return function secretIsValid(secret = '') {
|
|
||||||
return shieldsSecret && constEq(secret, shieldsSecret)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { makeSecretIsValid }
|
|
||||||
@@ -177,7 +177,6 @@ const privateConfigSchema = Joi.object({
|
|||||||
obs_pass: Joi.string(),
|
obs_pass: Joi.string(),
|
||||||
redis_url: Joi.string().uri({ scheme: ['redis', 'rediss'] }),
|
redis_url: Joi.string().uri({ scheme: ['redis', 'rediss'] }),
|
||||||
sentry_dsn: Joi.string(),
|
sentry_dsn: Joi.string(),
|
||||||
shields_secret: Joi.string(),
|
|
||||||
sl_insight_userUuid: Joi.string(),
|
sl_insight_userUuid: Joi.string(),
|
||||||
sl_insight_apiToken: Joi.string(),
|
sl_insight_apiToken: Joi.string(),
|
||||||
sonarqube_token: Joi.string(),
|
sonarqube_token: Joi.string(),
|
||||||
|
|||||||
@@ -328,29 +328,6 @@ class TokenPool {
|
|||||||
this.fifoQueue.forEach(visit)
|
this.fifoQueue.forEach(visit)
|
||||||
this.priorityQueue.forEach(visit)
|
this.priorityQueue.forEach(visit)
|
||||||
}
|
}
|
||||||
|
|
||||||
allValidTokenIds() {
|
|
||||||
const result = []
|
|
||||||
this.forEach(({ id }) => result.push(id))
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
serializeDebugInfo({ sanitize = true } = {}) {
|
|
||||||
const maybeSanitize = sanitize ? id => sanitizeToken(id) : id => id
|
|
||||||
|
|
||||||
const priorityQueue = []
|
|
||||||
this.priorityQueue.forEach(t =>
|
|
||||||
priorityQueue.push(t.getDebugInfo({ sanitize }))
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
utcEpochSeconds: getUtcEpochSeconds(),
|
|
||||||
allValidTokenIds: this.allValidTokenIds().map(maybeSanitize),
|
|
||||||
fifoQueue: this.fifoQueue.map(t => t.getDebugInfo({ sanitize })),
|
|
||||||
priorityQueue,
|
|
||||||
sanitized: sanitize,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { sanitizeToken, Token, TokenPool }
|
export { sanitizeToken, Token, TokenPool }
|
||||||
|
|||||||
@@ -19,10 +19,6 @@ describe('The token pool', function () {
|
|||||||
ids.forEach(id => tokenPool.add(id))
|
ids.forEach(id => tokenPool.add(id))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('allValidTokenIds() should return the full list', function () {
|
|
||||||
expect(tokenPool.allValidTokenIds()).to.deep.equal(ids)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should yield the expected tokens', function () {
|
it('should yield the expected tokens', function () {
|
||||||
ids.forEach(id =>
|
ids.forEach(id =>
|
||||||
times(batchSize, () => expect(tokenPool.next().id).to.equal(id))
|
times(batchSize, () => expect(tokenPool.next().id).to.equal(id))
|
||||||
@@ -38,67 +34,6 @@ describe('The token pool', function () {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('serializeDebugInfo should initially return the expected', function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
sinon.useFakeTimers({ now: 1544307744484 })
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(function () {
|
|
||||||
sinon.restore()
|
|
||||||
})
|
|
||||||
|
|
||||||
context('sanitize is not specified', function () {
|
|
||||||
it('returns fully sanitized results', function () {
|
|
||||||
// This is `sha()` of '1', '2', '3', '4', '5'. These are written
|
|
||||||
// literally for avoidance of doubt as to whether sanitization is
|
|
||||||
// happening.
|
|
||||||
const sanitizedIds = [
|
|
||||||
'6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b',
|
|
||||||
'd4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35',
|
|
||||||
'4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce',
|
|
||||||
'4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a',
|
|
||||||
'ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d',
|
|
||||||
]
|
|
||||||
|
|
||||||
expect(tokenPool.serializeDebugInfo()).to.deep.equal({
|
|
||||||
allValidTokenIds: sanitizedIds,
|
|
||||||
priorityQueue: [],
|
|
||||||
fifoQueue: sanitizedIds.map(id => ({
|
|
||||||
data: '[redacted]',
|
|
||||||
id,
|
|
||||||
isFrozen: false,
|
|
||||||
isValid: true,
|
|
||||||
nextReset: Token.nextResetNever,
|
|
||||||
usesRemaining: batchSize,
|
|
||||||
})),
|
|
||||||
sanitized: true,
|
|
||||||
utcEpochSeconds: 1544307744,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
context('with sanitize: false', function () {
|
|
||||||
it('returns unsanitized results', function () {
|
|
||||||
expect(tokenPool.serializeDebugInfo({ sanitize: false })).to.deep.equal(
|
|
||||||
{
|
|
||||||
allValidTokenIds: ids,
|
|
||||||
priorityQueue: [],
|
|
||||||
fifoQueue: ids.map(id => ({
|
|
||||||
data: undefined,
|
|
||||||
id,
|
|
||||||
isFrozen: false,
|
|
||||||
isValid: true,
|
|
||||||
nextReset: Token.nextResetNever,
|
|
||||||
usesRemaining: batchSize,
|
|
||||||
})),
|
|
||||||
sanitized: false,
|
|
||||||
utcEpochSeconds: 1544307744,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
context('tokens are marked exhausted immediately', function () {
|
context('tokens are marked exhausted immediately', function () {
|
||||||
it('should be exhausted', function () {
|
it('should be exhausted', function () {
|
||||||
ids.forEach(() => {
|
ids.forEach(() => {
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
#!/usr/bin/env fish
|
|
||||||
#
|
|
||||||
# Back up the GitHub tokens from each production server.
|
|
||||||
#
|
|
||||||
|
|
||||||
if test (count $argv) -lt 1
|
|
||||||
echo Usage: (basename (status -f)) shields_secret
|
|
||||||
end
|
|
||||||
|
|
||||||
set shields_secret $argv[1]
|
|
||||||
|
|
||||||
function do_backup
|
|
||||||
set server $argv[1]
|
|
||||||
curl --insecure -u ":$shields_secret" "https://$server.servers.shields.io/\$github-auth/tokens" > "$server""_tokens.json"
|
|
||||||
end
|
|
||||||
|
|
||||||
for server in s0 s1 s2
|
|
||||||
do_backup $server
|
|
||||||
end
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { makeSecretIsValid } from '../../../core/server/secret-is-valid.js'
|
|
||||||
|
|
||||||
function setRoutes({ shieldsSecret }, { apiProvider, server }) {
|
|
||||||
const secretIsValid = makeSecretIsValid(shieldsSecret)
|
|
||||||
|
|
||||||
// Allow the admin to obtain the tokens for operational and debugging
|
|
||||||
// purposes. This could be used to:
|
|
||||||
//
|
|
||||||
// - Ensure tokens have been propagated to all servers
|
|
||||||
// - Debug GitHub badge failures
|
|
||||||
//
|
|
||||||
// The admin can authenticate with HTTP Basic Auth, with an empty/any
|
|
||||||
// username and the shields secret in the password and an empty/any
|
|
||||||
// password.
|
|
||||||
//
|
|
||||||
// e.g.
|
|
||||||
// curl --insecure -u ':very-very-secret' 'https://img.shields.io/$github-auth/tokens'
|
|
||||||
server.ajax.on('github-auth/tokens', (json, end, ask) => {
|
|
||||||
if (!secretIsValid(ask.password)) {
|
|
||||||
// An unknown entity tries to connect. Let the connection linger for a minute.
|
|
||||||
return setTimeout(() => {
|
|
||||||
ask.res.statusCode = 401
|
|
||||||
ask.res.setHeader('Cache-Control', 'private')
|
|
||||||
end('Invalid secret.')
|
|
||||||
}, 10000)
|
|
||||||
}
|
|
||||||
ask.res.setHeader('Cache-Control', 'private')
|
|
||||||
end(apiProvider.serializeDebugInfo({ sanitize: false }))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export { setRoutes }
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import { expect } from 'chai'
|
|
||||||
import Camp from '@shields_io/camp'
|
|
||||||
import portfinder from 'portfinder'
|
|
||||||
import got from '../../../core/got-test-client.js'
|
|
||||||
import GithubApiProvider from '../github-api-provider.js'
|
|
||||||
import { setRoutes } from './admin.js'
|
|
||||||
|
|
||||||
describe('GitHub admin route', function () {
|
|
||||||
const shieldsSecret = '7'.repeat(40)
|
|
||||||
|
|
||||||
let port, baseUrl
|
|
||||||
before(async function () {
|
|
||||||
port = await portfinder.getPortPromise()
|
|
||||||
baseUrl = `http://127.0.0.1:${port}`
|
|
||||||
})
|
|
||||||
|
|
||||||
let camp
|
|
||||||
before(async function () {
|
|
||||||
camp = Camp.start({ port, hostname: '::' })
|
|
||||||
await new Promise(resolve => camp.on('listening', () => resolve()))
|
|
||||||
})
|
|
||||||
after(async function () {
|
|
||||||
if (camp) {
|
|
||||||
await new Promise(resolve => camp.close(resolve))
|
|
||||||
camp = undefined
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
before(function () {
|
|
||||||
const apiProvider = new GithubApiProvider({ withPooling: true })
|
|
||||||
setRoutes({ shieldsSecret }, { apiProvider, server: camp })
|
|
||||||
})
|
|
||||||
|
|
||||||
context('the password is correct', function () {
|
|
||||||
it('returns a valid JSON response', async function () {
|
|
||||||
const { statusCode, body, headers } = await got(
|
|
||||||
`${baseUrl}/$github-auth/tokens`,
|
|
||||||
{
|
|
||||||
username: '',
|
|
||||||
password: shieldsSecret,
|
|
||||||
responseType: 'json',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
expect(statusCode).to.equal(200)
|
|
||||||
expect(body).to.be.ok
|
|
||||||
expect(headers['cache-control']).to.equal('private')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Disabled because this code isn't modified often and the test is very
|
|
||||||
// slow. To run it, run `SLOW=true npm run test:core`
|
|
||||||
//
|
|
||||||
// I wasn't able to make this work with fake timers:
|
|
||||||
// https://github.com/sinonjs/sinon/issues/1739
|
|
||||||
if (process.env.SLOW) {
|
|
||||||
context('the password is missing', function () {
|
|
||||||
it('returns the expected message', async function () {
|
|
||||||
this.timeout(11000)
|
|
||||||
const { statusCode, body, headers } = await got(
|
|
||||||
`${baseUrl}/$github-auth/tokens`,
|
|
||||||
{
|
|
||||||
throwHttpErrors: false,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
expect(statusCode).to.equal(401)
|
|
||||||
expect(body).to.equal('"Invalid secret."')
|
|
||||||
expect(headers['cache-control']).to.equal('private')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -54,18 +54,6 @@ class GithubApiProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serializeDebugInfo({ sanitize = true } = {}) {
|
|
||||||
if (this.withPooling) {
|
|
||||||
return {
|
|
||||||
standardTokens: this.standardTokens.serializeDebugInfo({ sanitize }),
|
|
||||||
searchTokens: this.searchTokens.serializeDebugInfo({ sanitize }),
|
|
||||||
graphqlTokens: this.graphqlTokens.serializeDebugInfo({ sanitize }),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addToken(tokenString) {
|
addToken(tokenString) {
|
||||||
if (this.withPooling) {
|
if (this.withPooling) {
|
||||||
this.standardTokens.add(tokenString)
|
this.standardTokens.add(tokenString)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { AuthHelper } from '../../core/base-service/auth-helper.js'
|
|||||||
import RedisTokenPersistence from '../../core/token-pooling/redis-token-persistence.js'
|
import RedisTokenPersistence from '../../core/token-pooling/redis-token-persistence.js'
|
||||||
import log from '../../core/server/log.js'
|
import log from '../../core/server/log.js'
|
||||||
import GithubApiProvider from './github-api-provider.js'
|
import GithubApiProvider from './github-api-provider.js'
|
||||||
import { setRoutes as setAdminRoutes } from './auth/admin.js'
|
|
||||||
import { setRoutes as setAcceptorRoutes } from './auth/acceptor.js'
|
import { setRoutes as setAcceptorRoutes } from './auth/acceptor.js'
|
||||||
|
|
||||||
// Convenience class with all the stuff related to the Github API and its
|
// Convenience class with all the stuff related to the Github API and its
|
||||||
@@ -23,7 +22,6 @@ class GithubConstellation {
|
|||||||
constructor(config) {
|
constructor(config) {
|
||||||
this._debugEnabled = config.service.debug.enabled
|
this._debugEnabled = config.service.debug.enabled
|
||||||
this._debugIntervalSeconds = config.service.debug.intervalSeconds
|
this._debugIntervalSeconds = config.service.debug.intervalSeconds
|
||||||
this.shieldsSecret = config.private.shields_secret
|
|
||||||
|
|
||||||
const { redis_url: redisUrl, gh_token: globalToken } = config.private
|
const { redis_url: redisUrl, gh_token: globalToken } = config.private
|
||||||
if (redisUrl) {
|
if (redisUrl) {
|
||||||
@@ -74,9 +72,6 @@ class GithubConstellation {
|
|||||||
this.apiProvider.addToken(tokenString)
|
this.apiProvider.addToken(tokenString)
|
||||||
})
|
})
|
||||||
|
|
||||||
const { shieldsSecret, apiProvider } = this
|
|
||||||
setAdminRoutes({ shieldsSecret }, { apiProvider, server })
|
|
||||||
|
|
||||||
if (this.oauthHelper.isConfigured) {
|
if (this.oauthHelper.isConfigured) {
|
||||||
setAcceptorRoutes({
|
setAcceptorRoutes({
|
||||||
server,
|
server,
|
||||||
|
|||||||
Reference in New Issue
Block a user