Skip to content

Commit

Permalink
fix: add tenant role validation (#61)
Browse files Browse the repository at this point in the history
* fix: add tenant role validation

* fix: linting issue by adding debug trace

* fix: consistent capitalization 1/2

Co-authored-by: Matthew Rogers <[email protected]>

* fix: consistent capitalization 2/2

Co-authored-by: Matthew Rogers <[email protected]>

* chore: update test following capitalization

---------

Co-authored-by: Matthew Rogers <[email protected]>
  • Loading branch information
aarlaud and soniqua authored Dec 18, 2024
1 parent c1ba6fe commit a43c7c7
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 9 deletions.
4 changes: 2 additions & 2 deletions config.default.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"API_HOSTNAME": "https://api.snyk.io",
"API_VERSION":"2024-02-08~experimental",
"API_VERSION_TENANTS": "2024-04-11~experimental",
"API_VERSION_TENANTS": "2024-10-14~experimental",
"APP_INSTALL_API_VERSION": "2024-05-31",
"MAX_RETRY": 3,
"LOG_LEVEL": "info"

}
}
23 changes: 23 additions & 0 deletions src/api/tenants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,26 @@ export const getAccessibleTenants = async () => {
throw new Error(error)
}
}

export const getTenantRole = async (tenantId: string) => {
const headers = {...commonHeaders, ...getAuthHeader()}
const apiPath = `rest/tenants/${tenantId}/memberships`
const config = getConfig()

const url = new URL(`${config.API_HOSTNAME}/${apiPath}`)
url.searchParams.append('role_name', 'admin')
url.searchParams.append('version', config.API_VERSION_TENANTS)

const req: HttpRequest = {
url: url.toString(),
headers: headers,
method: 'GET',
}
try {
const response = await makeRequest(req)
logger.debug({url: req.url, statusCode: response.statusCode, response: response.body}, 'Response')
return JSON.parse(response.body) as TenantsListingResponse
} catch (error: any) {
throw new Error(error)
}
}
17 changes: 14 additions & 3 deletions src/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {createDeployment, DeploymentAttributes, DeploymentResponse, getDeploymen
import {ConnectionId, ConnectionSelection, DeploymentId, InstallId, SetupParameters, TenantId} from './types.js'
import {getConnectionsForDeployment, createConnectionForDeployment} from './api/connections.js'
import {captureConnectionParams} from './command-helpers/connections/parameters-capture.js'
import {getAccessibleTenants} from './api/tenants.js'
import {getAccessibleTenants, getTenantRole} from './api/tenants.js'
import {validatedInput, ValidationType} from './utils/input-validation.js'
import {validateSnykToken} from './api/snyk.js'

Expand Down Expand Up @@ -52,18 +52,19 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {

try {
await validateSnykToken(process.env.SNYK_TOKEN)
this.log(`✓ Valid Snyk Token.`)
} catch (error) {
this.error(`Invalid Snyk Token. ${error}`)
}
if (!process.env.TENANT_ID) {
const accessibleTenants = await getAccessibleTenants()
if (accessibleTenants.data.length === 0) {
this.error(
'Not tenant accessible with your credentials. A Tenant is required for Universal Broker. Personal organizations are not compatible.',
'No Tenant accessible with your credentials. A Tenant is required for Universal Broker. Personal organizations are not compatible.',
)
} else if (accessibleTenants.data.length === 1) {
process.env.TENANT_ID = accessibleTenants.data[0].id
this.log(ux.colorize('yellow', `Found single accessible Tenant. Using ${process.env.TENANT_ID}.`))
this.log(ux.colorize('yellow', `Found single accessible Tenant. Using ${process.env.TENANT_ID}.`))
} else {
this.log(
ux.colorize(
Expand All @@ -77,6 +78,16 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
process.env.TENANT_ID ?? (await validatedInput({message: 'Enter your tenantID.'}, ValidationType.UUID))
process.env.TENANT_ID = tenantId

try {
await getTenantRole(tenantId)
this.log(`✓ Tenant Admin role confirmed.`)
} catch (error) {
this.debug(error)
this.error(
`This tool requires Tenant Admin role. Please use a Tenant level Admin account or upgrade your account to be Tenant Admin.`,
)
}

let orgId
let installId
if (process.env.INSTALL_ID) {
Expand Down
62 changes: 59 additions & 3 deletions test/api/tenants.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {getAccessibleTenants} from '../../src/api/tenants'
import {getAccessibleTenants, getTenantRole} from '../../src/api/tenants'
import {expect} from 'chai'
import nock from 'nock'

describe('Tenants Api calls', () => {
const tenantId = '00000000-0000-0000-0000-000000000000'
const tenants = {
data: [
{
Expand All @@ -12,7 +13,7 @@ describe('Tenants Api calls', () => {
slug: 'test-tenant',
updated_at: 'date',
},
id: '00000000-0000-0000-0000-000000000000',
id: tenantId,
relationships: {
owner: {
data: {
Expand All @@ -25,17 +26,72 @@ describe('Tenants Api calls', () => {
},
],
}
const tenantRoles = {
data: [
{
type: 'tenant_membership',
id: '00000000-0000-0000-0000-000000000000',
attributes: {
created_at: '2024-07-23T11:39:10.568336Z',
},
relationships: {
tenant: {
data: {
type: 'tenant',
id: tenantId,
attributes: {
name: 'Snyk Support',
},
},
},
user: {
data: {
type: 'user',
id: '00000000-0000-0000-0000-000000000000',
attributes: {
name: 'Name',
username: '[email protected]',
email: '[email protected]',
login_method: 'samlp',
account_type: 'user',
active: true,
},
},
},
role: {
data: {
type: 'tenant_role',
id: '00000000-0000-0000-0000-000000000000',
attributes: {
name: 'Tenant Admin',
},
},
},
},
},
],
jsonapi: {},
}
before(() => {
process.env.SNYK_TOKEN = 'dummy'
nock('https://api.snyk.io')
.persist()
.get('/rest/tenants?version=2024-04-11~experimental')
.get('/rest/tenants?version=2024-10-14~experimental')
.reply(() => {
return [200, tenants]
})
.get(`/rest/tenants/${tenantId}/memberships?role_name=admin&version=2024-10-14~experimental`)
.reply(() => {
return [200, tenantRoles]
})
})
it('getAccessibleTenants', async () => {
const accessibleTenants = await getAccessibleTenants()
expect(accessibleTenants).to.deep.equal(tenants)
})

it('getTenantRole', async () => {
const tenantRoles = await getTenantRole(tenantId)
expect(tenantRoles).to.deep.equal(tenantRoles)
})
})
75 changes: 74 additions & 1 deletion test/test-utils/nock-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const orgId2 = '3a7c1ab9-8914-4f39-a8c0-5752af653a89'
export const orgId3 = '3a7c1ab9-8914-4f39-a8c0-5752af653a8a'
export const orgId4 = '3a7c1ab9-8914-4f39-a8c0-5752af653a8b'
export const tenantId = '00000000-0000-0000-0000-000000000000'
export const nonAdminTenantId = '00000000-0000-0000-0000-000000000001'
export const installId = '00000000-0000-0000-0000-000000000000'
export const installId2 = '00000000-0000-0000-0000-000000000002'
export const installId3 = '00000000-0000-0000-0000-000000000003'
Expand Down Expand Up @@ -70,6 +71,70 @@ export const tenants = {
],
}

const tenantRoles = {
data: [
{
type: 'tenant_membership',
id: '00000000-0000-0000-0000-000000000000',
attributes: {
created_at: '2024-07-23T11:39:10.568336Z',
},
relationships: {
tenant: {
data: {
type: 'tenant',
id: tenantId,
attributes: {
name: 'Snyk Support',
},
},
},
user: {
data: {
type: 'user',
id: '00000000-0000-0000-0000-000000000000',
attributes: {
name: 'Name',
username: '[email protected]',
email: '[email protected]',
login_method: 'samlp',
account_type: 'user',
active: true,
},
},
},
role: {
data: {
type: 'tenant_role',
id: '00000000-0000-0000-0000-000000000000',
attributes: {
name: 'Tenant Admin',
},
},
},
},
},
],
jsonapi: {},
}

export const forbiddenTenantMembersResponse = {
jsonapi: {
version: '1.0',
},
errors: [
{
status: '403',
detail: 'Forbidden',
id: '00000000-0000-0000-0000-000000000000',
title: 'Forbidden',
meta: {
created: '2024-12-17T13:59:39.147423838Z',
},
},
],
}

export const appResponse = {
data: [
{
Expand Down Expand Up @@ -195,10 +260,18 @@ export const beforeStep = () => {
.reply(() => {
return [200, appResponse4]
})
.get('/rest/tenants?version=2024-04-11~experimental')
.get('/rest/tenants?version=2024-10-14~experimental')
.reply(() => {
return [200, tenants]
})
.get(`/rest/tenants/${tenantId}/memberships?role_name=admin&version=2024-10-14~experimental`)
.reply(() => {
return [200, tenantRoles]
})
.get(`/rest/tenants/${nonAdminTenantId}/memberships?role_name=admin&version=2024-10-14~experimental`)
.reply(() => {
return [403, forbiddenTenantMembersResponse]
})
.get(`${urlPrefixTenantIdAndInstallId}/deployments?version=2024-02-08~experimental`)
.reply((uri, body) => {
const response = apiResponseSchema
Expand Down
33 changes: 33 additions & 0 deletions test/workflows/deployments/get-failed-due-to-tenant-role.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {captureOutput} from '@oclif/test'
import {expect} from 'chai'
import {stdin as fstdin} from 'mock-stdin'

import Deployments from '../../../src/commands/workflows/deployments/get'
import {beforeStep, orgId, snykToken} from '../../test-utils/nock-utils'
import {sendScenario} from '../../test-utils/stdin-utils'

describe('deployment workflows', () => {
before(beforeStep)

after(() => {
delete process.env.TENANT_ID
})
it('runs workflow deployment list', async () => {
process.env.TENANT_ID = '00000000-0000-0000-0000-000000000001'
const stdin = fstdin()
// @ts-ignore
const cfg: Config = {}
const getDeployment = new Deployments([], cfg)
const {stdout, stderr, error} = await captureOutput(
async () => {
sendScenario(stdin, [snykToken, 'n', orgId])

return getDeployment.run()
},
{print: false},
)
expect(error?.message).to.contain(
'This tool requires Tenant Admin role. Please use a Tenant level Admin account or upgrade your account to be Tenant Admin.',
)
})
})
1 change: 1 addition & 0 deletions tsconfig.tsbuildinfo
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"root":["./src/base-command.ts","./src/index.ts","./src/types.ts","./src/api/apps.ts","./src/api/connections.ts","./src/api/credentials.ts","./src/api/deployments.ts","./src/api/integrations-types.ts","./src/api/integrations-utils.ts","./src/api/integrations.ts","./src/api/snyk.ts","./src/api/tenants.ts","./src/api/types.ts","./src/command-helpers/connections/connections-flags.ts","./src/command-helpers/connections/flags.ts","./src/command-helpers/connections/parameters-capture.ts","./src/command-helpers/connections/type-params-mapping.ts","./src/command-helpers/connections/types.ts","./src/command-helpers/credentials/flags.ts","./src/command-helpers/deployments/flags.ts","./src/commands/connections/create.ts","./src/commands/connections/delete.ts","./src/commands/connections/list.ts","./src/commands/connections/update.ts","./src/commands/credentials/create.ts","./src/commands/credentials/delete.ts","./src/commands/credentials/list.ts","./src/commands/credentials/update.ts","./src/commands/deployments/create.ts","./src/commands/deployments/delete.ts","./src/commands/deployments/list.ts","./src/commands/deployments/update.ts","./src/commands/integrations/create.ts","./src/commands/integrations/delete.ts","./src/commands/integrations/list.ts","./src/commands/introduction/index.ts","./src/commands/workflows/connections/create.ts","./src/commands/workflows/connections/delete.ts","./src/commands/workflows/connections/disconnect.ts","./src/commands/workflows/connections/get.ts","./src/commands/workflows/connections/integrate.ts","./src/commands/workflows/connections/migrate.ts","./src/commands/workflows/credentials/create.ts","./src/commands/workflows/credentials/delete.ts","./src/commands/workflows/credentials/get.ts","./src/commands/workflows/deployments/create.ts","./src/commands/workflows/deployments/delete.ts","./src/commands/workflows/deployments/get.ts","./src/commands/workflows/deployments/update.ts","./src/common/args.ts","./src/common/rest-helpers.ts","./src/config/config.ts","./src/utils/auth.ts","./src/utils/display.ts","./src/utils/http-request.ts","./src/utils/input-validation.ts","./src/utils/logger.ts","./src/utils/utils.ts","./src/utils/validation.ts","./src/workflows/apps.ts"],"version":"5.7.2"}

0 comments on commit a43c7c7

Please sign in to comment.