Skip to content

Commit ecb7775

Browse files
dthyressonjtoar
authored andcommitted
feat: Support Authentication in Realtime Subscriptions (#8815)
This PR implements validator directives on subscriptions like `@requireAuth` so one may enabled authentication on a Subscription just like one does for Queries and Mutation in RedwoodJS's GraphQL. It also: * enables any validator directive on Subscriptions (but not transformer ones) * checks that subscriptions have the `@skipAuth` or `@requireAuth` directive (as do queries and mutations) * updates the subscription templates to include `@requireAuth` For authorization (that it is I can Subscribe to newMessage -- authn -- but not listen to changes to room 227 -- authz -- one can do something like where you check for a user and the room and raise if no access rights: ```ts const newMessage = { newMessage: { subscribe: ( _, { roomId }, { pubSub, currentUser, }: { pubSub: NewMessageChannelType; currentUser: Record<string, any> } ) => { if (currentUser?.id === '1234' && roomId === '227') { throw new ForbiddenError('no can do') } logger.debug({ roomId }, 'newMessage subscription') return pubSub.subscribe('newMessage', roomId) }, ... ``` TODO: * docs * docs for directives and how Subscriptions cannot have transformer directives * see maybe if Subs can transformer directives -- otherwise see if need to warn better
1 parent 7c79bc5 commit ecb7775

10 files changed

Lines changed: 51 additions & 12 deletions

File tree

packages/cli/src/commands/experimental/templates/liveQueries/auctions/auctions.sdl.ts.template

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
export const schema = gql`
44
type Query {
5-
auction(id: ID!): Auction @skipAuth
5+
auction(id: ID!): Auction @requireAuth
66
}
77

88
type Auction {
@@ -17,7 +17,7 @@ export const schema = gql`
1717
}
1818

1919
type Mutation {
20-
bid(input: BidInput!): Bid @skipAuth
20+
bid(input: BidInput!): Bid @requireAuth
2121
}
2222

