Skip to content

Commit

Permalink
fix: preventing PATs from creating PATs
Browse files Browse the repository at this point in the history
  • Loading branch information
fabis94 committed Dec 11, 2023
1 parent a329f91 commit 3689e1c
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ const redirectUrl = ref('')
const description = ref('')
const applicationScopes = computed(() => {
return Object.values(AllScopes).map((value) => ({
id: value,
text: value
return Object.values(allScopes.value).map((value) => ({
id: value.name,
text: value.name
}))
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@

<script setup lang="ts">
import { useMutation } from '@vue/apollo-composable'
import { AllScopes } from '@speckle/shared'
import { LayoutDialog, FormSelectBadges } from '@speckle/ui-components'
import type { TokenFormValues } from '~~/lib/developer-settings/helpers/types'
import { createAccessTokenMutation } from '~~/lib/developer-settings/graphql/mutations'
Expand All @@ -50,11 +49,13 @@ import {
getFirstErrorMessage
} from '~~/lib/common/helpers/graphql'
import { useGlobalToast, ToastNotificationType } from '~~/lib/common/composables/toast'
import { useServerInfoScopes } from '~/lib/common/composables/serverInfo'
const emit = defineEmits<{
(e: 'token-created', tokenId: string): void
}>()
const { scopes: allScopes } = useServerInfoScopes()
const { mutate: createToken } = useMutation(createAccessTokenMutation)
const { triggerNotification } = useGlobalToast()
const { handleSubmit } = useForm<TokenFormValues>()
Expand All @@ -65,9 +66,9 @@ const name = ref('')
const scopes = ref<typeof apiTokenScopes.value>([])
const apiTokenScopes = computed(() => {
return Object.values(AllScopes).map((value) => ({
id: value,
text: value
return Object.values(allScopes.value).map((value) => ({
id: value.name,
text: value.name
}))
})
Expand Down
5 changes: 5 additions & 0 deletions packages/server/modules/core/errors/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ export class UserValidationError extends BaseError {
static defaultMessage = 'The user input data is invalid'
static code = 'USER_VALIDATION_ERROR'
}

export class TokenCreateError extends BaseError {
static code = 'TOKEN_CREATE_ERROR'
static defaultMessage = 'An error occurred while creating a token'
}
12 changes: 6 additions & 6 deletions packages/server/modules/core/graph/resolvers/apitoken.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const {
revokeToken,
getUserTokens
} = require('../../services/tokens')
const { canCreateToken } = require('@/modules/core/helpers/token')
const { canCreatePAT } = require('@/modules/core/helpers/token')

/** @type {import('@/modules/core/graph/generated/graphql').Resolvers} */
const resolvers = {
Expand All @@ -23,11 +23,11 @@ const resolvers = {
},
Mutation: {
async apiTokenCreate(parent, args, context) {
if (!canCreateToken(context.scopes || [], args.token.scopes)) {
throw new ForbiddenError(
"You can't create a token with scopes that you don't have"
)
}
canCreatePAT({
userScopes: context.scopes || [],
tokenScopes: args.token.scopes,
strict: true
})

return await createPersonalAccessToken(
context.userId,
Expand Down
36 changes: 34 additions & 2 deletions packages/server/modules/core/helpers/token.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,35 @@
export const canCreateToken = (userScopes: string[], tokenScopes: string[]) => {
return tokenScopes.every((scope) => userScopes.includes(scope))
import { TokenCreateError } from '@/modules/core/errors/user'
import { Scopes } from '@speckle/shared'

export const canCreateToken = (params: {
userScopes: string[]
tokenScopes: string[]
strict?: boolean
}) => {
const { userScopes, tokenScopes, strict } = params
const hasAllScopes = tokenScopes.every((scope) => userScopes.includes(scope))
if (!hasAllScopes) {
if (!strict) return false
throw new TokenCreateError(
"You can't create a token with scopes that you don't have"
)
}

return true
}

export const canCreatePAT = (params: {
userScopes: string[]
tokenScopes: string[]
strict?: boolean
}) => {
const { tokenScopes, strict } = params
if (tokenScopes.includes(Scopes.Tokens.Write)) {
if (!strict) return false
throw new TokenCreateError(
"You can't create a personal access token with the tokens:write scope"
)
}

return canCreateToken(params)
}
28 changes: 28 additions & 0 deletions packages/server/modules/core/tests/apitokens.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,34 @@ describe('API Tokens', () => {
).to.be.ok
})

it("can't create PAT with tokens:write scope", async () => {
const scopes = [Scopes.Profile.Read, Scopes.Tokens.Write]
const { data, errors } = await apollo.execute(
CreateTokenDocument,
{
token: {
name: 'sometoken',
scopes
}
},
{
context: {
scopes
}
}
)

expect(data?.apiTokenCreate).to.not.be.ok
expect(errors).to.be.ok
expect(
errors!.find((e) =>
e.message.includes(
"You can't create a personal access token with the tokens:write scope"
)
)
).to.be.ok
})

describe('without the tokens:write scope', () => {
const limitedTokenScopes = difference(AllScopes, [Scopes.Tokens.Write])
let limitedToken: string
Expand Down

0 comments on commit 3689e1c

Please sign in to comment.