Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ default_permissions:
# https://developer.github.com/v3/apps/permissions/#permission-on-members
members: write

# View organization roles used to identify security manager teams.
# https://docs.github.com/en/rest/orgs/organization-roles
organization_custom_roles: read

# View and manage users blocked by the organization.
# https://developer.github.com/v3/apps/permissions/#permission-on-organization-user-blocking
# organization_user_blocking: read
Expand Down
1 change: 1 addition & 0 deletions docs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ Every deployment will need an [App](https://developer.github.com/apps/).
#### Organization Permissions
- Administration: **Read & Write**
- Custom organization roles: **Read-only**
- Custom properties: **Admin**
- Members: **Read & Write**
Expand Down
109 changes: 80 additions & 29 deletions lib/plugins/teams.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ const Diffable = require('./diffable')
const NopCommand = require('../nopcommand')

const teamRepoEndpoint = '/orgs/:owner/teams/:team_slug/repos/:owner/:repo'
const securityManagerRoleName = 'security_manager'
const safeSecurityManagerStatuses = [403, 404, 422]
module.exports = class Teams extends Diffable {
async find () {
this.skipTeamDeletion = false
this.log.debug(`Finding teams for ${this.repo.owner}/${this.repo.repo}`)
return this.github.paginate(this.github.rest.repos.listTeams, this.repo).then(res => {
this.log.debug(`Found teams ${JSON.stringify(res)}`)
Expand All @@ -14,47 +17,90 @@ module.exports = class Teams extends Diffable {
// remove all security manager teams
async checkSecurityManager (teams) {
try {
// Uncomment the following lines to handle the deprecation of the teams api https://gh.io/security-managers-rest-api-sunset
// but this would require a new permission on the app
//
// const roles = await this.github.paginate('GET /orgs/{org}/roles', { org: this.repo.owner })
// const securityManagerRole = roles.find(role => role.name === 'security_manager')
//
// this.log.debug(`Calling API to get security managers ${JSON.stringify(this.github.request.endpoint('GET /orgs/{org}/roles/{role_id}/teams',
// {
// org: this.repo.owner,
// role_id: securityManagerRole.id
// }))} `)
// const resp = await this.github.paginate('GET /orgs/{org}/roles/{role_id}/teams',
// {
// org: this.repo.owner,
// role_id: securityManagerRole.id
// })

this.log.debug('Removing all security manager teams since they should not be handled here')
this.log.debug(`Calling API to get security managers ${JSON.stringify(this.github.request.endpoint('GET /orgs/{org}/security-managers',
{
org: this.repo.owner
}))} `)
const resp = await this.github.paginate('GET /orgs/{org}/security-managers',
this.log.debug(`Calling API to get organization roles ${JSON.stringify(this.github.request.endpoint('GET /orgs/{org}/organization-roles',
{
org: this.repo.owner
}))} `)
const rolesResp = await this.github.paginate('GET /orgs/{org}/organization-roles',
{ org: this.repo.owner })
const roles = this.toArray(rolesResp, 'roles')
const securityManagerRole = roles.find(role => this.isSecurityManagerRole(role))

if (!securityManagerRole || !securityManagerRole.id) {
this.log.debug(`${this.repo.owner} Org does not have a security manager organization role set up`)
return teams
}

const params = {
org: this.repo.owner,
role_id: securityManagerRole.id
}
this.log.debug(`Calling API to get security manager teams ${JSON.stringify(this.github.request.endpoint('GET /orgs/{org}/organization-roles/{role_id}/teams', params))} `)
const resp = await this.github.paginate('GET /orgs/{org}/organization-roles/{role_id}/teams', params)

this.log.debug(`Response from the call is ${JSON.stringify(resp)}`)
return teams.filter(team => !resp.some(sec => sec.name === team.name))
const securityManagerTeams = this.toArray(resp, 'teams')
const securityManagerTeamIdentifiers = new Set(securityManagerTeams.flatMap(team => [team.slug, team.name].map(name => this.normalizeTeamIdentifier(name))).filter(Boolean))

return teams.filter(team => !this.isSecurityManagerTeam(team, securityManagerTeamIdentifiers))
Comment on lines 42 to +46
} catch (e) {
if (e.status === 404) {
this.log.debug(`${this.repo.owner} Org does not have Security manager teams set up ${e}`)
this.skipTeamDeletion = true
const status = e && e.status
if (safeSecurityManagerStatuses.includes(status)) {
this.log.debug(`${this.repo.owner} Org security manager teams could not be fetched with status ${status}; keeping repository teams unchanged ${e}`)
} else {
this.log.error(
`Unexpected error when fetching for security manager teams org ${this.repo.owner} = ${e}`
`Unexpected error when fetching security manager teams for org ${this.repo.owner}; keeping repository teams unchanged ${e}`
)
}
return teams
}
}

toArray (resp, propertyName) {
if (Array.isArray(resp)) {
return resp
}

if (resp && Array.isArray(resp[propertyName])) {
return resp[propertyName]
}

return []
}

isSecurityManagerRole (role) {
return [role && role.name, role && role.slug]
.map(name => this.normalizeRoleName(name))
.includes(securityManagerRoleName)
}

normalizeRoleName (name) {
if (typeof name !== 'string') {
return ''
}

return name.trim().toLowerCase().replace(/[\s-]+/g, '_')
}

normalizeTeamIdentifier (name) {
if (typeof name !== 'string') {
return ''
}

return name.trim().toLowerCase().replace(/['’]/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
}

isSecurityManagerTeam (team, securityManagerTeamIdentifiers) {
return [team.slug, team.name]
.map(name => this.normalizeTeamIdentifier(name))
.filter(Boolean)
.some(name => securityManagerTeamIdentifiers.has(name))
}

comparator (existing, attrs) {
return existing.slug === attrs.name.toLowerCase()
return this.normalizeTeamIdentifier(existing.slug || existing.name) === this.normalizeTeamIdentifier(attrs.name)
}

changed (existing, attrs) {
Expand All @@ -73,7 +119,7 @@ module.exports = class Teams extends Diffable {
add (attrs) {
let existing = { team_id: 1 }
this.log.debug(`Getting team with the parms ${JSON.stringify(attrs)}`)
return this.github.rest.teams.getByName({ org: this.repo.owner, team_slug: attrs.name }).then(res => {
return this.github.rest.teams.getByName({ org: this.repo.owner, team_slug: this.normalizeTeamIdentifier(attrs.name) }).then(res => {
existing = res.data
this.log.debug(`adding team ${attrs.name} to repo ${this.repo.repo}`)
if (this.nop) {
Expand Down Expand Up @@ -114,6 +160,11 @@ module.exports = class Teams extends Diffable {
}

remove (existing) {
if (this.skipTeamDeletion) {
this.log.debug(`Skipping deletion of team ${existing.slug} from repo ${this.repo.repo} because security manager team discovery failed`)
return Promise.resolve()
}

if (this.nop) {
return Promise.resolve([
new NopCommand(this.constructor.name, this.repo, this.github.request.endpoint(
Expand All @@ -132,7 +183,7 @@ module.exports = class Teams extends Diffable {
return {
team_id: existing.id,
org: this.repo.owner,
team_slug: attrs.name,
team_slug: existing.slug || this.normalizeTeamIdentifier(attrs.name),
owner: this.repo.owner,
repo: this.repo.repo,
permission: attrs.permission
Expand Down
15 changes: 14 additions & 1 deletion test/integration/plugins/teams.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ describe('teams plugin', function () {
const probotTeamId = any.integer()
const greenkeeperKeeperTeamId = any.integer()
const formationTeamId = any.integer()
const securityManagerRoleId = any.integer()
const securityManagerTeamId = any.integer()
githubScope
.get(`/repos/${repository.owner.name}/${repository.name}/contents/${settings.FILE_PATH}`)
.reply(OK, { content: encodedConfig, name: 'settings.yml', type: 'file' })
Expand All @@ -33,9 +35,20 @@ describe('teams plugin', function () {
OK,
[
{ slug: 'greenkeeper-keeper', id: greenkeeperKeeperTeamId, permission: 'pull' },
{ slug: 'form8ion', id: formationTeamId, permission: 'push' }
{ slug: 'form8ion', id: formationTeamId, permission: 'push' },
{ slug: 'security-managers', id: securityManagerTeamId, permission: 'push' }
]
)
githubScope
.get(`/orgs/${repository.owner.name}/organization-roles`)
.reply(OK, {
roles: [{ id: securityManagerRoleId, slug: 'security_manager', name: 'Security Manager' }]
})
githubScope
.get(`/orgs/${repository.owner.name}/organization-roles/${securityManagerRoleId}/teams`)
.reply(OK, {
teams: [{ id: securityManagerTeamId, slug: 'security-managers', name: 'Security Managers' }]
})
githubScope
.get(`/orgs/${repository.owner.name}/teams/probot`)
.reply(OK, { id: probotTeamId })
Expand Down
Loading
Loading