Merge branch 'main' into andrew/web-3448-enable-auto-joining-a-workspace-as-a-setting

This commit is contained in:
andrewwallacespeckle
2025-05-28 10:47:57 +02:00
16 changed files with 344 additions and 198 deletions
-20
View File
@@ -372,11 +372,6 @@ workflows:
- get-version
- publish-approval
- update-helm-documentation:
filters: *filters-publish
requires:
- publish-helm-chart
- publish-npm:
filters:
tags:
@@ -1110,21 +1105,6 @@ jobs:
name: Publish Helm Chart
command: ./.circleci/publish_helm_chart.sh
update-helm-documentation:
<<: *docker-node-image
working_directory: *work-dir
steps:
- checkout
- attach_workspace:
at: /tmp/ci/workspace
- run: cat workspace/env-vars >> $BASH_ENV
- add_ssh_keys:
fingerprints:
- '4d:68:70:66:49:97:ba:8b:8c:55:96:df:3d:be:6e:05'
- run:
name: Update Helm Documentation
command: ./.circleci/update_helm_documentation.sh
publish-viewer-sandbox-cloudflare-pages:
<<: *docker-node-image
working_directory: *work-dir
-71
View File
@@ -1,71 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
if ! command -v node &> /dev/null
then
echo "🛑 node could not be found. Please install node (and ensure it is in your PATH) before trying again."
exit 1
fi
if ! command -v git &> /dev/null
then
echo "🛑 git could not be found. Please install git (and ensure it is in your PATH) before trying again."
exit 1
fi
GIT_ROOT="$(git rev-parse --show-toplevel)"
README_GENERATOR_DIR="${GIT_ROOT}/../readme-generator-for-helm"
HELM_DIR="${GIT_ROOT}/../speckle-helm"
HELM_GIT_TARGET_BRANCH="gh-pages"
if [ ! -d "${README_GENERATOR_DIR}" ]; then
echo "🔭 Could not find 'readme-generator-for-helm' in a sibling directory"
echo "👩‍👩‍👧‍👧 Proceeding with cloning readme-generator-for-helm to a sibling directory, 'readme-generator-for-helm'"
git clone git@github.com:bitnami-labs/readme-generator-for-helm.git "${README_GENERATOR_DIR}"
fi
if [ -n "${CI}" ]; then
git config --global user.email "devops+circleci@speckle.systems"
git config --global user.name "CI"
fi
pushd "${README_GENERATOR_DIR}"
echo "✨ Updating to the latest version of readme-generator-for-helm"
git switch main
git pull origin main
npm install
popd
if [ ! -d "${HELM_DIR}" ]; then
echo "🔭 Could not find Speckle Helm in a sibling directory (named 'speckle-helm')"
echo "👩‍👩‍👧‍👧 Proceeding with cloning Speckle's helm repository to a sibling directory, 'speckle-helm'"
git clone git@github.com:specklesystems/helm.git "${HELM_DIR}"
fi
pushd "${HELM_DIR}"
echo "✨ Updating to the latest version of Speckle helm"
git switch main
git pull origin main
echo "🍽 Preparing gh-pages branch for updates"
git switch "${HELM_GIT_TARGET_BRANCH}"
git pull origin "${HELM_GIT_TARGET_BRANCH}"
popd
pushd "${GIT_ROOT}"
echo "🏗 Generating the documentation"
node "${README_GENERATOR_DIR}/bin/index.js" \
--config "${GIT_ROOT}/utils/helm/.helm-readme-configuration.json" \
--values "${GIT_ROOT}/utils/helm/speckle-server/values.yaml" \
--readme "${HELM_DIR}/README.md"
popd
pushd "${HELM_DIR}"
echo "🌳 Preparing commit to branch '${HELM_GIT_TARGET_BRANCH}' for Helm README..."
if [[ $(git status --porcelain) ]]; then
git add README.md
git commit -m "Updating README with revised parameters from values.yaml of Helm Chart."
git push --set-upstream origin "${HELM_GIT_TARGET_BRANCH}"
fi
popd
echo "✅ All done 🎉"
+8 -8
View File
@@ -36,14 +36,14 @@ repos:
args:
- --ignore=E501 # ignoring error about lines that are too long
# - repo: local
# hooks:
# - id: helm-documentation
# name: Helm Json Schema
# language: system
# files: utils\/helm\/speckle\-server\/values\.yaml
# entry: utils/helm/update-schema-json.sh
# description: If this fails it is because the values.yaml file was updated. Or has missing or incorrect documentation.
- repo: local
hooks:
- id: helm-documentation
name: Helm Json Schema
language: system
files: utils\/helm\/speckle\-server\/values\.yaml
entry: utils/helm/update-schema-json.sh
description: If this fails it is because the values.yaml file was updated. Or has missing or incorrect documentation.
# helmlint should occur after the json schema is updated
- repo: https://github.com/gruntwork-io/pre-commit
@@ -126,6 +126,7 @@ import { useFilterUtilities } from '~/lib/viewer/composables/ui'
import { projectsRoute, workspaceRoute } from '~~/lib/common/helpers/route'
import { useMixpanel } from '~/lib/core/composables/mp'
import { writableAsyncComputed } from '~/lib/common/composables/async'
import { parseUrlParameters, resourceBuilder } from '@speckle/shared/viewer/route'
graphql(`
fragment ModelPageProject on Project {
@@ -197,15 +198,20 @@ const limitsDialogType = ref<'version' | 'comment' | 'federated'>('version')
// Check for missing referencedObject in url referenced versions (out of plan limits)
const hasMissingReferencedObject = computed(() => {
const resourceIds = resourceIdString.value.split(',')
const resources = parseUrlParameters(resourceIdString.value)
const result = modelsAndVersionIds.value.some((item) => {
const version = item.model?.versions?.items?.find((v) => v.id === item.versionId)
const version = item.model?.loadedVersion?.items?.find(
(v) => v.id === item.versionId
)
if (version && version.referencedObject === null) {
// Check if this model+version is in the URL (latest version always available)
const modelVersionString = `${item.model.id}@${item.versionId}`.toLowerCase()
const isInUrl = resourceIds.some((r) => r.toLowerCase() === modelVersionString)
if (!version || version.referencedObject === null) {
const modelVersionString = resourceBuilder()
.addModel(item.model.id, item.versionId)
.toString()
const isInUrl = resources.some(
(r) => r.toString().toLowerCase() === modelVersionString
)
return isInUrl
}
@@ -298,18 +304,11 @@ watch(
}
showLimitsDialog.value = true
return
}
// If no workspace and no missing objects, don't show dialog
if (!project.value?.workspace) {
showLimitsDialog.value = false
return
}
// Only show comment dialog if it's a federated view AND we have a missing referenced object
if (missingThread && isFederated.value && hasMissingReferencedObject.value) {
} else if (missingThread && isFederated.value && hasMissingReferencedObject.value) {
limitsDialogType.value = 'comment'
showLimitsDialog.value = true
} else {
showLimitsDialog.value = false
}
},
{ immediate: true }
+7 -6
View File
@@ -20,11 +20,13 @@
</template>
<div class="flex flex-col items-center justify-center p-4 relative">
<h1 class="text-heading-xl text-foreground mb-3">
<template v-if="!showEmbed">Book an intro call</template>
<template v-if="!showEmbed">Do you want a personal onboarding call?</template>
<template v-else>Find a time</template>
</h1>
<template v-if="!showEmbed">
<p class="text-body-sm text-foreground-2">We'd love to help you get started</p>
<p class="text-body-sm text-foreground-2">
Get started like our most successful users
</p>
<div class="flex flex-col gap-3 w-full md:max-w-96 mt-8">
<FormRadioGroup v-model="bookDemoSelected" :options="options" is-stacked />
</div>
@@ -77,13 +79,12 @@ const showEmbed = ref(false)
const options = computed(() => [
{
value: 'yes',
title: `Yes, let's talk`,
subtitle: 'Find a time in the next step'
title: `Yes, help me get started`,
subtitle: 'Schedule your 15 minute call in the next step'
},
{
value: 'no',
title: 'No, maybe later',
subtitle: 'You can also book a time later'
title: 'No, I will pass for now'
}
])
@@ -0,0 +1,9 @@
export const ExpectedAuthFailure = {
UnverifiedEmailSSOLoginError: 'UnverifiedEmailSSOLoginError',
UserInputError: 'UserInputError',
InviteNotFoundError: 'InviteNotFoundError',
InvalidGrantError: 'InvalidGrantError'
} as const
export type ExpectedAuthFailure =
(typeof ExpectedAuthFailure)[keyof typeof ExpectedAuthFailure]
+3 -1
View File
@@ -120,7 +120,9 @@ const commonBuilderDeps = {
validateServerInvite,
finalizeInvitedServerRegistration,
resolveAuthRedirectPath,
passportAuthenticateHandlerBuilder: passportAuthenticateHandlerBuilderFactory()
passportAuthenticateHandlerBuilder: passportAuthenticateHandlerBuilderFactory({
resolveAuthRedirectPath
})
}
const setupStrategies = setupStrategiesFactory({
githubStrategyBuilder: githubStrategyBuilderFactory({
+20 -10
View File
@@ -113,6 +113,15 @@ export default function (app: Express) {
app.options('/auth/token', corsMiddlewareFactory())
app.post('/auth/token', corsMiddlewareFactory(), async (req, res) => {
try {
if (!req.body.appId)
throw new BadRequestError(
`Invalid request, insufficient information provided. App Id is required.`
)
if (!req.body.appSecret)
throw new BadRequestError(
`Invalid request, insufficient information provided. App Secret is required.`
)
const createRefreshToken = createRefreshTokenFactory({ db })
const getApp = getAppFactory({ db })
const createAppToken = createAppTokenFactory({
@@ -143,9 +152,11 @@ export default function (app: Express) {
})
// Token refresh
if (req.body.refreshToken) {
if (!req.body.appId || !req.body.appSecret)
throw new BadRequestError('Invalid request - App Id and Secret are required.')
if ('refreshToken' in req.body) {
if (!req.body.refreshToken)
throw new BadRequestError(
'Invalid request, insufficient information provided. A valid refresh token is required.'
)
const authResponse = await withOperationLogging(
async () =>
@@ -164,14 +175,13 @@ export default function (app: Express) {
}
// Access-code - token exchange
if (
!req.body.appId ||
!req.body.appSecret ||
!req.body.accessCode ||
!req.body.challenge
)
if (!req.body.accessCode)
throw new BadRequestError(
`Invalid request, insufficient information provided in the request. App Id, Secret, Access Code, and Challenge are required.`
`Invalid request, insufficient information provided. Access Code is required.`
)
if (!req.body.challenge)
throw new BadRequestError(
`Invalid request, insufficient information provided. Challenge is required.`
)
const authResponse = await withOperationLogging(
@@ -1,14 +1,11 @@
import passport, { Strategy, AuthenticateOptions } from 'passport'
import { getFrontendOrigin } from '@/modules/shared/helpers/envHelper'
import {
UnverifiedEmailSSOLoginError,
UserInputError
} from '@/modules/core/errors/userinput'
import type { Strategy, AuthenticateOptions } from 'passport'
import passport from 'passport'
import type { Request, Response, NextFunction, RequestHandler } from 'express'
import { ensureError, Optional } from '@speckle/shared'
import { ensureError, type Optional, throwUncoveredError } from '@speckle/shared'
import { get, isArray, isObjectLike, isString } from 'lodash'
import { PassportAuthenticateHandlerBuilder } from '@/modules/auth/domain/operations'
import { InviteNotFoundError } from '@/modules/serverinvites/errors'
import type { PassportAuthenticateHandlerBuilder } from '@/modules/auth/domain/operations'
import { ExpectedAuthFailure } from '@/modules/auth/domain/const'
import type { ResolveAuthRedirectPath } from '@/modules/serverinvites/services/operations'
const resolveInfoMessage = (
info?: Optional<string | Record<string, unknown> | Array<string | undefined>>
@@ -27,61 +24,147 @@ const resolveInfoMessage = (
return null
}
const resolveFailureType = (
info?: Optional<string | Record<string, unknown> | Array<string | undefined>>
) => {
if (!info) return null
if (isString(info)) return null
if (isArray(info)) return null
if (isObjectLike(info)) {
const failureType = get(info, 'failureType')
if (
isString(failureType) &&
Object.values(ExpectedAuthFailure).includes(failureType as ExpectedAuthFailure)
) {
return failureType as ExpectedAuthFailure
}
}
return null
}
const resolveEmail = (
info?: Optional<string | Record<string, unknown> | Array<string | undefined>>
) => {
if (!info) return ''
if (isString(info)) return ''
if (isArray(info)) return ''
if (isObjectLike(info)) {
const email = get(info, 'email')
if (isString(email)) {
return email
}
}
return ''
}
const defaultErrorPath = (message: string) => `/error?message=${message}`
const unverifiedEmailPath = (email: string) => `/error-email-verify?email=${email}`
const buildRedirectUrl = (params: {
resolveAuthRedirectPath: ResolveAuthRedirectPath
path: string
}) => new URL(params.path, params.resolveAuthRedirectPath()).toString()
export const passportAuthenticationCallbackFactory =
(context: {
strategy: Strategy | string
req: Request
res: Response
next: NextFunction
resolveAuthRedirectPath: ResolveAuthRedirectPath
}) =>
(
e: unknown,
callbackError: unknown,
user: Optional<Express.User>,
info: Optional<string | Record<string, unknown> | Array<string | undefined>>
) => {
const { strategy, req, res, next } = context
const { strategy, req, res, next, resolveAuthRedirectPath } = context
if (user && !e) {
let e = callbackError
let failureType = resolveFailureType(info)
// WORKAROUND
// passportjs states that 'verify' method of the strategy should not pass in
// an error for user input problems.
// Unfortunately openid-client <6.0.0 does not provide a third 'info' parameter
// so we rely on user-input problems being passed to callback as errors.
// This is a workaround until we upgrade to openid-client >=6.0.0
if (e && strategy === 'oidc' && failureType === null) {
switch (e.constructor.name) {
case ExpectedAuthFailure.UserInputError:
case ExpectedAuthFailure.InviteNotFoundError:
case ExpectedAuthFailure.UnverifiedEmailSSOLoginError:
// the error was being overloaded with user input problem information
// so we need to extract it and set it as the info
// and set the error to null
failureType = e.constructor.name
e = null
break
default:
// what we have is an unexpected error, so nothing needs to change
break
}
}
if (e) {
const err = ensureError(
e,
'Unknown authentication error. Please contact server admins'
)
// unknown and unexpected error
req.log.error({ err, strategy }, 'Authentication error for strategy "{strategy}"')
return next(err)
}
if (user && failureType === null) {
req.user = user
// user authenticated successfully
next()
return
}
// no user, but no error either. This is expected in some cases (e.g. user input error)
// in this case, we need to redirect the user to the correct page
const infoMsg = resolveInfoMessage(info)
if (!user && !e) {
// no user despite there being no error, so authentication failed
const message = infoMsg || 'Failed to authenticate, contact server admins'
res.redirect(new URL(defaultErrorPath(message), getFrontendOrigin()).toString())
return
}
const err = ensureError(
e,
'Unknown authentication error. Please contact server admins'
)
switch (err.constructor) {
case UserInputError:
case InviteNotFoundError:
const message = infoMsg || err.message
res.redirect(new URL(defaultErrorPath(message), getFrontendOrigin()).toString())
return
case UnverifiedEmailSSOLoginError:
const email = (err as UnverifiedEmailSSOLoginError).info()?.email || ''
switch (failureType) {
case ExpectedAuthFailure.UserInputError:
case ExpectedAuthFailure.InviteNotFoundError:
case ExpectedAuthFailure.InvalidGrantError:
res.redirect(
new URL(unverifiedEmailPath(email), getFrontendOrigin()).toString()
buildRedirectUrl({
resolveAuthRedirectPath,
path: defaultErrorPath(
infoMsg || 'Failed to authenticate, contact server admins'
)
})
)
return
case ExpectedAuthFailure.UnverifiedEmailSSOLoginError:
const email = resolveEmail(info)
res.redirect(
buildRedirectUrl({
resolveAuthRedirectPath,
path: unverifiedEmailPath(email)
})
)
return
case null:
// unexpected error or missing info
req.log.error(
{ info, strategy },
"Authentication error for strategy '{strategy}' encountered an unexpected failure type or 'info' parameter is missing or invalid"
)
const message = infoMsg || 'Failed to authenticate, contact server admins'
res.redirect(
buildRedirectUrl({
resolveAuthRedirectPath,
path: defaultErrorPath(message)
})
)
return
default:
req.log.error(
{ err, strategy },
'Authentication error for strategy "{strategy}"'
)
return next(err)
// throwUncoveredError(err) //TODO at some point in the future, ideally change error handling to use this
throwUncoveredError(failureType)
}
}
@@ -90,7 +173,9 @@ export const passportAuthenticationCallbackFactory =
* (passport.authenticate() by default doesn't, so don't use it)
*/
export const passportAuthenticateHandlerBuilderFactory =
(): PassportAuthenticateHandlerBuilder =>
(deps: {
resolveAuthRedirectPath: ResolveAuthRedirectPath
}): PassportAuthenticateHandlerBuilder =>
(
strategy: Strategy | string,
options: Optional<AuthenticateOptions> = undefined
@@ -99,7 +184,7 @@ export const passportAuthenticateHandlerBuilderFactory =
passport.authenticate(
strategy,
options || {},
passportAuthenticationCallbackFactory({ strategy, req, res, next })
passportAuthenticationCallbackFactory({ ...deps, strategy, req, res, next })
)(req, res, next)
}
}
@@ -34,6 +34,7 @@ import crs from 'crypto-random-string'
import { GetServerInfo } from '@/modules/core/domain/server/operations'
import { EnvironmentResourceError } from '@/modules/shared/errors'
import { InviteNotFoundError } from '@/modules/serverinvites/errors'
import { ExpectedAuthFailure } from '@/modules/auth/domain/const'
const githubStrategyBuilderFactory =
(deps: {
@@ -164,11 +165,14 @@ const githubStrategyBuilderFactory =
case InviteNotFoundError:
case UnverifiedEmailSSOLoginError:
logger.info({ err: e }, 'Auth error for GitHub strategy')
// note; passportjs suggests that err should be null for user input errors.
// However, we are relying on the error being passed to `passportAuthenticationCallbackFactory` and handling it there
return done(e, false, { message: e.message })
// note; passportjs suggests err should be null for user input errors.
// We also need to pass the error type in the info parameter
// so `passportAuthenticationCallbackFactory` can handle redirects appropriately
return done(null, false, {
message: e.message,
failureType: e.constructor.name as ExpectedAuthFailure
})
default:
logger.error({ err: e }, 'Auth error for GitHub strategy')
return done(e, false, { message: e.message })
}
}
@@ -28,7 +28,7 @@ import {
} from '@/modules/core/domain/users/operations'
import { GetServerInfo } from '@/modules/core/domain/server/operations'
import { EnvironmentResourceError } from '@/modules/shared/errors'
import { InviteNotFoundError } from '@/modules/serverinvites/errors'
import { ExpectedAuthFailure } from '@/modules/auth/domain/const'
const googleStrategyBuilderFactory =
(deps: {
@@ -142,15 +142,47 @@ const googleStrategyBuilderFactory =
err,
'Unexpected issue occured while authenticating with Google'
)
switch (e.constructor) {
case UnverifiedEmailSSOLoginError:
case UserInputError:
case InviteNotFoundError:
switch (e.constructor.name) {
case ExpectedAuthFailure.UserInputError:
case ExpectedAuthFailure.InviteNotFoundError:
logger.info({ err: e }, 'Auth error for Google strategy')
// note; passportjs suggests that err should be null for user input errors.
// However, we are relying on the error being passed to `passportAuthenticationCallbackFactory` and handling it there
return done(e, false, { message: e.message })
// note; passportjs suggests err should be null for user input errors.
// We also need to pass the error type in the info parameter
// so `passportAuthenticationCallbackFactory` can handle redirects appropriately
return done(null, false, {
message: e.message,
failureType: e.constructor.name as ExpectedAuthFailure
})
case ExpectedAuthFailure.UnverifiedEmailSSOLoginError:
logger.info({ err: e }, 'Auth error for Google strategy')
// note; passportjs suggests err should be null for user input errors.
// We also need to pass the error type in the info parameter
// so `passportAuthenticationCallbackFactory` can handle redirects appropriately
return done(null, false, {
message: e.message,
failureType: e.constructor.name as ExpectedAuthFailure,
email: (e as UnverifiedEmailSSOLoginError).info().email
})
default:
// handle other common errors thrown by the underlying client libraries
if (
e.name === 'TokenError' &&
'code' in e &&
e.code === 'invalid_grant'
) {
req.log.warn(
{ err: e },
"Authentication error for strategy 'google' encountered an Invalid Grant error"
)
// This is a common error from Google and a number of reasons
// can cause it. Many user-related issues, so we will treat it as user-related.
// https://blog.timekit.io/google-oauth-invalid-grant-nightmare-and-how-to-fix-it-9f4efaf1da35
return done(null, false, {
message: e.message,
failureType: ExpectedAuthFailure.InvalidGrantError
})
}
logger.error({ err: e }, 'Auth error for Google strategy')
return done(e, false, { message: e.message })
}
@@ -164,6 +164,7 @@ const oidcStrategyBuilderFactory =
logger.info({ err: e }, 'Auth error for OIDC strategy')
// note; passportjs suggests that err should be null for user input errors.
// However, we are relying on the error being passed to `passportAuthenticationCallbackFactory` and handling it there
// as openid-client <6.0.0 does not support a third 'info' parameter for the callback function.
return done(e, undefined)
default:
logger.error({ err: e }, 'Auth error for OIDC strategy')
+93 -12
View File
@@ -74,6 +74,7 @@ import { authorizeResolver } from '@/modules/shared'
import { UserInputError } from '@/modules/core/errors/userinput'
import { createRandomEmail } from '@/modules/core/helpers/testHelpers'
import cryptoRandomString from 'crypto-random-string'
import { getFrontendOrigin } from '@/modules/shared/helpers/envHelper'
const getServerInfo = getServerInfoFactory({ db })
const getUser = getUserFactory({ db })
@@ -691,7 +692,8 @@ describe('Auth @auth', () => {
strategy: 'wotStrategy',
req,
res,
next
next,
resolveAuthRedirectPath: () => getFrontendOrigin()
})
const userId = cryptoRandomString({ length: 4 })
@@ -709,7 +711,7 @@ describe('Auth @auth', () => {
'next request handler should have been called'
).to.equal(1)
})
it('Should handle case where there is an unexpected error and a user', async () => {
it('Should handle case where there is an unexpected error but also a user', async () => {
const req = httpMocks.createRequest()
req.log = logger
const res = httpMocks.createResponse()
@@ -725,7 +727,8 @@ describe('Auth @auth', () => {
strategy: 'wotStrategy',
req,
res,
next
next,
resolveAuthRedirectPath: () => getFrontendOrigin()
})
SUT(
@@ -745,7 +748,7 @@ describe('Auth @auth', () => {
'next request handler should have been called'
).to.equal(1)
})
it('Should handle case where there is a user-derived error and a user', async () => {
it('Should handle case where there is a user-derived failure but also a user', async () => {
const req = httpMocks.createRequest()
req.log = logger
const res = httpMocks.createResponse()
@@ -761,13 +764,14 @@ describe('Auth @auth', () => {
strategy: 'wotStrategy',
req,
res,
next
next,
resolveAuthRedirectPath: () => getFrontendOrigin()
})
SUT(
new UserInputError('I brrrrroke'),
null,
{ id: cryptoRandomString({ length: 4 }), email: createRandomEmail() },
undefined
{ failureType: 'UserInputError' }
)
expect(
res._getRedirectUrl().includes('/error'),
@@ -799,7 +803,8 @@ describe('Auth @auth', () => {
strategy: 'wotStrategy',
req,
res,
next
next,
resolveAuthRedirectPath: () => getFrontendOrigin()
})
SUT(null, undefined, undefined)
@@ -817,7 +822,7 @@ describe('Auth @auth', () => {
'next request handler should not have been called'
).to.equal(0)
})
it('Should handle case where there is a user-derived error but no user', async () => {
it('Should handle case where there is a user-derived failure and no user', async () => {
const req = httpMocks.createRequest()
req.log = logger
const res = httpMocks.createResponse()
@@ -833,10 +838,11 @@ describe('Auth @auth', () => {
strategy: 'wotStrategy',
req,
res,
next
next,
resolveAuthRedirectPath: () => getFrontendOrigin()
})
SUT(new UserInputError('I brrrrroke'), undefined, undefined)
SUT(null, undefined, { failureType: 'UserInputError' })
expect(
res._getRedirectUrl().includes('/error'),
`Redirect url was '${res._getRedirectUrl()}'`
@@ -867,7 +873,8 @@ describe('Auth @auth', () => {
strategy: 'wotStrategy',
req,
res,
next
next,
resolveAuthRedirectPath: () => getFrontendOrigin()
})
SUT(new Error('surprise!!!'), undefined, undefined)
@@ -881,5 +888,79 @@ describe('Auth @auth', () => {
'next request handler should have been called with an error "next(err)"'
).to.equal(1)
})
it('should handle invalid grant user-derived failure and no user', async () => {
const req = httpMocks.createRequest()
req.log = logger
const res = httpMocks.createResponse()
let errorCalledCounter = 0
let nextCalledCounter = 0
const next = (err: unknown) => {
if (err) {
errorCalledCounter++
}
nextCalledCounter++
}
const SUT = passportAuthenticationCallbackFactory({
strategy: 'google',
req,
res,
next,
resolveAuthRedirectPath: () => getFrontendOrigin()
})
SUT(null, undefined, {
failureType: 'InvalidGrantError',
message: 'Some kind of invalid grant'
})
expect(
res._getRedirectUrl().includes('/error'),
`Redirect url was '${res._getRedirectUrl()}'`
).to.be.true
expect(req).not.to.have.property('user')
expect(
errorCalledCounter,
'error request handler "next(err)" should not have been called'
).to.equal(0)
expect(
nextCalledCounter,
'next request handler should not have been called'
).to.equal(0)
})
//TODO remove this if we upgrade to openid-client >=6.0.0
it('should handle case for OIDC with a user-derived error and no user', async () => {
const req = httpMocks.createRequest()
req.log = logger
const res = httpMocks.createResponse()
let errorCalledCounter = 0
let nextCalledCounter = 0
const next = (err: unknown) => {
if (err) {
errorCalledCounter++
}
nextCalledCounter++
}
const SUT = passportAuthenticationCallbackFactory({
strategy: 'oidc',
req,
res,
next,
resolveAuthRedirectPath: () => getFrontendOrigin()
})
SUT(new UserInputError('oidc version <6 is special'), undefined, undefined)
expect(
res._getRedirectUrl().includes('/error'),
`Redirect url was '${res._getRedirectUrl()}'`
).to.be.true
expect(req).not.to.have.property('user')
expect(
errorCalledCounter,
'error request handler "next(err)" should not have been called'
).to.equal(0)
expect(
nextCalledCounter,
'next request handler should not have been called'
).to.equal(0)
})
})
})
@@ -131,7 +131,7 @@ export = {
await validateScopes(context.scopes, Scopes.Users.Read)
if (args.query.length < 3)
throw new BadRequestError('Search query must be at least 3 carachters.')
throw new BadRequestError('Search query must be at least 3 characters.')
if (args.limit && args.limit > 100)
throw new BadRequestError(
@@ -34,6 +34,12 @@ const prepInsertionObject = (
obj.id =
obj.id || crypto.createHash('md5').update(JSON.stringify(obj)).digest('hex') // generate a hash if none is present
if (obj.id.length !== 32) {
throw new ObjectHandlingError(
`Invalid object ID. Object ID: ${obj.id}. Object ID's must be hashes represented by a string of 32 characters.`
)
}
const stringifiedObj = JSON.stringify(obj)
const objectByteSize = estimateStringMegabyteSize(stringifiedObj)
if (objectByteSize > MAX_OBJECT_SIZE_MB) {
+8 -1
View File
@@ -21,7 +21,14 @@ JSON_SCHEMA_PATH="${GIT_ROOT}/utils/helm/speckle-server/values.schema.json"
if [ ! -d "${README_GENERATOR_DIR}" ]; then
echo "🔭 Could not find readme-generator-for-helm in a sibling directory to speckle-server"
echo "👩‍👩‍👧‍👧 Proceeding with cloning readme-generator-for-helm to a sibling directory, readme-generator-for-helm"
git clone git@github.com:bitnami-labs/readme-generator-for-helm.git "${README_GENERATOR_DIR}"
SSH_OUTPUT="$(ssh -T git@github.com 2>&1 || true)"
if echo "${SSH_OUTPUT}" | grep -q 'successfully authenticated'; then
echo "🔑 SSH authentication successful, cloning using SSH"
git clone git@github.com:bitnami-labs/readme-generator-for-helm.git "${README_GENERATOR_DIR}"
else
echo "🔑 SSH authentication failed, cloning using HTTPS"
git clone https://github.com/bitnami-labs/readme-generator-for-helm "${README_GENERATOR_DIR}"
fi
fi
pushd "${README_GENERATOR_DIR}"