feat: authenticate user as function author (#2316)
* feat: check if user is fn author w/ exec engine * WIP auth redirect * finalized
This commit is contained in:
committed by
GitHub
parent
8f2974f8d4
commit
95aa58958f
@@ -38,8 +38,6 @@ NUXT_PUBLIC_DEBUG_CORE_WEB_VITALS=false
|
||||
|
||||
# Enable Speckle Automate functionality
|
||||
NUXT_PUBLIC_ENABLE_AUTOMATE_MODULE=false
|
||||
NUXT_PUBLIC_AUTOMATE_GH_CLIENT_ID=Iv1.79a1df48749f11b4
|
||||
AUTOMATE_GH_CLIENT_SECRET=5bb28a6397204edf259f3d40cf36afc6a95c3998
|
||||
|
||||
# Survicate
|
||||
NUXT_PUBLIC_SURVICATE_WORKSPACE_KEY=
|
||||
|
||||
@@ -50,6 +50,7 @@ import type {
|
||||
CreatableFunctionTemplate,
|
||||
FunctionDetailsFormValues
|
||||
} from '~/lib/automate/helpers/functions'
|
||||
import { automateGithubAppAuthorizationRoute } from '~/lib/common/helpers/route'
|
||||
import { useEnumSteps, useEnumStepsWidgetSetup } from '~/lib/form/composables/steps'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { useCreateAutomateFunction } from '~/lib/automate/composables/management'
|
||||
@@ -152,8 +153,8 @@ const title = computed(() => {
|
||||
})
|
||||
|
||||
const authorizeGithubUrl = computed(() => {
|
||||
// TODO:
|
||||
return new URL('/', apiBaseUrl).toString()
|
||||
const redirectUrl = new URL(automateGithubAppAuthorizationRoute, apiBaseUrl)
|
||||
return redirectUrl.toString()
|
||||
})
|
||||
|
||||
const buttons = computed((): LayoutDialogButton[] => {
|
||||
@@ -174,7 +175,6 @@ const buttons = computed((): LayoutDialogButton[] => {
|
||||
text: 'Authorize',
|
||||
props: {
|
||||
fullWidth: true,
|
||||
disabled: true,
|
||||
to: authorizeGithubUrl.value,
|
||||
external: true
|
||||
}
|
||||
@@ -187,7 +187,7 @@ const buttons = computed((): LayoutDialogButton[] => {
|
||||
text: 'Next',
|
||||
props: {
|
||||
iconRight: ChevronRightIcon,
|
||||
disabled: !selectedTemplate.value
|
||||
disabled: !selectedTemplate.value || true // TODO: Remove once fns work
|
||||
},
|
||||
onClick: () => step.value++
|
||||
}
|
||||
@@ -200,7 +200,8 @@ const buttons = computed((): LayoutDialogButton[] => {
|
||||
props: {
|
||||
color: 'secondary',
|
||||
iconLeft: ChevronLeftIcon,
|
||||
textColor: 'primary'
|
||||
textColor: 'primary',
|
||||
disabled: true // TODO: Remove once fns work
|
||||
},
|
||||
onClick: () => step.value--
|
||||
},
|
||||
|
||||
@@ -55,7 +55,7 @@ export const projectWebhooksRoute = (projectId: string) =>
|
||||
export const threadRedirectRoute = (projectId: string, threadId: string) =>
|
||||
`/projects/${projectId}/threads/${threadId}`
|
||||
|
||||
export const automateGithubAppAuthorizationCallback = '/api/auth/automate-github-app'
|
||||
export const automateGithubAppAuthorizationRoute = '/api/automate/auth/githubapp'
|
||||
|
||||
export const automationFunctionsRoute = '/functions'
|
||||
|
||||
|
||||
@@ -46,7 +46,6 @@ export default defineNuxtConfig({
|
||||
],
|
||||
runtimeConfig: {
|
||||
redisUrl: '',
|
||||
automateGhClientSecret: '',
|
||||
public: {
|
||||
...featureFlags,
|
||||
apiOrigin: 'UNDEFINED',
|
||||
@@ -73,8 +72,7 @@ export default defineNuxtConfig({
|
||||
datadogSite: '',
|
||||
datadogService: '',
|
||||
datadogEnv: '',
|
||||
enableDirectPreviews: true,
|
||||
automateGhClientId: 'Iv1.79a1df48749f11b4'
|
||||
enableDirectPreviews: true
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -384,7 +384,7 @@ export const getFunctions = async (params: {
|
||||
}
|
||||
|
||||
type UserGithubAuthStateResponse = {
|
||||
userHasAuthorizedGithubApp: boolean
|
||||
userHasAuthorizedGitHubApp: boolean
|
||||
}
|
||||
|
||||
export const getUserGithubAuthState = async (params: {
|
||||
|
||||
@@ -613,7 +613,7 @@ export = (FF_AUTOMATE_MODULE_ENABLED
|
||||
let hasAutomateGithubApp = false
|
||||
try {
|
||||
const authState = await getUserGithubAuthState({ userId })
|
||||
hasAutomateGithubApp = authState.userHasAuthorizedGithubApp
|
||||
hasAutomateGithubApp = authState.userHasAuthorizedGitHubApp
|
||||
} catch (e) {
|
||||
if (e instanceof ExecutionEngineFailedResponseError) {
|
||||
if (e.response.statusMessage === 'FunctionCreatorDoesNotExist') {
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
setupAutomationUpdateSubscriptions,
|
||||
setupStatusUpdateSubscriptions
|
||||
} from '@/modules/automate/services/subscriptions'
|
||||
import authGithubAppRest from '@/modules/automate/rest/authGithubApp'
|
||||
|
||||
const { FF_AUTOMATE_MODULE_ENABLED } = Environment.getFeatureFlags()
|
||||
let quitListeners: Optional<() => void> = undefined
|
||||
@@ -95,6 +96,7 @@ const automateModule: SpeckleModule = {
|
||||
|
||||
await initScopes()
|
||||
logStreamRest(app)
|
||||
authGithubAppRest(app)
|
||||
|
||||
if (isInitial) {
|
||||
quitListeners = initializeEventListeners()
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { createStoredAuthCode } from '@/modules/automate/services/executionEngine'
|
||||
import {
|
||||
handleAutomateFunctionCreatorAuthCallback,
|
||||
startAutomateFunctionCreatorAuth
|
||||
} from '@/modules/automate/services/functionManagement'
|
||||
import { getGenericRedis } from '@/modules/core'
|
||||
import { corsMiddleware } from '@/modules/core/configs/cors'
|
||||
import { validateScope, validateServerRole } from '@/modules/shared/authz'
|
||||
import { authMiddlewareCreator } from '@/modules/shared/middleware'
|
||||
import { Roles, Scopes } from '@speckle/shared'
|
||||
import { Application } from 'express'
|
||||
|
||||
export default (app: Application) => {
|
||||
app.get(
|
||||
'/api/automate/auth/githubapp',
|
||||
corsMiddleware(),
|
||||
authMiddlewareCreator([
|
||||
validateServerRole({ requiredRole: Roles.Server.Guest }),
|
||||
validateScope({ requiredScope: Scopes.AutomateFunctions.Write })
|
||||
]),
|
||||
async (req, res) => {
|
||||
const startAuth = startAutomateFunctionCreatorAuth({
|
||||
createStoredAuthCode: createStoredAuthCode({
|
||||
redis: getGenericRedis()
|
||||
})
|
||||
})
|
||||
await startAuth({ req, res })
|
||||
}
|
||||
)
|
||||
|
||||
app.get(
|
||||
'/api/automate/ghAuthComplete',
|
||||
corsMiddleware(),
|
||||
authMiddlewareCreator([
|
||||
validateServerRole({ requiredRole: Roles.Server.Guest }),
|
||||
validateScope({ requiredScope: Scopes.AutomateFunctions.Write })
|
||||
]),
|
||||
async (req, res) => {
|
||||
const handleCallback = handleAutomateFunctionCreatorAuthCallback()
|
||||
await handleCallback({ req, res })
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -6,8 +6,8 @@ export const createStoredAuthCode = (deps: { redis: Redis }) => async () => {
|
||||
const { redis } = deps
|
||||
const codeId = cryptoRandomString({ length: 10 })
|
||||
const authCode = cryptoRandomString({ length: 20 })
|
||||
// prob hashing and salting it would be better, but they expire in 2 mins...
|
||||
await redis.set(codeId, authCode, 'EX', 120)
|
||||
// prob hashing and salting it would be better, but they expire in 5 mins...
|
||||
await redis.set(codeId, authCode, 'EX', 60 * 5)
|
||||
return `${codeId}${authCode}`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable camelcase */
|
||||
import {
|
||||
CreateFunctionBody,
|
||||
createFunction,
|
||||
@@ -30,6 +31,11 @@ import {
|
||||
FunctionReleaseSchemaType,
|
||||
FunctionSchemaType
|
||||
} from '@/modules/automate/helpers/executionEngine'
|
||||
import { Request, Response } from 'express'
|
||||
import { UnauthorizedError } from '@/modules/shared/errors'
|
||||
import { createStoredAuthCode } from '@/modules/automate/services/executionEngine'
|
||||
import { getServerOrigin, speckleAutomateUrl } from '@/modules/shared/helpers/envHelper'
|
||||
import { getFunctionsMarketplaceUrl } from '@/modules/core/helpers/routeHelper'
|
||||
|
||||
const repoUrlToBasicGitRepositoryMetadata = (
|
||||
url: string
|
||||
@@ -167,3 +173,49 @@ export const updateFunction =
|
||||
|
||||
return convertFunctionToGraphQLReturn(apiResult)
|
||||
}
|
||||
|
||||
export type StartAutomateFunctionCreatorAuthDeps = {
|
||||
createStoredAuthCode: ReturnType<typeof createStoredAuthCode>
|
||||
}
|
||||
|
||||
export const startAutomateFunctionCreatorAuth =
|
||||
(deps: StartAutomateFunctionCreatorAuthDeps) =>
|
||||
async (params: { req: Request; res: Response }) => {
|
||||
const { createStoredAuthCode } = deps
|
||||
const { req, res } = params
|
||||
|
||||
const userId = req.context.userId
|
||||
if (!userId) {
|
||||
throw new UnauthorizedError()
|
||||
}
|
||||
|
||||
const authCode = await createStoredAuthCode()
|
||||
const redirectUrl = new URL(
|
||||
'/api/v2/functions/auth/githubapp/authorize',
|
||||
speckleAutomateUrl()
|
||||
)
|
||||
redirectUrl.searchParams.set('speckleUserId', userId)
|
||||
redirectUrl.searchParams.set(
|
||||
'speckleServerOrigin',
|
||||
new URL(getServerOrigin()).origin
|
||||
)
|
||||
redirectUrl.searchParams.set('speckleServerAuthenticationCode', authCode)
|
||||
|
||||
return res.redirect(redirectUrl.toString())
|
||||
}
|
||||
|
||||
export const handleAutomateFunctionCreatorAuthCallback =
|
||||
() => async (params: { req: Request; res: Response }) => {
|
||||
const { req, res } = params
|
||||
const {
|
||||
ghAuth = 'unknown',
|
||||
ghAuthDesc = 'GitHub Authentication unexpectedly failed'
|
||||
} = req.query as Record<string, string>
|
||||
|
||||
const isSuccess = ghAuth === 'success'
|
||||
const redirectUrl = getFunctionsMarketplaceUrl()
|
||||
redirectUrl.searchParams.set('ghAuth', isSuccess ? 'success' : ghAuth)
|
||||
redirectUrl.searchParams.set('ghAuthDesc', isSuccess ? '' : ghAuthDesc)
|
||||
|
||||
return res.redirect(redirectUrl.toString())
|
||||
}
|
||||
|
||||
@@ -49,3 +49,7 @@ export function getStreamCollaboratorsRoute(streamId: string): string {
|
||||
export function buildAbsoluteFrontendUrlFromPath(route: string): string {
|
||||
return new URL(route, getFrontendOrigin()).toString()
|
||||
}
|
||||
|
||||
export function getFunctionsMarketplaceUrl() {
|
||||
return new URL('/functions', getFrontendOrigin())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user