Migrate [Discord] implementation to use bot token (#5346)

* Migrate [Discord] implementation to use bot token

* Rework authorization field creation

* Revert "Rework authorization field creation"

This reverts commit caf65bde5d.

* Add LGTM exclusion for hardcoded credentials
This commit is contained in:
Pierre-Yves B
2020-07-24 18:04:12 +02:00
committed by GitHub
parent c85512997c
commit 135b842946
10 changed files with 90 additions and 45 deletions

View File

@@ -64,8 +64,6 @@ public:
fetchLimit: 'FETCH_LIMIT'
shieldsProductionHerokuHacks: 'SHIELDS_PRODUCTION_HEROKU_HACKS'
private:
azure_devops_token: 'AZURE_DEVOPS_TOKEN'
bintray_user: 'BINTRAY_USER'
@@ -74,6 +72,7 @@ private:
bitbucket_password: 'BITBUCKET_PASS'
bitbucket_server_username: 'BITBUCKET_SERVER_USER'
bitbucket_server_password: 'BITBUCKET_SERVER_PASS'
discord_bot_token: 'DISCORD_BOT_TOKEN'
drone_token: 'DRONE_TOKEN'
gh_client_id: 'GH_CLIENT_ID'
gh_client_secret: 'GH_CLIENT_SECRET'

View File

@@ -36,6 +36,4 @@ public:
fetchLimit: '10MB'
shieldsProductionHerokuHacks: false
private: {}

View File

@@ -1,5 +1,6 @@
private:
# These are the keys which are set on the production servers.
discord_bot_token: ...
gh_client_id: ...
gh_client_secret: ...
redis_url: ...

View File

@@ -146,9 +146,11 @@ class AuthHelper {
)
}
get _bearerAuthHeader() {
_bearerAuthHeader(bearerKey) {
const { _pass: pass } = this
return this.isConfigured ? { Authorization: `Bearer ${pass}` } : undefined
return this.isConfigured
? { Authorization: `${bearerKey} ${pass}` }
: undefined
}
static _mergeHeaders(requestParams, headers) {
@@ -168,9 +170,15 @@ class AuthHelper {
}
}
withBearerAuthHeader(requestParams) {
withBearerAuthHeader(
requestParams,
bearerKey = 'Bearer' // lgtm [js/hardcoded-credentials]
) {
return this._withAnyAuth(requestParams, requestParams =>
this.constructor._mergeHeaders(requestParams, this._bearerAuthHeader)
this.constructor._mergeHeaders(
requestParams,
this._bearerAuthHeader(bearerKey)
)
)
}

View File

@@ -148,13 +148,13 @@ const publicConfigSchema = Joi.object({
rateLimit: Joi.boolean().required(),
handleInternalErrors: Joi.boolean().required(),
fetchLimit: Joi.string().regex(/^[0-9]+(b|kb|mb|gb|tb)$/i),
shieldsProductionHerokuHacks: Joi.boolean(),
}).required()
const privateConfigSchema = Joi.object({
azure_devops_token: Joi.string(),
bintray_user: Joi.string(),
bintray_apikey: Joi.string(),
discord_bot_token: Joi.string(),
drone_token: Joi.string(),
gh_client_id: Joi.string(),
gh_client_secret: Joi.string(),
@@ -392,8 +392,6 @@ class Server {
rasterUrl: config.public.rasterUrl,
private: config.private,
public: config.public,
shieldsProductionHerokuHacks:
config.public.shieldsProductionHerokuHacks,
}
)
)

View File

@@ -34,6 +34,7 @@ Production hosting is managed by the Shields ops team:
| Cloudflare (CDN) | Access management | @espadrine |
| Cloudflare (CDN) | Admin access | @calebcartwright, @chris48s, @espadrine, @paulmelnikow, @PyvesB |
| Twitch | OAuth app | @PyvesB |
| Discord | OAuth app | @PyvesB |
| YouTube | Account owner | @PyvesB |
| OpenStreetMap (for Wheelmap) | Account owner | @paulmelnikow |
| DNS | Account owner | @olivierlacan |

View File

@@ -105,6 +105,15 @@ self-hosted Shields installation access to private repositories hosted on bitbuc
Bitbucket badges use basic auth. Provide a username and password to give your
self-hosted Shields installation access to a private Bitbucket Server instance.
### Discord
Using a token for Dicsord is optional but will allow higher API rates.
- `DISCORD_BOT_TOKEN` (yml: `discord_bot_token`)
Register an application in the [Discord developer console](https://discord.com/developers).
To obtain a token, simply create a bot for your application.
### Drone
- `DRONE_ORIGINS` (yml: `public.services.drone.authorizedOrigins`)

View File

@@ -4,15 +4,10 @@ const Joi = require('@hapi/joi')
const { nonNegativeInteger } = require('../validators')
const { BaseJsonService } = require('..')
const discordSchema = Joi.object({
const schema = Joi.object({
presence_count: nonNegativeInteger,
}).required()
const proxySchema = Joi.object({
message: Joi.string().required(),
color: Joi.string().required(),
}).required()
const documentation = `
<p>
The Discord badge requires the <code>SERVER ID</code> in order access the Discord JSON API.
@@ -41,6 +36,14 @@ module.exports = class Discord extends BaseJsonService {
}
}
static get auth() {
return {
passKey: 'discord_bot_token',
authorizedOrigins: ['https://discord.com'],
isRequired: false,
}
}
static get examples() {
return [
{
@@ -67,36 +70,24 @@ module.exports = class Discord extends BaseJsonService {
}
}
constructor(context, config) {
super(context, config)
this._shieldsProductionHerokuHacks = config.shieldsProductionHerokuHacks
}
async fetch({ serverId }) {
const url = `https://discord.com/api/guilds/${serverId}/widget.json`
return this._requestJson({
url,
schema: discordSchema,
errorMessages: {
404: 'invalid server',
403: 'widget disabled',
},
})
}
async fetchOvhProxy({ serverId }) {
return this._requestJson({
url: `https://legacy-img.shields.io/discord/${serverId}.json`,
schema: proxySchema,
})
const url = `https://discord.com/api/v6/guilds/${serverId}/widget.json`
return this._requestJson(
this.authHelper.withBearerAuthHeader(
{
url,
schema,
errorMessages: {
404: 'invalid server',
403: 'widget disabled',
},
},
'Bot'
)
)
}
async handle({ serverId }) {
if (this._shieldsProductionHerokuHacks) {
const { message, color } = await this.fetchOvhProxy({ serverId })
return { message, color }
}
const data = await this.fetch({ serverId })
return this.constructor.render({ members: data.presence_count })
}

View File

@@ -0,0 +1,40 @@
'use strict'
const { expect } = require('chai')
const nock = require('nock')
const { cleanUpNockAfterEach, defaultContext } = require('../test-helpers')
const Discord = require('./discord.service')
describe('Discord', function () {
cleanUpNockAfterEach()
it('sends the auth information as configured', async function () {
const pass = 'password'
const config = {
private: {
discord_bot_token: pass,
},
}
const scope = nock(`https://discord.com`, {
// This ensures that the expected credential is actually being sent with the HTTP request.
// Without this the request wouldn't match and the test would fail.
reqheaders: { Authorization: `Bot password` },
})
.get(`/api/v6/guilds/12345/widget.json`)
.reply(200, {
presence_count: 125,
})
expect(
await Discord.invoke(defaultContext, config, {
serverId: '12345',
})
).to.deep.equal({
message: '125 online',
color: 'brightgreen',
})
scope.done()
})
})

View File

@@ -19,7 +19,7 @@ t.create('widget disabled')
.get('/12345.json')
.intercept(nock =>
nock('https://discord.com/')
.get('/api/guilds/12345/widget.json')
.get('/api/v6/guilds/12345/widget.json')
.reply(403, {
code: 50004,
message: 'Widget Disabled',
@@ -31,7 +31,7 @@ t.create('server error')
.get('/12345.json')
.intercept(nock =>
nock('https://discord.com/')
.get('/api/guilds/12345/widget.json')
.get('/api/v6/guilds/12345/widget.json')
.reply(500, 'Something broke')
)
.expectBadge({ label: 'chat', message: 'inaccessible' })