Skip to content

Commit

Permalink
fix(babel): Fix opentelemetry api wrapping and allow it to be disabled (
Browse files Browse the repository at this point in the history
#9298)

**Problem**
Some projects fail to build when opentelemetry is enabled. This is
because the babel plugin which automatically wraps the api side
functions failed to handle some syntax cases. In those cases the
transpiled syntax would be invalid javascript.

**Changes**
1. Supports default values in function arguments.
2. Supports some `AssignmentPattern` function arguments.
3. Add test fixtures. We can now see if the transpiled code is valid and
as we expect.
4. Allow the api wrapping to be disabled via a `wrapApi` toml option
5. Expose the `wrapApi` toml option during opentelemetry setup.

**Notes**
There are still some syntax cases which aren't supported in the babel
plugin. In this case we now bail out and do not modify the users source
code rather than transpile into nonsense syntax.

---------

Co-authored-by: Dominic Saadi <[email protected]>
  • Loading branch information
Josh-Walker-GM and jtoar committed Nov 3, 2023
1 parent f32e0b3 commit 10e6ccf
Show file tree
Hide file tree
Showing 22 changed files with 1,301 additions and 285 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import gql from 'graphql-tag'

import { createValidatorDirective } from '@redwoodjs/graphql-server'

export const schema = gql`
"""
Use to skip authentication checks and allow public access.
"""
directive @skipAuth on FIELD_DEFINITION
`

const skipAuth = createValidatorDirective(schema, () => {
return
})

export default skipAuth
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { trace as RW_OTEL_WRAPPER_TRACE } from '@opentelemetry/api'
import gql from 'graphql-tag'
import { createValidatorDirective } from '@redwoodjs/graphql-server'
export const schema = gql`
"""
Use to skip authentication checks and allow public access.
"""
directive @skipAuth on FIELD_DEFINITION
`
const skipAuth = createValidatorDirective(schema, () => {
return
})
export default skipAuth
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { DbAuthHandler, DbAuthHandlerOptions } from '@redwoodjs/auth-dbauth-api'

import { db } from 'src/lib/db'

export const handler = async (
event,
context
) => {
const forgotPasswordOptions = {
handler: (user) => {
return user
},

expires: 60 * 60 * 24,

errors: {
usernameNotFound: 'Username not found',
usernameRequired: 'Username is required',
},
}

const loginOptions = {
handler: (user) => {
return user
},

errors: {
usernameOrPasswordMissing: 'Both username and password are required',
usernameNotFound: 'Username ${username} not found',
incorrectPassword: 'Incorrect password for ${username}',
},

expires: 60 * 60 * 24 * 365 * 10,
}

const resetPasswordOptions = {
handler: (_user) => {
return true
},

allowReusedPassword: true,

errors: {
resetTokenExpired: 'resetToken is expired',
resetTokenInvalid: 'resetToken is invalid',
resetTokenRequired: 'resetToken is required',
reusedPassword: 'Must choose a new password',
},
}

const signupOptions = {
handler: ({ username, hashedPassword, salt, userAttributes }) => {
return db.user.create({
data: {
email: username,
hashedPassword: hashedPassword,
salt: salt,
fullName: userAttributes['full-name'],
},
})
},

passwordValidation: (_password) => {
return true
},

errors: {
fieldMissing: '${field} is required',
usernameTaken: 'Username `${username}` already in use',
},
}

const authHandler = new DbAuthHandler(event, context, {
db: db,

authModelAccessor: 'user',

authFields: {
id: 'id',
username: 'email',
hashedPassword: 'hashedPassword',
salt: 'salt',
resetToken: 'resetToken',
resetTokenExpiresAt: 'resetTokenExpiresAt',
},

cookie: {
HttpOnly: true,
Path: '/',
SameSite: 'Strict',
Secure: process.env.NODE_ENV !== 'development',

},

forgotPassword: forgotPasswordOptions,
login: loginOptions,
resetPassword: resetPasswordOptions,
signup: signupOptions,
})

return await authHandler.invoke()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { trace as RW_OTEL_WRAPPER_TRACE } from '@opentelemetry/api'
import { DbAuthHandler, DbAuthHandlerOptions } from '@redwoodjs/auth-dbauth-api'
import { db } from 'src/lib/db'
export const handler = async (event, context) => {
const __handler = async (event, context) => {
const forgotPasswordOptions = {
handler: (user) => {
return user
},
expires: 60 * 60 * 24,
errors: {
usernameNotFound: 'Username not found',
usernameRequired: 'Username is required',
},
}
const loginOptions = {
handler: (user) => {
return user
},
errors: {
usernameOrPasswordMissing: 'Both username and password are required',
usernameNotFound: 'Username ${username} not found',
incorrectPassword: 'Incorrect password for ${username}',
},
expires: 60 * 60 * 24 * 365 * 10,
}
const resetPasswordOptions = {
handler: (_user) => {
return true
},
allowReusedPassword: true,
errors: {
resetTokenExpired: 'resetToken is expired',
resetTokenInvalid: 'resetToken is invalid',
resetTokenRequired: 'resetToken is required',
reusedPassword: 'Must choose a new password',
},
}
const signupOptions = {
handler: ({ username, hashedPassword, salt, userAttributes }) => {
return db.user.create({
data: {
email: username,
hashedPassword: hashedPassword,
salt: salt,
fullName: userAttributes['full-name'],
},
})
},
passwordValidation: (_password) => {
return true
},
errors: {
fieldMissing: '${field} is required',
usernameTaken: 'Username `${username}` already in use',
},
}
const authHandler = new DbAuthHandler(event, context, {
db: db,
authModelAccessor: 'user',
authFields: {
id: 'id',
username: 'email',
hashedPassword: 'hashedPassword',
salt: 'salt',
resetToken: 'resetToken',
resetTokenExpiresAt: 'resetTokenExpiresAt',
},
cookie: {
HttpOnly: true,
Path: '/',
SameSite: 'Strict',
Secure: process.env.NODE_ENV !== 'development',
},
forgotPassword: forgotPasswordOptions,
login: loginOptions,
resetPassword: resetPasswordOptions,
signup: signupOptions,
})
return await authHandler.invoke()
}
const RW_OTEL_WRAPPER_TRACER = RW_OTEL_WRAPPER_TRACE.getTracer('redwoodjs')
const RW_OTEL_WRAPPER_RESULT = await RW_OTEL_WRAPPER_TRACER.startActiveSpan(
'redwoodjs:api:__MOCKED_API_FOLDER__:handler',
async (span) => {
span.setAttribute('code.function', 'handler')
span.setAttribute('code.filepath', '__MOCKED_FILENAME__')
try {
const RW_OTEL_WRAPPER_INNER_RESULT = await __handler(event, context)
span.end()
return RW_OTEL_WRAPPER_INNER_RESULT
} catch (error) {
span.recordException(error)
span.setStatus({
code: 2,
message:
error?.message?.split('\n')[0] ?? error?.toString()?.split('\n')[0],
})
span.end()
throw error
}
}
)
return RW_OTEL_WRAPPER_RESULT
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { authDecoder } from '@redwoodjs/auth-dbauth-api'
import { createGraphQLHandler } from '@redwoodjs/graphql-server'

import directives from 'src/directives/**/*.{js,ts}'
import sdls from 'src/graphql/**/*.sdl.{js,ts}'
import services from 'src/services/**/*.{js,ts}'

import { getCurrentUser } from 'src/lib/auth'
import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'

export const handler = createGraphQLHandler({
authDecoder,
getCurrentUser,
loggerConfig: { logger, options: {} },
directives,
sdls,
services,
onException: () => {
// Disconnect from your database with an unhandled exception.
db.$disconnect()
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { trace as RW_OTEL_WRAPPER_TRACE } from '@opentelemetry/api'
import { authDecoder } from '@redwoodjs/auth-dbauth-api'
import { createGraphQLHandler } from '@redwoodjs/graphql-server'
import directives from 'src/directives/**/*.{js,ts}'
import sdls from 'src/graphql/**/*.sdl.{js,ts}'
import services from 'src/services/**/*.{js,ts}'
import { getCurrentUser } from 'src/lib/auth'
import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'
export const handler = createGraphQLHandler({
authDecoder,
getCurrentUser,
loggerConfig: {
logger,
options: {},
},
directives,
sdls,
services,
onException: () => {
// Disconnect from your database with an unhandled exception.
db.$disconnect()
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'

import { db } from './db'

export const getCurrentUser = async (session) => {
if (!session || typeof session.id !== 'number') {
throw new Error('Invalid session')
}

return await db.user.findUnique({
where: { id: session.id },
select: { id: true, roles: true, email: true },
})
}

export const isAuthenticated = () => {
return !!context.currentUser
}

export const hasRole = (roles) => {
if (!isAuthenticated()) {
return false
}

const currentUserRoles = context.currentUser?.roles

if (typeof roles === 'string') {
if (typeof currentUserRoles === 'string') {
// roles to check is a string, currentUser.roles is a string
return currentUserRoles === roles
} else if (Array.isArray(currentUserRoles)) {
// roles to check is a string, currentUser.roles is an array
return currentUserRoles?.some((allowedRole) => roles === allowedRole)
}
}

if (Array.isArray(roles)) {
if (Array.isArray(currentUserRoles)) {
// roles to check is an array, currentUser.roles is an array
return currentUserRoles?.some((allowedRole) =>
roles.includes(allowedRole)
)
} else if (typeof currentUserRoles === 'string') {
// roles to check is an array, currentUser.roles is a string
return roles.some((allowedRole) => currentUserRoles === allowedRole)
}
}

// roles not found
return false
}

export const requireAuth = ({ roles } = {}) => {
if (!isAuthenticated()) {
throw new AuthenticationError("You don't have permission to do that.")
}

if (roles && !hasRole(roles)) {
throw new ForbiddenError("You don't have access to do that.")
}
}
Loading

0 comments on commit 10e6ccf

Please sign in to comment.