Skip to content

Commit

Permalink
fix: Avoid caching permissions with different arguments (#8670)
Browse files Browse the repository at this point in the history
* fix: Avoid caching permissions with different arguments

Permissions are cached by function name. When these depend on the
arguments passed, they should have their own cache key, see
https://the-guild.dev/graphql/shield/docs/rules#limitations

* Make isViewerOnTeam strict

The passed function might access source or args and thus we should use
the strict cache.
  • Loading branch information
Dschoordsch authored Nov 17, 2023
1 parent 0fdef2d commit a6dcd7f
Show file tree
Hide file tree
Showing 3 changed files with 24 additions and 20 deletions.
2 changes: 1 addition & 1 deletion packages/server/graphql/public/rules/isEnvVarTrue.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {rule} from 'graphql-shield'

const isEnvVarTrue = (varName: string) =>
rule({cache: 'contextual'})(() => {
rule(`isEnvVarTrue-${varName}`, {cache: 'contextual'})(() => {
const isEnabled = process.env[varName] === 'true'
return isEnabled || new Error(`${varName} is not enabled`)
})
Expand Down
22 changes: 12 additions & 10 deletions packages/server/graphql/public/rules/isViewerOnTeam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ import {GQLContext} from '../../graphql'
type GetTeamId = (source: any, args: any, context: GQLContext) => Promise<string | Error>

const isViewerOnTeam = (getTeamId: GetTeamId) =>
rule({cache: 'contextual'})(async (source, args, context: GQLContext) => {
const {authToken, dataLoader} = context
const viewerId = getUserId(authToken)
const teamId = await getTeamId(source, args, context)
if (teamId instanceof Error) return teamId
const teamMemberId = TeamMemberId.join(teamId, viewerId)
const teamMember = await dataLoader.get('teamMembers').load(teamMemberId)
if (!teamMember) return new Error('Viewer in not on team')
return true
})
rule(`isViewerOnTeam-${getTeamId.name || getTeamId}`, {cache: 'strict'})(
async (source, args, context: GQLContext) => {
const {authToken, dataLoader} = context
const viewerId = getUserId(authToken)
const teamId = await getTeamId(source, args, context)
if (teamId instanceof Error) return teamId
const teamMemberId = TeamMemberId.join(teamId, viewerId)
const teamMember = await dataLoader.get('teamMembers').load(teamMemberId)
if (!teamMember) return new Error('Viewer is not on team')
return true
}
)

export default isViewerOnTeam
20 changes: 11 additions & 9 deletions packages/server/graphql/public/rules/rateLimit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ interface RateLimitOptions {
}

const rateLimit = ({perMinute, perHour}: RateLimitOptions) =>
rule({cache: 'contextual'})((_source, _args, context: GQLContext, info) => {
const {authToken, rateLimiter, ip} = context
const {fieldName} = info
const userId = getUserId(authToken) || ip
const {lastMinute, lastHour} = rateLimiter.log(userId, fieldName, !!perHour)
if (lastMinute > perMinute || (lastHour && lastHour > perHour)) {
return new Error('429 Too Many Requests')
rule(`rateLimit-${perMinute}-${perHour}`, {cache: 'contextual'})(
(_source, _args, context: GQLContext, info) => {
const {authToken, rateLimiter, ip} = context
const {fieldName} = info
const userId = getUserId(authToken) || ip
const {lastMinute, lastHour} = rateLimiter.log(userId, fieldName, !!perHour)
if (lastMinute > perMinute || (lastHour && lastHour > perHour)) {
return new Error('429 Too Many Requests')
}
return true
}
return true
})
)

export default rateLimit

0 comments on commit a6dcd7f

Please sign in to comment.