Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(sso): gatekeeper #3442

Merged
merged 66 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
b38133e
feat(workspaces): add workspace sso feature flag
gjedlicska Sep 23, 2024
2486e5c
Merge branch 'main' of github.com:specklesystems/speckle-server into …
gjedlicska Sep 25, 2024
29d6bdc
feat(workspaceSso): wip validate sso
gjedlicska Sep 26, 2024
8b8b9e6
Merge branch 'main' of github.com:specklesystems/speckle-server into …
gjedlicska Sep 26, 2024
1c8f16e
Merge branch 'main' of github.com:specklesystems/speckle-server into …
gjedlicska Sep 27, 2024
7da225b
feat(workspaces): validate and add sso provider to the workspace with…
gjedlicska Oct 1, 2024
6149b9e
feat(workspaces): validate and add sso provider to the workspace with…
gjedlicska Oct 1, 2024
57295b9
WIP
Mikehrn Oct 1, 2024
36944b5
fix(sso): restructure to handle all branches at end of flow
cdriesler Oct 1, 2024
a6e6307
fix(sso): add and validate emails used for sso
cdriesler Oct 1, 2024
19a834d
Merged main
Mikehrn Oct 2, 2024
a78e9d3
fix(sso): park progress
cdriesler Oct 2, 2024
a014c10
chore(workspaces): review sso login/valdate
gjedlicska Oct 2, 2024
6f48668
Merge branch 'main' into mike/sso
Mikehrn Oct 7, 2024
b8726bf
fix(sso): adjust validate url
cdriesler Oct 7, 2024
b0d2bbf
chore(sso): auth header puzzle
cdriesler Oct 7, 2024
49bc0a4
fix(sso): happy-path config
cdriesler Oct 8, 2024
54e0450
Merge branch 'main' of github.com:specklesystems/speckle-server into …
cdriesler Oct 9, 2024
3063d33
chore(gql): gqlgen
cdriesler Oct 9, 2024
db041b9
Merge branch 'mike/sso' into charles/more-sso-validation
cdriesler Oct 9, 2024
c98a783
Merge branch 'main' into charles/more-sso-validation
cdriesler Oct 10, 2024
3df52df
fix(sso): almost almost
cdriesler Oct 10, 2024
27ac047
fix(sso): auth endpoint
cdriesler Oct 10, 2024
7a7424b
a lil more terse
cdriesler Oct 13, 2024
4566840
fix(sso): light at the end of the tunnel
cdriesler Oct 13, 2024
7dae4a7
fix(sso): improve catch block error messages
cdriesler Oct 13, 2024
355aaa7
fix(sso): session lifespan => validUntil
cdriesler Oct 14, 2024
0abc084
fix(sso): I think we've got it
cdriesler Oct 14, 2024
6011280
feat(sso): limited workspace values for public sso login
cdriesler Oct 16, 2024
2fc11a7
fix(sso): use factory functions
cdriesler Oct 20, 2024
611200c
Merge remote-tracking branch 'origin' into charles/limitedWorkspace
cdriesler Oct 20, 2024
2b5abde
Merge branch 'charles/limitedWorkspace' into charles/more-sso-validation
cdriesler Oct 20, 2024
48814f9
fix(sso): til decrypt is single-use
cdriesler Oct 20, 2024
55c505d
fix(sso): correct usage of access codes
cdriesler Oct 21, 2024
3d9b749
fix(sso): use finalize middleware in all routes
cdriesler Oct 21, 2024
e56124b
chore(sso): cheeky tweak
cdriesler Oct 21, 2024
73c3dc2
Merge branch 'main' of github.com:specklesystems/speckle-server into …
cdriesler Oct 22, 2024
7f11a8d
fix(sso): move some types around
cdriesler Oct 22, 2024
ea42192
fix(sso): stencil final shape I'm sleepy
cdriesler Oct 23, 2024
d4c7d26
fix(sso): more factories more factories
cdriesler Oct 23, 2024
5e296fc
fix(sso): on to final boss of factories
cdriesler Oct 23, 2024
5225752
fix(sso): needs a haircut but she works
cdriesler Oct 23, 2024
b7811c7
Merge remote-tracking branch 'origin' into charles/more-sso-validation
cdriesler Oct 23, 2024
786da16
fix(sso): init rest w function, not side-effects
cdriesler Oct 24, 2024
fe29fc7
fix(sso): /authn => /sso
cdriesler Oct 24, 2024
af24a3f
chore(sso): errors
cdriesler Oct 28, 2024
441e39d
chore(sso): test test test
cdriesler Oct 29, 2024
b4ecd16
chore(sso): test all the corners
cdriesler Oct 30, 2024
1b80b41
Merge branch 'main' of github.com:specklesystems/speckle-server into …
cdriesler Oct 30, 2024
d72808a
feat(sso): list workspace sso memberships
cdriesler Oct 31, 2024
78cceac
chore(sso): tests, expose in rest
cdriesler Oct 31, 2024
c9e9a2c
fix(sso): sketch active user auth
cdriesler Oct 31, 2024
8e5018a
Merge remote-tracking branch 'origin' into charles/tryFindWorkspace
cdriesler Oct 31, 2024
7b97821
fix(sso): expose search via gql
cdriesler Oct 31, 2024
e294be9
Merge remote-tracking branch 'origin' into charles/tryFindWorkspace
cdriesler Oct 31, 2024
2e05b86
Merge branch 'charles/tryFindWorkspace' into charles/activeUserSso
cdriesler Oct 31, 2024
f5ff307
fix(sso): active user session information
cdriesler Oct 31, 2024
3757eb3
Merge branch 'main' into charles/activeUserSso
cdriesler Nov 1, 2024
df1e40e
chore(sso): sso session test utils
cdriesler Nov 3, 2024
4f59462
chore(sso): test sso session repo/services
cdriesler Nov 4, 2024
e4f502d
chore(sso): gqlgen
cdriesler Nov 4, 2024
e775f9e
feat(sso): throw error on missing or expired sso session
cdriesler Nov 4, 2024
8d1fac2
Merge branch 'main' of github.com:specklesystems/speckle-server into …
cdriesler Nov 5, 2024
640f756
chore(sso): tests for SSO access protection
cdriesler Nov 5, 2024
ae505a0
fix(sso): use gatekeeper to protect sso access
cdriesler Nov 5, 2024
2cf9a2d
Merge remote-tracking branch 'origin' into charles/girlbossSso
cdriesler Nov 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/server/modules/gatekeeper/errors/features.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { BaseError } from '@/modules/shared/errors'

