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:
Kristaps Fabians Geikins
2024-06-03 17:24:28 +03:00
committed by GitHub
parent 8f2974f8d4
commit 95aa58958f
11 changed files with 113 additions and 15 deletions
-2
View File
@@ -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'
+1 -3
View File
@@ -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())
}