2323
input BidInput {

packages/cli/src/commands/experimental/templates/liveQueries/auctions/auctions.ts.template

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// api/src/services/auctions/auctions.ts
2+
import type { LiveQueryStorageMechanism } from '@redwoodjs/graphql-server'
23

34
import { logger } from 'src/lib/logger'
45

@@ -40,7 +41,10 @@ export const auction = async ({ id }) => {
4041
return foundAuction
4142
}
4243

43-
export const bid = async ({ input }, { context }) => {
44+
export const bid = async (
45+
{ input },
46+
{ context }: { context: { liveQueryStore: LiveQueryStorageMechanism } }
47+
) => {
4448
const { auctionId, amount } = input
4549

4650
const index = auctions.findIndex((a) => a.id === auctionId)

packages/cli/src/commands/experimental/templates/liveQueries/blank/blank.sdl.ts.template

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
export const schema = gql`
44
type Query {
5-
${liveQueryName}(id: ID!): ${typeName} @skipAuth
5+
${liveQueryName}(id: ID!): ${typeName} @requireAuth
66
}
77

88
type ${typeName} {
@@ -17,7 +17,7 @@ export const schema = gql`
1717
}
1818

1919
type Mutation {
20-
create${typeName}Item(input: ${typeName}ItemInput!): ${typeName}Item @skipAuth
20+
create${typeName}Item(input: ${typeName}ItemInput!): ${typeName}Item @requireAuth
2121
}
2222

2323
input ${typeName}ItemInput {

packages/cli/src/commands/experimental/templates/liveQueries/blank/blank.service.ts.template

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// api/src/services/${name}s/${name}s.ts
2+
import type { LiveQueryStorageMechanism } from '@redwoodjs/graphql-server'
23

34
import { logger } from 'src/lib/logger'
45

@@ -40,7 +41,10 @@ export const ${liveQueryName} = async ({ id }) => {
4041
return found${modelName}
4142
}
4243

43-
export const create${typeName}Item = async ({ input }, { context }) => {
44+
export const create${typeName}Item = async (
45+
{ input },
46+
{ context }: { context: { liveQueryStore: LiveQueryStorageMechanism } }
47+
) => {
4448
const { ${camelName}Id, amount } = input
4549

4650
const index = ${collectionName}.findIndex((a) => a.id === ${camelName}Id)

packages/cli/src/commands/experimental/templates/subscriptions/blank/blank.ts.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { logger } from 'src/lib/logger'
66

77
export const schema = gql`
88
type Subscription {
9-
${subscriptionName}(id: ID!): ${typeName}!
9+
${subscriptionName}(id: ID!): ${typeName}! @requireAuth
1010
}
1111
`
1212

packages/cli/src/commands/experimental/templates/subscriptions/countdown/countdown.ts.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import gql from 'graphql-tag'
22

33
export const schema = gql`
44
type Subscription {
5-
countdown(from: Int!, interval: Int!): Int!
5+
countdown(from: Int!, interval: Int!): Int! @requireAuth
66
}
77
`
88

packages/cli/src/commands/experimental/templates/subscriptions/newMessage/newMessage.ts.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { logger } from 'src/lib/logger'
66

77
export const schema = gql`
88
type Subscription {
9-
newMessage(roomId: ID!): Message!
9+
newMessage(roomId: ID!): Message! @requireAuth
1010
}
1111
`
1212
export type NewMessageChannel = {

packages/graphql-server/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ export {
3434
export {
3535
useRedwoodRealtime,
3636
createPubSub,
37-
InMemoryLiveQueryStore,
3837
liveDirectiveTypeDefs,
38+
InMemoryLiveQueryStore,
39+
LiveQueryStorageMechanism,
3940
RedisLiveQueryStore,
4041
RedwoodRealtimeOptions,
4142
PublishClientType,

packages/graphql-server/src/plugins/useRedwoodDirective.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,11 @@ function wrapAffectedResolvers(
154154
if (directiveNode && directive) {
155155
const directiveArgs =
156156
getDirectiveValues(directive, { directives: [directiveNode] }) || {}
157+
157158
const originalResolve = fieldConfig.resolve ?? defaultFieldResolver
159+
// Only validator directives handle a subscribe function
160+
const originalSubscribe = fieldConfig.subscribe ?? defaultFieldResolver
161+
158162
if (_isValidator(options)) {
159163
return {
160164
...fieldConfig,
@@ -180,6 +184,28 @@ function wrapAffectedResolvers(
180184
}
181185
return originalResolve(root, args, context, info)
182186
},
187+
subscribe: function useRedwoodDirectiveValidatorResolver(
188+
root,
189+
args,
190+
context,
191+
info
192+
) {
193+
const result = options.onResolvedValue({
194+
root,
195+
args,
196+
context,
197+
info,
198+
directiveNode,
199+
directiveArgs,
200+
})
201+
202+
if (isPromise(result)) {
203+
return result.then(() =>
204+
originalSubscribe(root, args, context, info)
205+
)
206+
}
207+
return originalSubscribe(root, args, context, info)
208+
},
183209
}
184210
}
185211
if (_isTransformer(options)) {

packages/internal/src/validateSchema.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const DIRECTIVE_INVALID_ROLE_TYPES_ERROR_MESSAGE =
1313
'Please check that the requireAuth roles is a string or an array of strings.'
1414
export function validateSchemaForDirectives(
1515
schemaDocumentNode: DocumentNode,
16-
typesToCheck: string[] = ['Query', 'Mutation']
16+
typesToCheck: string[] = ['Query', 'Mutation', 'Subscription']
1717
) {
1818
const validationOutput: string[] = []
1919
const directiveRoleValidationOutput: Record<string, any> = []
@@ -106,7 +106,11 @@ export function validateSchemaForDirectives(
106106

107107
export const loadAndValidateSdls = async () => {
108108
const projectTypeSrc = await loadTypedefs(
109-
['graphql/**/*.sdl.{js,ts}', 'directives/**/*.{js,ts}'],
109+
[
110+
'graphql/**/*.sdl.{js,ts}',
111+
'directives/**/*.{js,ts}',
112+
'subscriptions/**/*.{js,ts}',
113+
],
110114
{
111115
loaders: [
112116
new CodeFileLoader({

0 commit comments

Comments
 (0)