export class FeatureAccessForbiddenError extends BaseError {
static defaultMessage = 'Access to feature forbidden by current plan level.'
static code = 'GATEKEEPER_FEATURE_ACCESS_FORBIDDEN_ERROR'
static statusCode = 403
}
6 changes: 6 additions & 0 deletions packages/server/modules/workspaces/helpers/sso.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import { buildDecryptor, buildEncryptor } from '@/modules/shared/utils/libsodium
import { SsoVerificationCodeMissingError } from '@/modules/workspaces/errors/sso'
import { Request } from 'express'

declare module 'express-session' {
interface SessionData {
workspaceId?: string
}
}

/**
* Generate Speckle URL to redirect users to after they complete authorization
* with the given SSO provider.
Expand Down
89 changes: 48 additions & 41 deletions packages/server/modules/workspaces/rest/sso.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ import {
getProviderAuthorizationUrl,
initializeIssuerAndClient
} from '@/modules/workspaces/clients/oidcProvider'
import { adminOverrideEnabled } from '@/modules/shared/helpers/envHelper'
import {
adminOverrideEnabled,
getFeatureFlags,
isProdEnv
} from '@/modules/shared/helpers/envHelper'
import {
storeOIDCProviderValidationRequestFactory,
getOIDCProviderValidationRequestFactory,
Expand Down Expand Up @@ -112,6 +116,9 @@ import {
SsoVerificationCodeMissingError
} from '@/modules/workspaces/errors/sso'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { FeatureAccessForbiddenError } from '@/modules/gatekeeper/errors/features'
import { canWorkspaceUseOidcSsoFactory } from '@/modules/gatekeeper/services/featureAuthorization'
import { getWorkspacePlanFactory } from '@/modules/gatekeeper/repositories/billing'

const moveAuthParamsToSessionMiddleware = moveAuthParamsToSessionMiddlewareFactory()
const sessionMiddleware = sessionMiddlewareFactory()
Expand All @@ -120,6 +127,37 @@ const finalizeAuthMiddleware = finalizeAuthMiddlewareFactory({
getUser: legacyGetUserFactory({ db })
})

const moveWorkspaceIdToSessionMiddleware: RequestHandler<
WorkspaceSsoAuthRequestParams
> = async (req, _res, next) => {
const workspace = await getWorkspaceBySlugFactory({ db })({
workspaceSlug: req.params.workspaceSlug
})
req.session.workspaceId = workspace?.id
next()
}

const validateFeatureAccessMiddlewareFactory: RequestHandler<
WorkspaceSsoAuthRequestParams
> = async (req, res, next) => {
try {
if (!req.session.workspaceId) throw new FeatureAccessForbiddenError()

const isGatekeeperEnabled =
getFeatureFlags().FF_GATEKEEPER_MODULE_ENABLED && isProdEnv()
if (!isGatekeeperEnabled) return next()

const isAllowed = await canWorkspaceUseOidcSsoFactory({
getWorkspacePlan: getWorkspacePlanFactory({ db })
})({ workspaceId: req.session.workspaceId })
if (!isAllowed) throw new FeatureAccessForbiddenError()

next()
} catch (e) {
res?.redirect(buildErrorUrl(e, req.params.workspaceSlug))
}
}

export const getSsoRouter = (): Router => {
const router = Router()

Expand All @@ -143,13 +181,14 @@ export const getSsoRouter = (): Router => {
'/api/v1/workspaces/:workspaceSlug/sso/auth',
sessionMiddleware,
moveAuthParamsToSessionMiddleware,
moveWorkspaceIdToSessionMiddleware,
validateFeatureAccessMiddlewareFactory,
validateRequest({
params: z.object({
workspaceSlug: z.string().min(1)
})
}),
handleSsoAuthRequestFactory({
getWorkspaceBySlug: getWorkspaceBySlugFactory({ db }),
getWorkspaceSsoProvider: getWorkspaceSsoProviderFactory({
db,
decrypt: getDecryptor()
Expand All @@ -161,37 +200,15 @@ export const getSsoRouter = (): Router => {
'/api/v1/workspaces/:workspaceSlug/sso/oidc/validate',
sessionMiddleware,
moveAuthParamsToSessionMiddleware,
moveWorkspaceIdToSessionMiddleware,
validateFeatureAccessMiddlewareFactory,
validateRequest({
params: z.object({
workspaceSlug: z.string().min(1)
}),
query: oidcProvider
}),
handleSsoValidationRequestFactory({
getWorkspaceBySlug: getWorkspaceBySlugFactory({ db }),
startOidcSsoProviderValidation: startOidcSsoProviderValidationFactory({
getOidcProviderAttributes: getOIDCProviderAttributes,
storeOidcProviderValidationRequest: storeOIDCProviderValidationRequestFactory({
redis: getGenericRedis(),
encrypt: getEncryptor()
}),
generateCodeVerifier: generators.codeVerifier
})
})
)

router.get(
'/api/v1/workspaces/:workspaceSlug/sso/oidc/validate',
sessionMiddleware,
moveAuthParamsToSessionMiddleware,
validateRequest({
params: z.object({
workspaceSlug: z.string().min(1)
}),
query: oidcProvider
}),
handleSsoValidationRequestFactory({
getWorkspaceBySlug: getWorkspaceBySlugFactory({ db }),
startOidcSsoProviderValidation: startOidcSsoProviderValidationFactory({
getOidcProviderAttributes: getOIDCProviderAttributes,
storeOidcProviderValidationRequest: storeOIDCProviderValidationRequestFactory({
Expand Down Expand Up @@ -348,21 +365,16 @@ const handleGetLimitedWorkspaceRequestFactory =
*/
const handleSsoAuthRequestFactory =
({
getWorkspaceBySlug,
getWorkspaceSsoProvider
}: {
getWorkspaceBySlug: GetWorkspaceBySlug
getWorkspaceSsoProvider: GetWorkspaceSsoProvider
}): RequestHandler<WorkspaceSsoAuthRequestParams> =>
async ({ params, session, res }) => {
try {
const workspace = await getWorkspaceBySlug({
workspaceSlug: params.workspaceSlug
})
if (!workspace) throw new WorkspaceNotFoundError()
if (!session.workspaceId) throw new WorkspaceNotFoundError()

const { provider } =
(await getWorkspaceSsoProvider({ workspaceId: workspace.id })) ?? {}
(await getWorkspaceSsoProvider({ workspaceId: session.workspaceId })) ?? {}
if (!provider) throw new SsoProviderMissingError()

const codeVerifier = generators.codeVerifier()
Expand All @@ -387,10 +399,8 @@ type WorkspaceSsoValidationRequestQuery = z.infer<typeof oidcProvider>
*/
const handleSsoValidationRequestFactory =
({
getWorkspaceBySlug,
startOidcSsoProviderValidation
}: {
getWorkspaceBySlug: GetWorkspaceBySlug
startOidcSsoProviderValidation: ReturnType<
typeof startOidcSsoProviderValidationFactory
>
Expand All @@ -402,14 +412,11 @@ const handleSsoValidationRequestFactory =
> =>
async ({ session, params, query: provider, res, context }) => {
try {
const workspace = await getWorkspaceBySlug({
workspaceSlug: params.workspaceSlug
})
if (!workspace) throw new WorkspaceNotFoundError()
if (!session.workspaceId) throw new WorkspaceNotFoundError()

await authorizeResolver(
context.userId,
workspace.id,
session.workspaceId,
Roles.Workspace.Admin,
context.resourceAccessRules
)
Expand Down Expand Up @@ -521,7 +528,7 @@ const handleOidcCallbackFactory =
}
})

req.authRedirectPath = buildFinalizeUrl(workspace.slug).toString()
req.authRedirectPath = buildFinalizeUrl(req.params.workspaceSlug).toString()
}

const createOidcProviderFactory =
Expand Down