Skip to content

Commit

Permalink
Fix createInitial* and send*MagicAuthLink to throw if the expecte…
Browse files Browse the repository at this point in the history
…d type from `sessionStrategy.start` is not a string (#9018)
  • Loading branch information
dcousens authored Feb 12, 2024
1 parent 3cb0cce commit 17ba589
Show file tree
Hide file tree
Showing 19 changed files with 179 additions and 154 deletions.
5 changes: 5 additions & 0 deletions .changeset/error-if-wrong-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
----
'@keystone-6/auth': patch
----

Fix `createInitial*` and `send*MagicAuthLink` to throw if the expected type from `sessionStrategy.start` is not a string
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
publish:
name: Publish
runs-on: ubuntu-latest
environment: release
environment: Release
steps:
- uses: actions/checkout@main
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish_snapshot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
publish_snapshot:
name: Publish (Snapshot)
runs-on: ubuntu-latest
environment: release
environment: Release
steps:
- uses: actions/checkout@main
with:
Expand Down
20 changes: 11 additions & 9 deletions packages/auth/src/gql/getBaseAuthSchema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { BaseItem } from '@keystone-6/core/types'
import { type BaseItem, type KeystoneContext } from '@keystone-6/core/types'
import { graphql } from '@keystone-6/core'
import { type AuthGqlNames, type SecretFieldImpl } from '../types'

Expand Down Expand Up @@ -60,9 +60,9 @@ export function getBaseAuthSchema<I extends string, S extends string> ({
type: graphql.union({
name: 'AuthenticatedItem',
types: [base.object(listKey) as graphql.ObjectType<BaseItem>],
resolveType: (root, context) => context.session?.listKey,
resolveType: (root, context: KeystoneContext) => context.session?.listKey,
}),
resolve (root, args, context) {
resolve (root, args, context: KeystoneContext) {
const { session } = context
if (!session) return null
if (!session.itemId) return null
Expand All @@ -83,10 +83,8 @@ export function getBaseAuthSchema<I extends string, S extends string> ({
[identityField]: graphql.arg({ type: graphql.nonNull(graphql.String) }),
[secretField]: graphql.arg({ type: graphql.nonNull(graphql.String) }),
},
async resolve (root, { [identityField]: identity, [secretField]: secret }, context) {
if (!context.sessionStrategy) {
throw new Error('No session implementation available on context')
}
async resolve (root, { [identityField]: identity, [secretField]: secret }, context: KeystoneContext) {
if (!context.sessionStrategy) throw new Error('No session implementation available on context')

const dbItemAPI = context.sudo().db[listKey]
const result = await validateSecret(
Expand All @@ -111,15 +109,19 @@ export function getBaseAuthSchema<I extends string, S extends string> ({
context,
})

// return Failure if sessionStrategy.start() returns null
// return Failure if sessionStrategy.start() is incompatible
if (typeof sessionToken !== 'string' || sessionToken.length === 0) {
return { code: 'FAILURE', message: 'Failed to start session.' }
}

return { sessionToken, item: result.item }
return {
sessionToken,
item: result.item
}
},
}),
},
}

return { extension, ItemAuthenticationWithPasswordSuccess }
}
12 changes: 9 additions & 3 deletions packages/auth/src/gql/getInitFirstItemSchema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type BaseItem, type KeystoneContext } from '@keystone-6/core/types'
import { graphql } from '@keystone-6/core'
import type { BaseItem } from '@keystone-6/core/types'
import { assertInputObjectType, GraphQLInputObjectType, type GraphQLSchema } from 'graphql'

import type { AuthGqlNames, InitFirstItemConfig } from '../types'
Expand Down Expand Up @@ -42,7 +42,7 @@ export function getInitFirstItemSchema ({
[gqlNames.createInitialItem]: graphql.field({
type: graphql.nonNull(ItemAuthenticationWithPasswordSuccess),
args: { data: graphql.arg({ type: graphql.nonNull(initialCreateInput) }) },
async resolve (rootVal, { data }, context) {
async resolve (rootVal, { data }, context: KeystoneContext) {
if (!context.sessionStrategy) {
throw new Error('No session implementation available on context')
}
Expand All @@ -63,7 +63,13 @@ export function getInitFirstItemSchema ({
const sessionToken = (await context.sessionStrategy.start({
data: { listKey, itemId: item.id.toString() },
context,
})) as string
}))

// return Failure if sessionStrategy.start() is incompatible
if (typeof sessionToken !== 'string' || sessionToken.length === 0) {
throw new Error('Failed to start session')
}

return { item, sessionToken }
},
}),
Expand Down
30 changes: 21 additions & 9 deletions packages/auth/src/gql/getMagicAuthLinkSchema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { BaseItem } from '@keystone-6/core/types'
import { type BaseItem, type KeystoneContext } from '@keystone-6/core/types'
import { graphql } from '@keystone-6/core'
import type { AuthGqlNames, AuthTokenTypeConfig, SecretFieldImpl } from '../types'

Expand Down Expand Up @@ -60,7 +60,7 @@ export function getMagicAuthLinkSchema<I extends string> ({
[gqlNames.sendItemMagicAuthLink]: graphql.field({
type: graphql.nonNull(graphql.Boolean),
args: { [identityField]: graphql.arg({ type: graphql.nonNull(graphql.String) }) },
async resolve (rootVal, { [identityField]: identity }, context) {
async resolve (rootVal, { [identityField]: identity }, context: KeystoneContext) {
const dbItemAPI = context.sudo().db[listKey]
const tokenType = 'magicAuth'

Expand Down Expand Up @@ -90,10 +90,9 @@ export function getMagicAuthLinkSchema<I extends string> ({
[identityField]: graphql.arg({ type: graphql.nonNull(graphql.String) }),
token: graphql.arg({ type: graphql.nonNull(graphql.String) }),
},
async resolve (rootVal, { [identityField]: identity, token }, context) {
if (!context.sessionStrategy) {
throw new Error('No session implementation available on context')
}

async resolve (rootVal, { [identityField]: identity, token }, context: KeystoneContext) {
if (!context.sessionStrategy) throw new Error('No session implementation available on context')

const dbItemAPI = context.sudo().db[listKey]
const tokenType = 'magicAuth'
Expand All @@ -109,8 +108,12 @@ export function getMagicAuthLinkSchema<I extends string> ({
)

if (!result.success) {
return { code: result.code, message: getAuthTokenErrorMessage({ code: result.code }) }
return {
code: result.code,
message: getAuthTokenErrorMessage({ code: result.code })
}
}

// Update system state
// Save the token and related info back to the item
await dbItemAPI.updateOne({
Expand All @@ -124,8 +127,17 @@ export function getMagicAuthLinkSchema<I extends string> ({
itemId: result.item.id.toString(),
},
context,
})) as string
return { token: sessionToken, item: result.item }
}))

// return Failure if sessionStrategy.start() is incompatible
if (typeof sessionToken !== 'string' || sessionToken.length === 0) {
return { code: 'FAILURE', message: 'Failed to start session.' }
}

return {
token: sessionToken,
item: result.item
}
},
}),
},
Expand Down
21 changes: 12 additions & 9 deletions packages/auth/src/gql/getPasswordResetSchema.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { type KeystoneContext } from '@keystone-6/core/types'
import { graphql } from '@keystone-6/core'
import type { AuthGqlNames, AuthTokenTypeConfig, SecretFieldImpl } from '../types'

Expand Down Expand Up @@ -36,16 +37,15 @@ export function getPasswordResetSchema<I extends string, S extends string> ({
message: graphql.field({ type: graphql.nonNull(graphql.String) }),
},
})
const ValidateItemPasswordResetTokenResult = getResult(
gqlNames.ValidateItemPasswordResetTokenResult
)

const ValidateItemPasswordResetTokenResult = getResult(gqlNames.ValidateItemPasswordResetTokenResult)
const RedeemItemPasswordResetTokenResult = getResult(gqlNames.RedeemItemPasswordResetTokenResult)
return {
mutation: {
[gqlNames.sendItemPasswordResetLink]: graphql.field({
type: graphql.nonNull(graphql.Boolean),
args: { [identityField]: graphql.arg({ type: graphql.nonNull(graphql.String) }) },
async resolve (rootVal, { [identityField]: identity }, context) {
async resolve (rootVal, { [identityField]: identity }, context: KeystoneContext) {
const dbItemAPI = context.sudo().db[listKey]
const tokenType = 'passwordReset'

Expand Down Expand Up @@ -79,7 +79,7 @@ export function getPasswordResetSchema<I extends string, S extends string> ({
async resolve (
rootVal,
{ [identityField]: identity, token, [secretField]: secret },
context
context: KeystoneContext
) {
const dbItemAPI = context.sudo().db[listKey]
const tokenType = 'passwordReset'
Expand Down Expand Up @@ -125,13 +125,12 @@ export function getPasswordResetSchema<I extends string, S extends string> ({
[identityField]: graphql.arg({ type: graphql.nonNull(graphql.String) }),
token: graphql.arg({ type: graphql.nonNull(graphql.String) }),
},
async resolve (rootVal, { [identityField]: identity, token }, context) {
async resolve (rootVal, { [identityField]: identity, token }, context: KeystoneContext) {
const dbItemAPI = context.sudo().db[listKey]
const tokenType = 'passwordReset'
const result = await validateAuthToken(
listKey,
passwordResetTokenSecretFieldImpl,
tokenType,
'passwordReset',
identityField,
identity,
passwordResetLink.tokensValidForMins,
Expand All @@ -140,8 +139,12 @@ export function getPasswordResetSchema<I extends string, S extends string> ({
)

if (!result.success) {
return { code: result.code, message: getAuthTokenErrorMessage({ code: result.code }) }
return {
code: result.code,
message: getAuthTokenErrorMessage({ code: result.code })
}
}

return null
},
}),
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { printGeneratedTypes } from './lib/schema-type-printer'
import { ExitError } from './scripts/utils'
import { initialiseLists } from './lib/core/initialise-lists'
import { printPrismaSchema } from './lib/core/prisma-schema-printer'
import { initConfig } from './lib/config'
import { initConfig } from './system'

export function getFormattedGraphQLSchema (schema: string) {
return (
Expand Down Expand Up @@ -93,19 +93,19 @@ export function getSystemPaths (cwd: string, config: KeystoneConfig) {
: null

const builtTypesPath = config.types?.path
? path.join(cwd, config.types.path)
? path.join(cwd, config.types.path) // TODO: enforce initConfig before getSystemPaths
: path.join(cwd, 'node_modules/.keystone/types.ts')

const builtPrismaPath = config.db?.prismaSchemaPath
? path.join(cwd, config.db.prismaSchemaPath)
? path.join(cwd, config.db.prismaSchemaPath) // TODO: enforce initConfig before getSystemPaths
: path.join(cwd, 'schema.prisma')

const relativePrismaPath = prismaClientPath
? `./${posixify(path.relative(path.dirname(builtTypesPath), prismaClientPath))}`
: '@prisma/client'

const builtGraphqlPath = config.graphql?.schemaPath
? path.join(cwd, config.graphql.schemaPath)
? path.join(cwd, config.graphql.schemaPath) // TODO: enforce initConfig before getSystemPaths
: path.join(cwd, 'schema.graphql')

return {
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { BaseKeystoneTypeInfo, KeystoneConfig, KeystoneContext } from './types'
import { initConfig } from './lib/config'
import {
type BaseKeystoneTypeInfo,
type KeystoneConfig,
type KeystoneContext
} from './types'
import { initConfig } from './system'
import { createSystem } from './lib/createSystem'

export function getContext<TypeInfo extends BaseKeystoneTypeInfo> (
Expand Down
19 changes: 1 addition & 18 deletions packages/core/src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { KeystoneConfig } from '../types'
import { idFieldType } from './id-field'

function applyIdFieldDefaults (config: KeystoneConfig): KeystoneConfig['lists'] {
export function applyIdFieldDefaults (config: KeystoneConfig): KeystoneConfig['lists'] {
// some error checking
for (const [listKey, list] of Object.entries(config.lists)) {
if (list.fields.id) {
Expand Down Expand Up @@ -53,20 +53,3 @@ function applyIdFieldDefaults (config: KeystoneConfig): KeystoneConfig['lists']

return listsWithIds
}

export function initConfig (config: KeystoneConfig) {
if (!['postgresql', 'sqlite', 'mysql'].includes(config.db.provider)) {
throw new TypeError(
'Invalid db configuration. Please specify db.provider as either "sqlite", "postgresql" or "mysql"'
)
}

// WARNING: Typescript should prevent this, but empty string is useful for Prisma errors
config.db.url ??= 'postgres://'

// TODO: use zod or something if want to follow this path
return {
...config,
lists: applyIdFieldDefaults(config),
}
}
13 changes: 7 additions & 6 deletions packages/core/src/lib/core/mutations/create-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,15 @@ async function updateSingle (
accessFilters: boolean | InputFilter
) {
const { where: uniqueInput, data: rawData } = updateInput
// Validate and resolve the input filter

// validate and resolve the input filter
const uniqueWhere = await resolveUniqueWhereInput(uniqueInput, list, context)

// Check filter access
// check filter access
const fieldKey = Object.keys(uniqueWhere)[0]
await checkFilterOrderAccess([{ fieldKey, list }], context, 'filter')

// Filter and Item access control. Will throw an accessDeniedError if not allowed.
// filter and item access control - throws an AccessDeniedError if not allowed
const item = await getAccessControlledItemForUpdate(
list,
context,
Expand All @@ -148,8 +149,8 @@ async function updateSingle (
runWithPrisma(context, list, model => model.update({ where: { id: item.id }, data }))
)

// after operation
await afterOperation(updatedItem)

return updatedItem
}

Expand All @@ -161,7 +162,7 @@ export async function updateOne (
const operationAccess = await getOperationAccess(list, context, 'update')
if (!operationAccess) throw accessDeniedError(cannotForItem('update', list))

// Get list-level access control filters
// get list-level access control filters
const accessFilters = await getAccessFilters(list, context, 'update')

return updateSingle(updateInput, list, context, accessFilters)
Expand All @@ -174,7 +175,7 @@ export async function updateMany (
) {
const operationAccess = await getOperationAccess(list, context, 'update')

// Get list-level access control filters
// get list-level access control filters
const accessFilters = await getAccessFilters(list, context, 'update')

return data.map(async updateInput => {
Expand Down
11 changes: 6 additions & 5 deletions packages/core/src/lib/core/mutations/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ async function deleteSingle (
context: KeystoneContext,
accessFilters: boolean | InputFilter
) {
// Validate and resolve the input filter
// validate and resolve the input filter
const uniqueWhere = await resolveUniqueWhereInput(uniqueInput, list, context)

// Check filter access
// check filter access
const fieldKey = Object.keys(uniqueWhere)[0]
await checkFilterOrderAccess([{ fieldKey, list }], context, 'filter')

// Filter and Item access control. Will throw an accessDeniedError if not allowed.
// filter and item access control throw an AccessDeniedError if not allowed
const item = await getAccessControlledItemForDelete(list, context, uniqueWhere, accessFilters)

const hookArgs = {
Expand All @@ -34,10 +34,10 @@ async function deleteSingle (
inputData: undefined,
}

// Apply all validation checks
// hooks
await validateDelete({ list, hookArgs })

// Before operation
// before operation
await runSideEffectOnlyHook(list, 'beforeOperation', hookArgs)

const writeLimit = getWriteLimit(context)
Expand All @@ -46,6 +46,7 @@ async function deleteSingle (
runWithPrisma(context, list, model => model.delete({ where: { id: item.id } }))
)

// after operation
await runSideEffectOnlyHook(list, 'afterOperation', {
...hookArgs,
item: undefined,
Expand Down
Loading

0 comments on commit 17ba589

Please sign in to comment.