feat(automate): use updated hybrid function search (#4085)
* fix(automate): use new function query for workspace functions * fix(automate): include workspace resource claims in auth flow * chore(automate): do not use btoa
This commit is contained in:
@@ -352,6 +352,13 @@ input AutomateAuthCodePayloadTest {
|
||||
action: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Additional resources to validate user access to.
|
||||
"""
|
||||
input AutomateAuthCodeResources {
|
||||
workspaceId: String
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
automateFunctions(
|
||||
filter: AutomateFunctionsFilter
|
||||
@@ -366,7 +373,10 @@ extend type Query {
|
||||
"""
|
||||
Part of the automation/function creation handshake mechanism
|
||||
"""
|
||||
automateValidateAuthCode(payload: AutomateAuthCodePayloadTest!): Boolean!
|
||||
automateValidateAuthCode(
|
||||
payload: AutomateAuthCodePayloadTest!
|
||||
resources: AutomateAuthCodeResources
|
||||
): Boolean!
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
retry,
|
||||
timeoutAt
|
||||
} from '@speckle/shared'
|
||||
import { has, isObjectLike } from 'lodash'
|
||||
import { has, isObjectLike, isEmpty } from 'lodash'
|
||||
|
||||
export type AuthCodePayloadWithOrigin = AuthCodePayload & { origin: string }
|
||||
|
||||
@@ -48,7 +48,7 @@ export type AutomationCreateResponse = {
|
||||
const getApiUrl = (
|
||||
path?: string,
|
||||
options?: Partial<{
|
||||
query: Record<string, string | number | boolean | undefined>
|
||||
query: Record<string, string[] | string | number | boolean | undefined>
|
||||
}>
|
||||
) => {
|
||||
const automateUrl = speckleAutomateUrl()
|
||||
@@ -62,9 +62,10 @@ const getApiUrl = (
|
||||
const url = new URL(path, automateUrl)
|
||||
if (options?.query) {
|
||||
Object.entries(options.query).forEach(([key, val]) => {
|
||||
if (isNullOrUndefined(val)) return
|
||||
if (isEmpty(val) || isNullOrUndefined(val)) return
|
||||
try {
|
||||
url.searchParams.append(key, val.toString())
|
||||
const urlValue = typeof val === 'object' ? val.join(',') : val.toString()
|
||||
url.searchParams.append(key, urlValue)
|
||||
} catch {
|
||||
console.log({ val })
|
||||
}
|
||||
@@ -460,7 +461,50 @@ export const getFunctionRelease = async (params: {
|
||||
: null
|
||||
}
|
||||
|
||||
export type GetFunctionsParams = {
|
||||
auth?: AuthCodePayload
|
||||
filters: {
|
||||
query?: string
|
||||
cursor?: string
|
||||
limit?: number
|
||||
requireRelease?: boolean
|
||||
includeFeatured?: boolean
|
||||
includeWorkspaces?: string[]
|
||||
includeUsers?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export type GetFunctionsResponse = {
|
||||
items: FunctionSchemaType[]
|
||||
cursor: Nullable<string>
|
||||
totalCount: number
|
||||
}
|
||||
|
||||
export const getFunctions = async (params: GetFunctionsParams) => {
|
||||
const url = getApiUrl(`/api/v2/functions`, {
|
||||
query: {
|
||||
requireRelease: true,
|
||||
...params.filters
|
||||
}
|
||||
})
|
||||
|
||||
const authToken = params.auth
|
||||
? Buffer.from(
|
||||
JSON.stringify({
|
||||
...params.auth,
|
||||
origin: getServerOrigin()
|
||||
})
|
||||
).toString('base64')
|
||||
: undefined
|
||||
|
||||
return await invokeSafeJsonRequest<GetFunctionsResponse>({
|
||||
url,
|
||||
method: 'get',
|
||||
token: authToken
|
||||
})
|
||||
}
|
||||
|
||||
export type GetPublicFunctionsResponse = {
|
||||
totalCount: number
|
||||
cursor: Nullable<string>
|
||||
items: FunctionWithVersionsSchemaType[]
|
||||
@@ -482,7 +526,7 @@ export const getPublicFunctions = async (params: {
|
||||
}
|
||||
})
|
||||
|
||||
return await invokeSafeJsonRequest<GetFunctionsResponse>({
|
||||
return await invokeSafeJsonRequest<GetPublicFunctionsResponse>({
|
||||
url,
|
||||
method: 'get'
|
||||
})
|
||||
|
||||
@@ -736,9 +736,13 @@ export = (FF_AUTOMATE_MODULE_ENABLED
|
||||
emit: getEventBus().emit
|
||||
})
|
||||
const payload = removeNullOrUndefinedKeys(args.payload)
|
||||
const resources = removeNullOrUndefinedKeys(args.resources ?? {})
|
||||
return await validate({
|
||||
...payload,
|
||||
action: args.payload.action as AuthCodePayloadAction
|
||||
payload: {
|
||||
...payload,
|
||||
action: args.payload.action as AuthCodePayloadAction
|
||||
},
|
||||
resources
|
||||
})
|
||||
},
|
||||
async automateFunction(_parent, { id }, ctx) {
|
||||
|
||||
@@ -20,6 +20,8 @@ export type FunctionSchemaType = {
|
||||
speckleUserId: string
|
||||
speckleServerOrigin: string
|
||||
}>
|
||||
functionCreatorSpeckleUserId: Nullable<string>
|
||||
functionCreatorSpeckleServerOrigin: Nullable<string>
|
||||
workspaceIds: string[]
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ export enum AuthCodePayloadAction {
|
||||
export type AuthCodePayload = {
|
||||
code: string
|
||||
userId: string
|
||||
workspaceId?: string
|
||||
action: AuthCodePayloadAction
|
||||
}
|
||||
|
||||
@@ -49,8 +48,14 @@ export const createStoredAuthCodeFactory =
|
||||
|
||||
export const validateStoredAuthCodeFactory =
|
||||
(deps: { redis: Redis; emit: EventBus['emit'] }) =>
|
||||
async (payload: AuthCodePayload) => {
|
||||
async (params: {
|
||||
payload: AuthCodePayload
|
||||
resources?: {
|
||||
workspaceId?: string
|
||||
}
|
||||
}) => {
|
||||
const { redis, emit } = deps
|
||||
const { payload, resources } = params
|
||||
|
||||
const potentialPayloadString = await redis.get(payload.code)
|
||||
const potentialPayload: unknown = potentialPayloadString
|
||||
@@ -67,10 +72,11 @@ export const validateStoredAuthCodeFactory =
|
||||
throw new AutomateAuthCodeHandshakeError('Invalid automate auth payload')
|
||||
}
|
||||
|
||||
if (payload.workspaceId) {
|
||||
// Token is valid, confirm user is authorized to access specified resources.
|
||||
if (resources?.workspaceId) {
|
||||
emit({
|
||||
eventName: 'workspace.authorized',
|
||||
payload: { userId: payload.userId, workspaceId: payload.workspaceId }
|
||||
payload: { userId: payload.userId, workspaceId: resources?.workspaceId }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,14 @@ const cleanFunctionLogo = (logo: MaybeNullOrUndefined<string>): Nullable<string>
|
||||
export const convertFunctionToGraphQLReturn = (
|
||||
fn: FunctionSchemaType
|
||||
): AutomateFunctionGraphQLReturn => {
|
||||
const functionCreator: FunctionSchemaType['functionCreator'] =
|
||||
fn.functionCreatorSpeckleUserId && fn.functionCreatorSpeckleServerOrigin
|
||||
? {
|
||||
speckleUserId: fn.functionCreatorSpeckleUserId,
|
||||
speckleServerOrigin: fn.functionCreatorSpeckleServerOrigin
|
||||
}
|
||||
: fn.functionCreator
|
||||
|
||||
const ret: AutomateFunctionGraphQLReturn = {
|
||||
id: fn.functionId,
|
||||
name: fn.functionName,
|
||||
@@ -98,7 +106,7 @@ export const convertFunctionToGraphQLReturn = (
|
||||
logo: cleanFunctionLogo(fn.logo),
|
||||
tags: fn.tags,
|
||||
supportedSourceApps: fn.supportedSourceApps,
|
||||
functionCreator: fn.functionCreator,
|
||||
functionCreator,
|
||||
workspaceIds: fn.workspaceIds
|
||||
}
|
||||
|
||||
|
||||
@@ -265,6 +265,11 @@ export type AutomateAuthCodePayloadTest = {
|
||||
workspaceId?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
/** Additional resources to validate user access to. */
|
||||
export type AutomateAuthCodeResources = {
|
||||
workspaceId?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type AutomateFunction = {
|
||||
__typename?: 'AutomateFunction';
|
||||
/** Only returned if user is a part of this speckle server */
|
||||
@@ -2747,6 +2752,7 @@ export type QueryAutomateFunctionsArgs = {
|
||||
|
||||
export type QueryAutomateValidateAuthCodeArgs = {
|
||||
payload: AutomateAuthCodePayloadTest;
|
||||
resources?: InputMaybe<AutomateAuthCodeResources>;
|
||||
};
|
||||
|
||||
|
||||
@@ -4938,6 +4944,7 @@ export type ResolversTypes = {
|
||||
ArchiveCommentInput: ArchiveCommentInput;
|
||||
AuthStrategy: ResolverTypeWrapper<AuthStrategy>;
|
||||
AutomateAuthCodePayloadTest: AutomateAuthCodePayloadTest;
|
||||
AutomateAuthCodeResources: AutomateAuthCodeResources;
|
||||
AutomateFunction: ResolverTypeWrapper<AutomateFunctionGraphQLReturn>;
|
||||
AutomateFunctionCollection: ResolverTypeWrapper<Omit<AutomateFunctionCollection, 'items'> & { items: Array<ResolversTypes['AutomateFunction']> }>;
|
||||
AutomateFunctionRelease: ResolverTypeWrapper<AutomateFunctionReleaseGraphQLReturn>;
|
||||
@@ -5251,6 +5258,7 @@ export type ResolversParentTypes = {
|
||||
ArchiveCommentInput: ArchiveCommentInput;
|
||||
AuthStrategy: AuthStrategy;
|
||||
AutomateAuthCodePayloadTest: AutomateAuthCodePayloadTest;
|
||||
AutomateAuthCodeResources: AutomateAuthCodeResources;
|
||||
AutomateFunction: AutomateFunctionGraphQLReturn;
|
||||
AutomateFunctionCollection: Omit<AutomateFunctionCollection, 'items'> & { items: Array<ResolversParentTypes['AutomateFunction']> };
|
||||
AutomateFunctionRelease: AutomateFunctionReleaseGraphQLReturn;
|
||||
|
||||
@@ -246,6 +246,11 @@ export type AutomateAuthCodePayloadTest = {
|
||||
workspaceId?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
/** Additional resources to validate user access to. */
|
||||
export type AutomateAuthCodeResources = {
|
||||
workspaceId?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type AutomateFunction = {
|
||||
__typename?: 'AutomateFunction';
|
||||
/** Only returned if user is a part of this speckle server */
|
||||
@@ -2728,6 +2733,7 @@ export type QueryAutomateFunctionsArgs = {
|
||||
|
||||
export type QueryAutomateValidateAuthCodeArgs = {
|
||||
payload: AutomateAuthCodePayloadTest;
|
||||
resources?: InputMaybe<AutomateAuthCodeResources>;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
import { createProjectInviteFactory } from '@/modules/serverinvites/services/projectInviteManagement'
|
||||
import { getInvitationTargetUsersFactory } from '@/modules/serverinvites/services/retrieval'
|
||||
import { authorizeResolver } from '@/modules/shared'
|
||||
import { getFeatureFlags, getServerOrigin } from '@/modules/shared/helpers/envHelper'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { WorkspaceInviteResourceType } from '@/modules/workspacesCore/domain/constants'
|
||||
import {
|
||||
@@ -169,17 +169,12 @@ import {
|
||||
listWorkspaceSsoMembershipsFactory
|
||||
} from '@/modules/workspaces/repositories/sso'
|
||||
import { getDecryptor } from '@/modules/workspaces/helpers/sso'
|
||||
import { getWorkspaceFunctions } from '@/modules/automate/clients/executionEngine'
|
||||
import { getFunctions } from '@/modules/automate/clients/executionEngine'
|
||||
import {
|
||||
ExecutionEngineFailedResponseError,
|
||||
ExecutionEngineNetworkError
|
||||
} from '@/modules/automate/errors/executionEngine'
|
||||
import { getDefaultRegionFactory } from '@/modules/workspaces/repositories/regions'
|
||||
import {
|
||||
AuthCodePayloadAction,
|
||||
createStoredAuthCodeFactory
|
||||
} from '@/modules/automate/services/authCode'
|
||||
import { getGenericRedis } from '@/modules/shared/redis/redis'
|
||||
import { convertFunctionToGraphQLReturn } from '@/modules/automate/services/functionManagement'
|
||||
import {
|
||||
getWorkspacePlanFactory,
|
||||
@@ -202,6 +197,11 @@ import { OperationTypeNode } from 'graphql'
|
||||
import { updateWorkspacePlanFactory } from '@/modules/gatekeeper/services/workspacePlans'
|
||||
import { GetWorkspaceCollaboratorsArgs } from '@/modules/workspaces/domain/operations'
|
||||
import { WorkspaceTeamMember } from '@/modules/workspaces/domain/types'
|
||||
import { getGenericRedis } from '@/modules/shared/redis/redis'
|
||||
import {
|
||||
AuthCodePayloadAction,
|
||||
createStoredAuthCodeFactory
|
||||
} from '@/modules/automate/services/authCode'
|
||||
|
||||
const eventBus = getEventBus()
|
||||
const getServerInfo = getServerInfoFactory({ db })
|
||||
@@ -1079,6 +1079,13 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
},
|
||||
automateFunctions: async (parent, args, context) => {
|
||||
try {
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
parent.id,
|
||||
Roles.Workspace.Member,
|
||||
context.resourceAccessRules
|
||||
)
|
||||
|
||||
const authCode = await createStoredAuthCodeFactory({
|
||||
redis: getGenericRedis()
|
||||
})({
|
||||
@@ -1086,14 +1093,16 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
action: AuthCodePayloadAction.ListWorkspaceFunctions
|
||||
})
|
||||
|
||||
const res = await getWorkspaceFunctions({
|
||||
workspaceId: parent.id,
|
||||
query: removeNullOrUndefinedKeys(args),
|
||||
body: {
|
||||
speckleServerAuthenticationPayload: {
|
||||
...authCode,
|
||||
origin: getServerOrigin()
|
||||
}
|
||||
const res = await getFunctions({
|
||||
auth: authCode,
|
||||
filters: {
|
||||
query: args.filter?.search ?? undefined,
|
||||
cursor: args.cursor ?? undefined,
|
||||
limit: args.limit,
|
||||
requireRelease: true,
|
||||
includeFeatured: true,
|
||||
includeWorkspaces: [parent.id],
|
||||
includeUsers: []
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1105,12 +1114,12 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
}
|
||||
}
|
||||
|
||||
const items = res.functions.map(convertFunctionToGraphQLReturn)
|
||||
const items = res.items.map(convertFunctionToGraphQLReturn)
|
||||
|
||||
return {
|
||||
cursor: undefined,
|
||||
totalCount: res.functions.length,
|
||||
items
|
||||
items,
|
||||
cursor: res.cursor,
|
||||
totalCount: res.totalCount
|
||||
}
|
||||
} catch (e) {
|
||||
const isNotFound =
|
||||
|
||||
@@ -247,6 +247,11 @@ export type AutomateAuthCodePayloadTest = {
|
||||
workspaceId?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
/** Additional resources to validate user access to. */
|
||||
export type AutomateAuthCodeResources = {
|
||||
workspaceId?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type AutomateFunction = {
|
||||
__typename?: 'AutomateFunction';
|
||||
/** Only returned if user is a part of this speckle server */
|
||||
@@ -2729,6 +2734,7 @@ export type QueryAutomateFunctionsArgs = {
|
||||
|
||||
export type QueryAutomateValidateAuthCodeArgs = {
|
||||
payload: AutomateAuthCodePayloadTest;
|
||||
resources?: InputMaybe<AutomateAuthCodeResources>;
|
||||
};
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user