diff --git a/packages/global/support/permission/collaborator.d.ts b/packages/global/support/permission/collaborator.d.ts index 150864c5d3f3..6e927c8122e3 100644 --- a/packages/global/support/permission/collaborator.d.ts +++ b/packages/global/support/permission/collaborator.d.ts @@ -1,31 +1,33 @@ +import type { UpdateAppCollaboratorBody } from 'core/app/collaborator'; import type { RequireOnlyOne } from '../../common/type/utils'; import { RequireAtLeastOne } from '../../common/type/utils'; import type { Permission } from './controller'; -import type { PermissionValueType } from './type'; +import type { PermissionValueType, RoleValueType } from './type'; -export type CollaboratorItemType = { - teamId: string; - permission: Permission; - name: string; - avatar: string; -} & RequireOnlyOne<{ +export type CollaboratorIdType = RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string; }>; -export type UpdateClbPermissionProps = { - members?: string[]; - groups?: string[]; - orgs?: string[]; -} & (addOnly extends true - ? {} - : { - permission: PermissionValueType; - }); +export type CollaboratorItemDetailType = { + teamId: string; + permission: Permission; + name: string; + avatar: string; +} & CollaboratorIdType; + +export type CollaboratorItemType = { + permission: PermissionValueType; +} & CollaboratorIdType; -export type DeletePermissionQuery = RequireOnlyOne<{ - tmbId?: string; - groupId?: string; - orgId?: string; -}>; +export type UpdateClbPermissionProps = { + collaborators: CollaboratorItemType[]; +}; + +export type DeletePermissionQuery = CollaboratorIdType; + +export type CollaboratorListType = { + clbs: CollaboratorItemDetailType[]; + parentClbs?: CollaboratorItemDetailType[]; +}; diff --git a/packages/global/support/permission/type.d.ts b/packages/global/support/permission/type.ts similarity index 84% rename from packages/global/support/permission/type.d.ts rename to packages/global/support/permission/type.ts index db0a085e635c..5b77958d9a65 100644 --- a/packages/global/support/permission/type.d.ts +++ b/packages/global/support/permission/type.ts @@ -1,10 +1,8 @@ import type { UserModelSchema } from '../user/type'; import type { RequireOnlyOne } from '../../common/type/utils'; import type { TeamMemberSchema } from '../user/team/type'; -import { MemberGroupSchemaType } from './memberGroup/type'; -import type { TeamMemberWithUserSchema } from '../user/team/type'; -import type { CommonPerKeyEnum, CommonRoleKeyEnum } from './constant'; -import { AuthUserTypeEnum, type CommonPerKeyEnum, type PerResourceTypeEnum } from './constant'; +import type { CommonRoleKeyEnum } from './constant'; +import { type CommonPerKeyEnum, type PerResourceTypeEnum } from './constant'; // PermissionValueType, the type of permission's value is a number, which is a bit field actually. // It is spired by the permission system in Linux. @@ -18,7 +16,7 @@ export type ResourceType = `${PerResourceTypeEnum}`; /** * Define the roles. Each role is a binary number, only one bit is set to 1. */ -export type RoleListType = Readonly< +export type RoleListType = Readonly< Record< T | CommonRoleKeyEnum, Readonly<{ @@ -43,7 +41,7 @@ export type RoleListType = Readonly< * write: 0b110, // bad, should be 0b010 * } */ -export type PermissionListType = Readonly< +export type PermissionListType = Readonly< Record >; diff --git a/packages/global/support/permission/utils.ts b/packages/global/support/permission/utils.ts index b39900f0630a..3e2012f98192 100644 --- a/packages/global/support/permission/utils.ts +++ b/packages/global/support/permission/utils.ts @@ -1,48 +1,12 @@ +import type { CollaboratorIdType, CollaboratorItemType } from './collaborator'; +import type { RoleValueType} from './type'; import { type PermissionValueType } from './type'; -import { NullRoleVal, PermissionTypeEnum } from './constant'; -import type { Permission } from './controller'; - -/* team public source, or owner source in team */ -export function mongoRPermission({ - teamId, - tmbId, - permission -}: { - teamId: string; - tmbId: string; - permission: Permission; -}) { - if (permission.isOwner) { - return { - teamId - }; - } - return { - teamId, - $or: [{ permission: PermissionTypeEnum.public }, { tmbId }] - }; -} -export function mongoOwnerPermission({ teamId, tmbId }: { teamId: string; tmbId: string }) { - return { - teamId, - tmbId - }; -} - -// return permission-related schema to define the schema of resources -export function getPermissionSchema(defaultPermission: PermissionValueType = NullRoleVal) { - return { - defaultPermission: { - type: Number, - default: defaultPermission - }, - inheritPermission: { - type: Boolean, - default: true - } - }; -} - +/** + * Sum the permission value. + * If no permission value is provided, return undefined to fallback to default value. + * @param per permission value (number) + * @returns sum of permission value + */ export const sumPer = (...per: PermissionValueType[]) => { if (per.length === 0) { // prevent sum 0 value, to fallback to default value @@ -50,3 +14,165 @@ export const sumPer = (...per: PermissionValueType[]) => { } return per.reduce((acc, cur) => acc | cur, 0); }; + +/** + * Check if the update cause conflict (need to remove inheritance permission). + * Conflict condition: + * The updated collaborator is a parent collaborator. + * @param parentClbs parent collaborators + * @param oldChildClbs old child collaborators + * @param newChildClbs new child collaborators + */ +export const checkRoleUpdateConflict = ({ + parentClbs, + oldRealClbs, + newChildClbs +}: { + parentClbs: CollaboratorItemType[]; + oldRealClbs: CollaboratorItemType[]; + newChildClbs: CollaboratorItemType[]; +}): boolean => { + if (parentClbs.length === 0) { + return false; + } + + // Use a Map for faster lookup by teamId + const parentClbRoleMap = new Map(parentClbs.map((clb) => [getCollaboratorId(clb), clb])); + + const changedClbs = getChangedCollaborators({ + newRealClbs: newChildClbs, + oldRealClbs: oldRealClbs + }); + + for (const changedClb of changedClbs) { + const parent = parentClbRoleMap.get(getCollaboratorId(changedClb)); + if (parent && (changedClb.changedRole & parent.permission) !== 0) { + return true; + } + } + + return false; +}; + +export type ChangedClbType = { + changedRole: RoleValueType; + deleted: boolean; +} & CollaboratorIdType; + +/** + * Get changed collaborators. + * return empty array if all collaborators are unchanged. + * + * for each return item: + * ```typescript + * { + * // ... ids + * changedRole: number; // set bit means the role is changed + * deleted: boolean; // is deleted + * } + * ``` + * + * **special**: for low 3 bit: always get the lowest change, unset the higher change. + */ +export const getChangedCollaborators = ({ + oldRealClbs, + newRealClbs +}: { + oldRealClbs: CollaboratorItemType[]; + newRealClbs: CollaboratorItemType[]; +}): ChangedClbType[] => { + if (oldRealClbs.length === 0) { + return newRealClbs.map((clb) => ({ + ...clb, + changedRole: clb.permission, + deleted: false + })); + } + const oldClbsMap = new Map(oldRealClbs.map((clb) => [getCollaboratorId(clb), clb])); + const changedClbs: ChangedClbType[] = []; + for (const newClb of newRealClbs) { + const oldClb = oldClbsMap.get(getCollaboratorId(newClb)); + if (!oldClb) { + changedClbs.push({ + ...newClb, + changedRole: newClb.permission, + deleted: false + }); + continue; + } + const changedRole = oldClb.permission ^ newClb.permission; + if (changedRole) { + changedClbs.push({ + ...newClb, + changedRole, + deleted: false + }); + } + } + + for (const oldClb of oldRealClbs) { + const newClb = newRealClbs.find((clb) => getCollaboratorId(clb) === getCollaboratorId(oldClb)); + if (!newClb) { + changedClbs.push({ + ...oldClb, + changedRole: oldClb.permission, + deleted: true + }); + } + } + + changedClbs.forEach((clb) => { + // For the lowest 3 bits, only keep the lowest set bit as 1, clear other lower bits, keep higher bits unchanged + const low3 = clb.changedRole & 0b111; + const lowestBit = low3 & -low3; + clb.changedRole = (clb.changedRole & ~0b111) | lowestBit; + }); + + return changedClbs; +}; + +export const getCollaboratorId = (clb: CollaboratorIdType) => + (clb.tmbId || clb.groupId || clb.orgId)!; + +/** + * merge collaboratorLists into one list + * the intersection of the lists will calculate the sumPer. + */ +export const mergeCollaboratorList = (...clbLists: T[][]): T[] => { + const merge = (list1: T[], list2: T[]): T[] => { + const idToClb = new Map(); + + // Add all items from list1 + for (const clb of list1) { + idToClb.set(getCollaboratorId(clb), { ...clb }); + } + + // Merge permissions from list2 + for (const clb2 of list2) { + const id = getCollaboratorId(clb2); + if (idToClb.has(id)) { + // If already exists, merge permission bits + const original = idToClb.get(id)!; + idToClb.set(id, { + ...original, + permission: sumPer(original.permission, clb2.permission)! + }); + } else { + idToClb.set(id, { ...clb2 }); + } + } + + return Array.from(idToClb.values()); + }; + if (clbLists.length === 0) { + return []; + } + if (clbLists.length === 1) { + return clbLists[0]; + } + if (clbLists.length === 2) { + const [list1, list2] = clbLists; + return merge(list1, list2); + } + return mergeCollaboratorList(merge(clbLists[0], clbLists[1]), ...clbLists.slice(2)); +}; diff --git a/packages/service/common/mongo/index.ts b/packages/service/common/mongo/index.ts index 0abb1e615f36..5666d6057464 100644 --- a/packages/service/common/mongo/index.ts +++ b/packages/service/common/mongo/index.ts @@ -64,6 +64,33 @@ const addCommonMiddleware = (schema: mongoose.Schema) => { } next(); }); + + // Convert _id to string + schema.post(/^find/, function (docs) { + if (!docs) return; + + const convertObjectIds = (obj: any) => { + if (!obj) return; + + // Convert _id + if (obj._id && obj._id.toString) { + obj._id = obj._id.toString(); + } + + // Convert other ObjectId fields + Object.keys(obj).forEach((key) => { + if (obj[key] && obj[key]._bsontype === 'ObjectId') { + obj[key] = obj[key].toString(); + } + }); + }; + + if (Array.isArray(docs)) { + docs.forEach((doc) => convertObjectIds(doc)); + } else { + convertObjectIds(docs); + } + }); }); return schema; diff --git a/packages/service/common/response/index.ts b/packages/service/common/response/index.ts index 5d2f58db6ea8..350a028472eb 100644 --- a/packages/service/common/response/index.ts +++ b/packages/service/common/response/index.ts @@ -2,9 +2,9 @@ import type { NextApiResponse } from 'next'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { proxyError, ERROR_RESPONSE, ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; import { addLog } from '../system/log'; -import { clearCookie } from '../../support/permission/controller'; import { replaceSensitiveText } from '@fastgpt/global/common/string/tools'; import { UserError } from '@fastgpt/global/common/error/utils'; +import { clearCookie } from '../../support/permission/auth/common'; export interface ResponseType { code: number; diff --git a/packages/service/support/permission/app/auth.ts b/packages/service/support/permission/app/auth.ts index 0accefdc7d7d..9f45bd483cc0 100644 --- a/packages/service/support/permission/app/auth.ts +++ b/packages/service/support/permission/app/auth.ts @@ -1,7 +1,6 @@ /* Auth app permission */ import { MongoApp } from '../../../core/app/schema'; import { type AppDetailType } from '@fastgpt/global/core/app/type.d'; -import { parseHeaderCert } from '../controller'; import { PerResourceTypeEnum, ReadPermissionVal, @@ -9,7 +8,7 @@ import { } from '@fastgpt/global/support/permission/constant'; import { AppErrEnum } from '@fastgpt/global/common/error/code/app'; import { getTmbInfoByTmbId } from '../../user/team/controller'; -import { getResourcePermission } from '../controller'; +import { getTmbPermission } from '../controller'; import { AppPermission } from '@fastgpt/global/support/permission/app/controller'; import { type PermissionValueType } from '@fastgpt/global/support/permission/type'; import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants'; @@ -18,6 +17,7 @@ import { PluginSourceEnum } from '@fastgpt/global/core/app/plugin/constants'; import { type AuthModeType, type AuthResponseType } from '../type'; import { splitCombinePluginId } from '@fastgpt/global/core/app/plugin/utils'; import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/constant'; +import { parseHeaderCert } from '../auth/common'; export const authPluginByTmbId = async ({ tmbId, @@ -90,53 +90,18 @@ export const authAppByTmbId = async ({ const isOwner = tmbPer.isOwner || String(app.tmbId) === String(tmbId); - const { Per } = await (async () => { - if (isOwner) { - return { - Per: new AppPermission({ isOwner: true }) - }; - } - - if ( - AppFolderTypeList.includes(app.type) || - app.inheritPermission === false || - !app.parentId - ) { - // 1. is a folder. (Folders have completely permission) - // 2. inheritPermission is false. - // 3. is root folder/app. - const role = await getResourcePermission({ - teamId, - tmbId, - resourceId: appId, - resourceType: PerResourceTypeEnum.app - }); - const Per = new AppPermission({ role, isOwner }); - - if (app.favourite || app.quick) { - Per.addRole(ReadRoleVal); - } - - return { - Per - }; - } else { - // is not folder and inheritPermission is true and is not root folder. - const { app: parent } = await authAppByTmbId({ - tmbId, - appId: app.parentId, - per - }); - - const Per = new AppPermission({ - role: parent.permission.role, - isOwner - }); - return { - Per - }; - } - })(); + const isGetParentClb = + app.inheritPermission && !AppFolderTypeList.includes(app.type) && !!app.parentId; + + const Per = new AppPermission({ + role: await getTmbPermission({ + teamId, + tmbId, + resourceId: isGetParentClb ? app.parentId! : app._id, + resourceType: PerResourceTypeEnum.app + }), + isOwner + }); if (!Per.checkPer(per)) { return Promise.reject(AppErrEnum.unAuthApp); diff --git a/packages/service/support/permission/auth/common.ts b/packages/service/support/permission/auth/common.ts index 31b0b13cdb20..23f40362a406 100644 --- a/packages/service/support/permission/auth/common.ts +++ b/packages/service/support/permission/auth/common.ts @@ -1,7 +1,13 @@ -import { parseHeaderCert } from '../controller'; +import type { ReqHeaderAuthType } from '../type'; import { type AuthModeType } from '../type'; import { SERVICE_LOCAL_HOST } from '../../../common/system/tools'; import { type ApiRequestProps } from '../../../type/next'; +import type { NextApiResponse } from 'next'; +import Cookie from 'cookie'; +import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; +import { authUserSession } from '../../../support/user/session'; +import { authOpenApiKey } from '../../../support/openapi/auth'; +import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant'; export const authCert = async (props: AuthModeType) => { const result = await parseHeaderCert(props); @@ -19,3 +25,149 @@ export const authRequestFromLocal = ({ req }: { req: ApiRequestProps }) => { return Promise.reject('Invalid request'); } }; + +export async function parseHeaderCert({ + req, + authToken = false, + authRoot = false, + authApiKey = false +}: AuthModeType) { + // parse jwt + async function authCookieToken(cookie?: string, token?: string) { + // 获取 cookie + const cookies = Cookie.parse(cookie || ''); + const cookieToken = token || cookies[TokenName]; + + if (!cookieToken) { + return Promise.reject(ERROR_ENUM.unAuthorization); + } + + return { ...(await authUserSession(cookieToken)), sessionId: cookieToken }; + } + // from authorization get apikey + async function parseAuthorization(authorization?: string) { + if (!authorization) { + return Promise.reject(ERROR_ENUM.unAuthorization); + } + + // Bearer fastgpt-xxxx-appId + const auth = authorization.split(' ')[1]; + if (!auth) { + return Promise.reject(ERROR_ENUM.unAuthorization); + } + + const { apikey, appId: authorizationAppid = '' } = await (async () => { + const arr = auth.split('-'); + // abandon + if (arr.length === 3) { + return { + apikey: `${arr[0]}-${arr[1]}`, + appId: arr[2] + }; + } + if (arr.length === 2) { + return { + apikey: auth + }; + } + return Promise.reject(ERROR_ENUM.unAuthorization); + })(); + + // auth apikey + const { teamId, tmbId, appId: apiKeyAppId = '', sourceName } = await authOpenApiKey({ apikey }); + + return { + uid: '', + teamId, + tmbId, + apikey, + appId: apiKeyAppId || authorizationAppid, + sourceName + }; + } + // root user + async function parseRootKey(rootKey?: string) { + if (!rootKey || !process.env.ROOT_KEY || rootKey !== process.env.ROOT_KEY) { + return Promise.reject(ERROR_ENUM.unAuthorization); + } + } + + const { cookie, token, rootkey, authorization } = (req.headers || {}) as ReqHeaderAuthType; + + const { uid, teamId, tmbId, appId, openApiKey, authType, isRoot, sourceName, sessionId } = + await (async () => { + if (authApiKey && authorization) { + // apikey from authorization + const authResponse = await parseAuthorization(authorization); + return { + uid: authResponse.uid, + teamId: authResponse.teamId, + tmbId: authResponse.tmbId, + appId: authResponse.appId, + openApiKey: authResponse.apikey, + authType: AuthUserTypeEnum.apikey, + sourceName: authResponse.sourceName + }; + } + if (authToken && (token || cookie)) { + // user token(from fastgpt web) + const res = await authCookieToken(cookie, token); + + return { + uid: res.userId, + teamId: res.teamId, + tmbId: res.tmbId, + appId: '', + openApiKey: '', + authType: AuthUserTypeEnum.token, + isRoot: res.isRoot, + sessionId: res.sessionId + }; + } + if (authRoot && rootkey) { + await parseRootKey(rootkey); + // root user + return { + uid: '', + teamId: '', + tmbId: '', + appId: '', + openApiKey: '', + authType: AuthUserTypeEnum.root, + isRoot: true + }; + } + + return Promise.reject(ERROR_ENUM.unAuthorization); + })(); + + if (!authRoot && (!teamId || !tmbId)) { + return Promise.reject(ERROR_ENUM.unAuthorization); + } + + return { + userId: String(uid), + teamId: String(teamId), + tmbId: String(tmbId), + appId, + authType, + sourceName, + apikey: openApiKey, + isRoot: !!isRoot, + sessionId + }; +} + +/* set cookie */ +export const TokenName = 'fastgpt_token'; +export const setCookie = (res: NextApiResponse, token: string) => { + res.setHeader( + 'Set-Cookie', + `${TokenName}=${token}; Path=/; HttpOnly; Max-Age=604800; Samesite=Strict;` + ); +}; + +/* clear cookie */ +export const clearCookie = (res: NextApiResponse) => { + res.setHeader('Set-Cookie', `${TokenName}=; Path=/; Max-Age=0`); +}; diff --git a/packages/service/support/permission/auth/file.ts b/packages/service/support/permission/auth/file.ts index 6ea324d04466..bc4d868072a4 100644 --- a/packages/service/support/permission/auth/file.ts +++ b/packages/service/support/permission/auth/file.ts @@ -1,11 +1,15 @@ import { type AuthModeType, type AuthResponseType } from '../type'; import { type DatasetFileSchema } from '@fastgpt/global/core/dataset/type'; -import { parseHeaderCert } from '../controller'; import { getFileById } from '../../../common/file/gridfs/controller'; -import { BucketNameEnum } from '@fastgpt/global/common/file/constants'; +import { BucketNameEnum, bucketNameMap } from '@fastgpt/global/common/file/constants'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { OwnerPermissionVal, ReadRoleVal } from '@fastgpt/global/support/permission/constant'; import { Permission } from '@fastgpt/global/support/permission/controller'; +import type { FileTokenQuery } from '@fastgpt/global/common/file/type'; +import { addMinutes } from 'date-fns'; +import { parseHeaderCert } from './common'; +import jwt from 'jsonwebtoken'; +import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; export const authCollectionFile = async ({ fileId, @@ -46,3 +50,45 @@ export const authCollectionFile = async ({ file }; }; + +/* file permission */ +export const createFileToken = (data: FileTokenQuery) => { + if (!process.env.FILE_TOKEN_KEY) { + return Promise.reject('System unset FILE_TOKEN_KEY'); + } + + const expireMinutes = + data.customExpireMinutes ?? bucketNameMap[data.bucketName].previewExpireMinutes; + const expiredTime = Math.floor(addMinutes(new Date(), expireMinutes).getTime() / 1000); + + const key = (process.env.FILE_TOKEN_KEY as string) ?? 'filetoken'; + const token = jwt.sign( + { + ...data, + exp: expiredTime + }, + key + ); + return Promise.resolve(token); +}; + +export const authFileToken = (token?: string) => + new Promise((resolve, reject) => { + if (!token) { + return reject(ERROR_ENUM.unAuthFile); + } + const key = (process.env.FILE_TOKEN_KEY as string) ?? 'filetoken'; + + jwt.verify(token, key, (err, decoded: any) => { + if (err || !decoded.bucketName || !decoded?.teamId || !decoded?.fileId) { + reject(ERROR_ENUM.unAuthFile); + return; + } + resolve({ + bucketName: decoded.bucketName, + teamId: decoded.teamId, + uid: decoded.uid, + fileId: decoded.fileId + }); + }); + }); diff --git a/packages/service/support/permission/auth/openapi.ts b/packages/service/support/permission/auth/openapi.ts index 206eff13d8b7..a2a404fce4a5 100644 --- a/packages/service/support/permission/auth/openapi.ts +++ b/packages/service/support/permission/auth/openapi.ts @@ -1,11 +1,11 @@ import { type AuthModeType, type AuthResponseType } from '../type'; import { type OpenApiSchema } from '@fastgpt/global/support/openapi/type'; -import { parseHeaderCert } from '../controller'; import { getTmbInfoByTmbId } from '../../user/team/controller'; import { MongoOpenApi } from '../../openapi/schema'; import { OpenApiErrEnum } from '@fastgpt/global/common/error/code/openapi'; import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant'; import { authAppByTmbId } from '../app/auth'; +import { parseHeaderCert } from './common'; export async function authOpenApiKeyCrud({ id, diff --git a/packages/service/support/permission/controller.ts b/packages/service/support/permission/controller.ts index 96279c472630..880a3b0119c3 100644 --- a/packages/service/support/permission/controller.ts +++ b/packages/service/support/permission/controller.ts @@ -1,27 +1,28 @@ -import Cookie from 'cookie'; -import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; -import jwt from 'jsonwebtoken'; -import { type NextApiResponse, type NextApiRequest } from 'next'; -import type { AuthModeType, ReqHeaderAuthType } from './type.d'; +import type { ClientSession, AnyBulkWriteOperation } from '../../common/mongo'; import type { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; -import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant'; -import { authOpenApiKey } from '../openapi/auth'; -import { type FileTokenQuery } from '@fastgpt/global/common/file/type'; +import { ManageRoleVal, OwnerRoleVal } from '@fastgpt/global/support/permission/constant'; import { MongoResourcePermission } from './schema'; -import { type ClientSession } from 'mongoose'; +import type { ResourcePermissionType, ResourceType } from '@fastgpt/global/support/permission/type'; import { type PermissionValueType } from '@fastgpt/global/support/permission/type'; -import { bucketNameMap } from '@fastgpt/global/common/file/constants'; -import { addMinutes } from 'date-fns'; import { getGroupsByTmbId } from './memberGroup/controllers'; import { Permission } from '@fastgpt/global/support/permission/controller'; import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; -import { type MemberGroupSchemaType } from '@fastgpt/global/support/permission/memberGroup/type'; -import { type TeamMemberSchema } from '@fastgpt/global/support/user/team/type'; -import { type OrgSchemaType } from '@fastgpt/global/support/user/team/org/type'; import { getOrgIdSetWithParentByTmbId } from './org/controllers'; -import { authUserSession } from '../user/session'; -import { sumPer } from '@fastgpt/global/support/permission/utils'; +import { + getCollaboratorId, + mergeCollaboratorList, + sumPer +} from '@fastgpt/global/support/permission/utils'; +import { type SyncChildrenPermissionResourceType } from './inheritPermission'; +import { pickCollaboratorIdFields } from './utils'; +import type { + CollaboratorItemDetailType, + CollaboratorItemType +} from '@fastgpt/global/support/permission/collaborator'; +import { MongoTeamMember } from '../../support/user/team/teamMemberSchema'; +import { MongoOrgModel } from './org/orgSchema'; +import { MongoMemberGroupModel } from './memberGroup/memberGroupSchema'; /** get resource permission for a team member * If there is no permission for the team member, it will return undefined @@ -31,7 +32,7 @@ import { sumPer } from '@fastgpt/global/support/permission/utils'; * @param resourceId * @returns PermissionValueType | undefined */ -export const getResourcePermission = async ({ +export const getTmbPermission = async ({ resourceType, teamId, tmbId, @@ -106,17 +107,27 @@ export const getResourcePermission = async ({ return sumPer(...groupPers, ...orgPers); }; -export async function getResourceClbsAndGroups({ - resourceId, +/** + * Only get resource's owned clbs, not including parents'. + */ +export async function getResourceOwnedClbs({ resourceType, teamId, + resourceId, session }: { - resourceId: ParentIdType; - resourceType: Omit<`${PerResourceTypeEnum}`, 'team'>; teamId: string; - session: ClientSession; -}) { + session?: ClientSession; +} & ( + | { + resourceType: 'team'; + resourceId?: undefined; + } + | { + resourceType: Omit; + resourceId: ParentIdType; + } +)) { return MongoResourcePermission.find( { resourceId, @@ -124,16 +135,109 @@ export async function getResourceClbsAndGroups({ teamId }, undefined, - { session } + { ...(session ? { session } : {}) } ).lean(); } -export const getClbsAndGroupsWithInfo = async ({ +export const getClbsInfo = async ({ + clbs, + teamId, + ownerTmbId +}: { + clbs: CollaboratorItemType[]; + teamId: string; + ownerTmbId?: string; +}): Promise => { + const tmbIds = []; + const orgIds = []; + const groupIds = []; + + for (const clb of clbs) { + if (clb.tmbId) tmbIds.push(clb.tmbId); + if (clb.orgId) orgIds.push(clb.orgId); + if (clb.groupId) groupIds.push(clb.groupId); + } + + const infos = ( + await Promise.all([ + MongoTeamMember.find({ _id: { $in: tmbIds }, teamId }, '_id name avatar').lean(), + MongoOrgModel.find({ _id: { $in: orgIds }, teamId }, '_id name avatar').lean(), + MongoMemberGroupModel.find({ _id: { $in: groupIds }, teamId }, '_id name avatar').lean() + ]) + ).flat(); + + return clbs.map((clb) => { + const info = infos.find((info) => info._id === getCollaboratorId(clb))!; + return { + ...clb, + teamId, + permission: new Permission({ role: clb.permission, isOwner: ownerTmbId === clb.tmbId }), + name: info.name, + avatar: info.avatar + }; + }); +}; + +/** + * Get Resource's Collaborators (owned and parent's if it is inherit) + */ +export const getResourceClbs = async ({ + resourceType, + teamId, + resourceId, + session, + inherit, + isFolder, + parentId +}: { + teamId: string; + session?: ClientSession; + parentId: ParentIdType; + inherit?: boolean; + isFolder?: boolean; +} & ( + | { + resourceId: ParentIdType; + resourceType: Omit<`${PerResourceTypeEnum}`, 'team'>; + } + | { + resourceId: ParentIdType; + resourceType: 'team'; + } +)) => { + const ownedClbs = await getResourceOwnedClbs({ + resourceType, + resourceId, + teamId, + session + }); + if (inherit === false || isFolder || !parentId || resourceType === 'team') { + return ownedClbs; + } + // otherwise, we need the parent's clbs + const parentClbs = await getResourceOwnedClbs({ + resourceType, + resourceId, + teamId + }); + + return mergeCollaboratorList(parentClbs, ownedClbs); +}; + +export const getClbsWithInfo = async ({ resourceId, resourceType, - teamId + teamId, + tmbId, + parentId, + isFolder, + inherit }: { teamId: string; + tmbId?: string; + parentId?: string; + isFolder?: boolean; + inherit?: boolean; } & ( | { resourceId: ParentIdType; @@ -143,42 +247,26 @@ export const getClbsAndGroupsWithInfo = async ({ resourceType: 'team'; resourceId?: undefined; } -)) => - Promise.all([ - MongoResourcePermission.find({ - teamId, - resourceId, - resourceType, - tmbId: { - $exists: true - } - }) - .populate<{ tmb: TeamMemberSchema }>({ - path: 'tmb', - select: 'name userId avatar' - }) - .lean(), - MongoResourcePermission.find({ - teamId, - resourceId, - resourceType, - groupId: { - $exists: true - } - }) - .populate<{ group: MemberGroupSchemaType }>('group', 'name avatar') - .lean(), - MongoResourcePermission.find({ - teamId, - resourceId, - resourceType, - orgId: { - $exists: true - } - }) - .populate<{ org: OrgSchemaType }>({ path: 'org', select: 'name avatar' }) - .lean() - ]); +)) => { + if (!resourceId && resourceType !== 'team') { + return []; + } + + const clbs = await getResourceClbs({ + resourceType, + resourceId, + teamId, + inherit, + isFolder, + parentId + }); + + return getClbsInfo({ + clbs, + ownerTmbId: tmbId, + teamId + }); +}; export const delResourcePermissionById = (id: string) => { return MongoResourcePermission.findByIdAndDelete(id); @@ -214,192 +302,63 @@ export const delResourcePermission = ({ ); }; -/* 下面代码等迁移 */ - -export async function parseHeaderCert({ - req, - authToken = false, - authRoot = false, - authApiKey = false -}: AuthModeType) { - // parse jwt - async function authCookieToken(cookie?: string, token?: string) { - // 获取 cookie - const cookies = Cookie.parse(cookie || ''); - const cookieToken = token || cookies[TokenName]; - - if (!cookieToken) { - return Promise.reject(ERROR_ENUM.unAuthorization); - } - - return { ...(await authUserSession(cookieToken)), sessionId: cookieToken }; - } - // from authorization get apikey - async function parseAuthorization(authorization?: string) { - if (!authorization) { - return Promise.reject(ERROR_ENUM.unAuthorization); - } - - // Bearer fastgpt-xxxx-appId - const auth = authorization.split(' ')[1]; - if (!auth) { - return Promise.reject(ERROR_ENUM.unAuthorization); - } - - const { apikey, appId: authorizationAppid = '' } = await (async () => { - const arr = auth.split('-'); - // abandon - if (arr.length === 3) { - return { - apikey: `${arr[0]}-${arr[1]}`, - appId: arr[2] - }; - } - if (arr.length === 2) { - return { - apikey: auth - }; - } - return Promise.reject(ERROR_ENUM.unAuthorization); - })(); +export const createResourceDefaultCollaborators = async ({ + resource, + resourceType, + session, + tmbId +}: { + resource: SyncChildrenPermissionResourceType; + resourceType: PerResourceTypeEnum; - // auth apikey - const { teamId, tmbId, appId: apiKeyAppId = '', sourceName } = await authOpenApiKey({ apikey }); + // should be provided when inheritPermission is true + session: ClientSession; + tmbId: string; +}) => { + const parentClbs = await getResourceOwnedClbs({ + resourceId: resource.parentId, + resourceType, + teamId: resource.teamId, + session + }); + // 1. add owner into the permission list with owner per + // 2. remove parent's owner permission, instead of manager - return { - uid: '', - teamId, + const collaborators: CollaboratorItemType[] = [ + ...parentClbs + .filter((item) => item.tmbId !== tmbId) + .map((clb) => { + if (clb.permission === OwnerRoleVal) { + clb.permission = ManageRoleVal; + } + return clb; + }), + { tmbId, - apikey, - appId: apiKeyAppId || authorizationAppid, - sourceName - }; - } - // root user - async function parseRootKey(rootKey?: string) { - if (!rootKey || !process.env.ROOT_KEY || rootKey !== process.env.ROOT_KEY) { - return Promise.reject(ERROR_ENUM.unAuthorization); + permission: OwnerRoleVal } - } - - const { cookie, token, rootkey, authorization } = (req.headers || {}) as ReqHeaderAuthType; + ]; - const { uid, teamId, tmbId, appId, openApiKey, authType, isRoot, sourceName, sessionId } = - await (async () => { - if (authApiKey && authorization) { - // apikey from authorization - const authResponse = await parseAuthorization(authorization); - return { - uid: authResponse.uid, - teamId: authResponse.teamId, - tmbId: authResponse.tmbId, - appId: authResponse.appId, - openApiKey: authResponse.apikey, - authType: AuthUserTypeEnum.apikey, - sourceName: authResponse.sourceName - }; - } - if (authToken && (token || cookie)) { - // user token(from fastgpt web) - const res = await authCookieToken(cookie, token); + const ops: AnyBulkWriteOperation[] = []; - return { - uid: res.userId, - teamId: res.teamId, - tmbId: res.tmbId, - appId: '', - openApiKey: '', - authType: AuthUserTypeEnum.token, - isRoot: res.isRoot, - sessionId: res.sessionId - }; - } - if (authRoot && rootkey) { - await parseRootKey(rootkey); - // root user - return { - uid: '', - teamId: '', - tmbId: '', - appId: '', - openApiKey: '', - authType: AuthUserTypeEnum.root, - isRoot: true - }; + for (const clb of collaborators) { + ops.push({ + updateOne: { + filter: { + ...pickCollaboratorIdFields(clb), + teamId: resource.teamId, + resourceId: resource._id, + resourceType + }, + update: { + $set: { + permission: clb.permission + } + }, + upsert: true } - - return Promise.reject(ERROR_ENUM.unAuthorization); - })(); - - if (!authRoot && (!teamId || !tmbId)) { - return Promise.reject(ERROR_ENUM.unAuthorization); - } - - return { - userId: String(uid), - teamId: String(teamId), - tmbId: String(tmbId), - appId, - authType, - sourceName, - apikey: openApiKey, - isRoot: !!isRoot, - sessionId - }; -} - -/* set cookie */ -export const TokenName = 'fastgpt_token'; -export const setCookie = (res: NextApiResponse, token: string) => { - res.setHeader( - 'Set-Cookie', - `${TokenName}=${token}; Path=/; HttpOnly; Max-Age=604800; Samesite=Strict;` - ); -}; - -/* clear cookie */ -export const clearCookie = (res: NextApiResponse) => { - res.setHeader('Set-Cookie', `${TokenName}=; Path=/; Max-Age=0`); -}; - -/* file permission */ -export const createFileToken = (data: FileTokenQuery) => { - if (!process.env.FILE_TOKEN_KEY) { - return Promise.reject('System unset FILE_TOKEN_KEY'); + }); } - const expireMinutes = - data.customExpireMinutes ?? bucketNameMap[data.bucketName].previewExpireMinutes; - const expiredTime = Math.floor(addMinutes(new Date(), expireMinutes).getTime() / 1000); - - const key = (process.env.FILE_TOKEN_KEY as string) ?? 'filetoken'; - const token = jwt.sign( - { - ...data, - exp: expiredTime - }, - key - ); - return Promise.resolve(token); + await MongoResourcePermission.bulkWrite(ops, { session }); }; - -export const authFileToken = (token?: string) => - new Promise((resolve, reject) => { - if (!token) { - return reject(ERROR_ENUM.unAuthFile); - } - const key = (process.env.FILE_TOKEN_KEY as string) ?? 'filetoken'; - - jwt.verify(token, key, (err, decoded: any) => { - if (err || !decoded.bucketName || !decoded?.teamId || !decoded?.fileId) { - reject(ERROR_ENUM.unAuthFile); - return; - } - resolve({ - bucketName: decoded.bucketName, - teamId: decoded.teamId, - uid: decoded.uid, - fileId: decoded.fileId - }); - }); - }); diff --git a/packages/service/support/permission/dataset/auth.ts b/packages/service/support/permission/dataset/auth.ts index 3b4ca9d8358f..2b4d20d54126 100644 --- a/packages/service/support/permission/dataset/auth.ts +++ b/packages/service/support/permission/dataset/auth.ts @@ -1,5 +1,5 @@ import { type PermissionValueType } from '@fastgpt/global/support/permission/type'; -import { getResourcePermission, parseHeaderCert } from '../controller'; +import { getTmbPermission } from '../controller'; import { type CollectionWithDatasetType, type DatasetDataItemType, @@ -21,6 +21,7 @@ import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { DataSetDefaultRoleVal } from '@fastgpt/global/support/permission/dataset/constant'; import { getDatasetImagePreviewUrl } from '../../../core/dataset/image/utils'; import { i18nT } from '../../../../web/i18n/utils'; +import { parseHeaderCert } from '../auth/common'; export const authDatasetByTmbId = async ({ tmbId, @@ -61,54 +62,19 @@ export const authDatasetByTmbId = async ({ } const isOwner = tmbPer.isOwner || String(dataset.tmbId) === String(tmbId); + const isGetParentClb = + dataset.inheritPermission && dataset.type !== DatasetTypeEnum.folder && !!dataset.parentId; // get dataset permission or inherit permission from parent folder. - const { Per } = await (async () => { - if (isOwner) { - return { - Per: new DatasetPermission({ isOwner: true }) - }; - } - if ( - dataset.type === DatasetTypeEnum.folder || - dataset.inheritPermission === false || - !dataset.parentId - ) { - // 1. is a folder. (Folders have completely permission) - // 2. inheritPermission is false. - // 3. is root folder/dataset. - const rp = await getResourcePermission({ - teamId, - tmbId, - resourceId: datasetId, - resourceType: PerResourceTypeEnum.dataset - }); - const Per = new DatasetPermission({ - role: rp, - isOwner - }); - return { - Per - }; - } else { - // is not folder and inheritPermission is true and is not root folder. - const { dataset: parent } = await authDatasetByTmbId({ - tmbId, - datasetId: dataset.parentId, - per, - isRoot - }); - - const Per = new DatasetPermission({ - role: parent.permission.role, - isOwner - }); - - return { - Per - }; - } - })(); + const Per = new DatasetPermission({ + role: await getTmbPermission({ + teamId, + tmbId, + resourceId: isGetParentClb ? dataset.parentId! : datasetId, + resourceType: PerResourceTypeEnum.dataset + }), + isOwner + }); if (!Per.checkPer(per)) { return Promise.reject(DatasetErrEnum.unAuthDataset); diff --git a/packages/service/support/permission/evaluation/auth.ts b/packages/service/support/permission/evaluation/auth.ts index 1622ef31e7e8..c5d4266a3d33 100644 --- a/packages/service/support/permission/evaluation/auth.ts +++ b/packages/service/support/permission/evaluation/auth.ts @@ -1,4 +1,3 @@ -import { parseHeaderCert } from '../controller'; import { authAppByTmbId } from '../app/auth'; import { ManagePermissionVal, @@ -7,6 +6,7 @@ import { import type { EvaluationSchemaType } from '@fastgpt/global/core/app/evaluation/type'; import type { AuthModeType } from '../type'; import { MongoEvaluation } from '../../../core/app/evaluation/evalSchema'; +import { parseHeaderCert } from '../auth/common'; export const authEval = async ({ evalId, diff --git a/packages/service/support/permission/inheritPermission.ts b/packages/service/support/permission/inheritPermission.ts index 2ee7d93719d3..c8c946818e8e 100644 --- a/packages/service/support/permission/inheritPermission.ts +++ b/packages/service/support/permission/inheritPermission.ts @@ -1,11 +1,23 @@ import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; -import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; -import type { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; -import type { PermissionValueType } from '@fastgpt/global/support/permission/type'; -import type { ClientSession, Model } from 'mongoose'; +import { + ManageRoleVal, + NullPermissionVal, + OwnerRoleVal, + type PerResourceTypeEnum +} from '@fastgpt/global/support/permission/constant'; +import type { ResourcePermissionType } from '@fastgpt/global/support/permission/type'; import { mongoSessionRun } from '../../common/mongo/sessionRun'; -import { getResourceClbsAndGroups } from './controller'; +import { getResourceOwnedClbs } from './controller'; import { MongoResourcePermission } from './schema'; +import type { ClientSession, Model, AnyBulkWriteOperation } from '../../common/mongo'; +import { + getChangedCollaborators, + getCollaboratorId, + mergeCollaboratorList, + sumPer +} from '@fastgpt/global/support/permission/utils'; +import type { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator'; +import { pickCollaboratorIdFields } from './utils'; export type SyncChildrenPermissionResourceType = { _id: string; @@ -13,15 +25,10 @@ export type SyncChildrenPermissionResourceType = { teamId: string; parentId?: ParentIdType; }; -export type UpdateCollaboratorItem = { - permission: PermissionValueType; -} & RequireOnlyOne<{ - tmbId: string; - groupId: string; - orgId: string; -}>; - -// sync the permission to all children folders. + +/** + * sync the permission to all children folders. + */ export async function syncChildrenPermission({ resource, folderTypeList, @@ -42,55 +49,155 @@ export async function syncChildrenPermission({ // should be provided when inheritPermission is true session: ClientSession; - collaborators?: UpdateCollaboratorItem[]; + collaborators: CollaboratorItemType[]; }) { // only folder has permission const isFolder = folderTypeList.includes(resource.type); + const teamId = resource.teamId; + // If the 'root' is not a folder, which means the 'root' has no children, no need to sync. if (!isFolder) return; - // get all folders and the resource permission of the app + // get all the resource permission of the app const allFolders = await resourceModel .find( { - teamId: resource.teamId, - type: { $in: folderTypeList }, - inheritPermission: true + teamId, + inheritPermission: true, + type: { + $in: folderTypeList + } }, '_id parentId' ) .lean() .session(session); - // bfs to get all children + const allClbs = await MongoResourcePermission.find({ + resourceType, + teamId, + resourceId: { + $in: allFolders.map((folder) => folder._id) + } + }) + .lean() + .session(session); + + /** ResourceMap */ + const resourceMap = new Map(); + /** parentChildrenMap */ + const parentChildrenMap = new Map(); + + // init the map + allFolders.forEach((resource) => { + resourceMap.set(resource._id, resource); + const parentId = String(resource.parentId); + if (!parentChildrenMap.has(parentId)) { + parentChildrenMap.set(parentId, []); + } + parentChildrenMap.get(parentId)!.push(resource); + }); + + /** resourceIdPermissionMap + * save the clb virtual state, not the real state at present in the DB. + */ + const resourceIdClbMap = new Map(); + + // Initialize the resourceIdPermissionMap + // 1. add `root` clbs first + resourceIdClbMap.set( + resource._id, + collaborators?.map((clb) => ({ + ...clb, + teamId: resource.teamId, + resourceId: resource._id, + resourceType: resourceType + })) ?? [] + ); + // 2. add the clbs what we have now according to allClbs + for (const clb of allClbs) { + const resourceId = clb.resourceId; + if (resourceId === resource._id) continue; + const arr = resourceIdClbMap.get(resourceId); + if (!arr) { + resourceIdClbMap.set(resourceId, [clb]); + } else { + arr.push(clb); + } + } + + // BFS to get all children const queue = [String(resource._id)]; - const children: string[] = []; + const ops: AnyBulkWriteOperation[] = []; + const clbMap = new Map(collaborators.map((clb) => [getCollaboratorId(clb), { ...clb }])); + while (queue.length) { - const parentId = queue.shift(); - const folderChildren = allFolders.filter( - (folder) => String(folder.parentId) === String(parentId) - ); - children.push(...folderChildren.map((folder) => folder._id)); - queue.push(...folderChildren.map((folder) => folder._id)); - } - if (!children.length) return; + const parentId = String(queue.shift()); + const _children = parentChildrenMap.get(parentId) || []; + if (_children.length === 0) continue; + for (const child of _children) { + // 1. get parent's permission and what permission I have. + const parentClbs = resourceIdClbMap.get(String(child.parentId)) || []; + const myClbs = resourceIdClbMap.get(child._id) || []; + const parentIdSet = new Set(parentClbs.map((clb) => getCollaboratorId(clb))); + const myClbsIdSet = new Set(myClbs.map((clb) => getCollaboratorId(clb))); - // sync the resource permission - if (collaborators) { - // Update the collaborators of all children - for await (const childId of children) { - await syncCollaborators({ - resourceType, - session, - collaborators, - teamId: resource.teamId, - resourceId: childId - }); + // add or update + for (const clb of collaborators) { + if (!myClbsIdSet.has(getCollaboratorId(clb))) { + ops.push({ + insertOne: { + document: { + resourceId: child._id, + resourceType, + teamId, + permission: parentClbs[0].permission, + ...pickCollaboratorIdFields(clb) + } as ResourcePermissionType + } + }); + } else { + const myclb = myClbs.find((clb) => getCollaboratorId(clb) === getCollaboratorId(clb))!; + ops.push({ + updateOne: { + filter: { + resourceId: child._id, + teamId, + ...pickCollaboratorIdFields(clb), + resourceType + }, + update: { + permission: sumPer(myclb.permission, clb.permission) + } + } + }); + } + } + + // delele + for (const myClb of myClbs) { + if (!clbMap.get(getCollaboratorId(myClb))) { + // the new collaborators doesnt have it. remove it + ops.push({ + deleteOne: { + filter: { + resourceId: child._id, + teamId, + ...pickCollaboratorIdFields(myClb), + resourceType + } + } + }); + } + } + queue.push(child._id); } } + await MongoResourcePermission.bulkWrite(ops, { session }); + return; } -/* Resume the inherit permission of the resource. +/** Resume the inherit permission of the resource. 1. Folder: Sync parent's defaultPermission and clbs, and sync its children. 2. Resource: Sync parent's defaultPermission, and delete all its clbs. */ @@ -108,7 +215,6 @@ export async function resumeInheritPermission({ session?: ClientSession; }) { const isFolder = folderTypeList.includes(resource.type); - const fn = async (session: ClientSession) => { // update the resource permission await resourceModel.updateOne( @@ -122,36 +228,31 @@ export async function resumeInheritPermission({ ); // Folder resource, need to sync children - if (isFolder) { - const parentClbsAndGroups = await getResourceClbsAndGroups({ - resourceId: resource.parentId, - teamId: resource.teamId, - resourceType, - session - }); + const parentClbs = await getResourceOwnedClbs({ + resourceId: resource.parentId, + teamId: resource.teamId, + resourceType, + session + }); + if (isFolder) { // sync self await syncCollaborators({ resourceType, - collaborators: parentClbsAndGroups, + collaborators: parentClbs, teamId: resource.teamId, resourceId: resource._id, session }); // sync children await syncChildrenPermission({ - resource: { - ...resource - }, + resource, resourceModel, folderTypeList, resourceType, session, - collaborators: parentClbsAndGroups + collaborators: parentClbs }); - } else { - // Not folder, delete all clb - await MongoResourcePermission.deleteMany({ resourceId: resource._id }, { session }); } }; @@ -162,9 +263,9 @@ export async function resumeInheritPermission({ } } -/* - Delete all the collaborators and then insert the new collaborators. -*/ +/** + * sync parent collaborators to children. + */ export async function syncCollaborators({ resourceType, teamId, @@ -175,30 +276,59 @@ export async function syncCollaborators({ resourceType: PerResourceTypeEnum; teamId: string; resourceId: string; - collaborators: UpdateCollaboratorItem[]; + collaborators: CollaboratorItemType[]; session: ClientSession; }) { - await MongoResourcePermission.deleteMany( - { - resourceType, - teamId, - resourceId - }, - { session } - ); - await MongoResourcePermission.insertMany( - collaborators.map((item) => ({ - teamId: teamId, - resourceId, - resourceType: resourceType, - tmbId: item.tmbId, - groupId: item.groupId, - orgId: item.orgId, - permission: item.permission - })), - { - session, - ordered: true + // should change parent owner permission into manage + collaborators.forEach((clb) => { + if (clb.permission === OwnerRoleVal) { + clb.permission = ManageRoleVal; } + }); + const parentClbMap = new Map(collaborators.map((clb) => [getCollaboratorId(clb), clb])); + const clbsNow = await MongoResourcePermission.find({ + resourceType, + teamId, + resourceId + }) + .lean() + .session(session); + const ops: AnyBulkWriteOperation[] = []; + for (const clb of clbsNow) { + const parentClb = parentClbMap.get(getCollaboratorId(clb)); + const permission = sumPer(parentClb?.permission ?? NullPermissionVal, clb.permission); + ops.push({ + updateOne: { + filter: { + teamId, + resourceId, + resourceType, + ...pickCollaboratorIdFields(clb) + }, + update: { + permission + } + } + }); + } + + const parentHasAndIHaveNot = collaborators.filter( + (clb) => !clbsNow.some((myClb) => getCollaboratorId(clb) === getCollaboratorId(myClb)) ); + + for (const clb of parentHasAndIHaveNot) { + ops.push({ + insertOne: { + document: { + teamId, + resourceId, + resourceType, + ...pickCollaboratorIdFields(clb), + permission: clb.permission + } as ResourcePermissionType + } + }); + } + + await MongoResourcePermission.bulkWrite(ops, { session }); } diff --git a/packages/service/support/permission/memberGroup/controllers.ts b/packages/service/support/permission/memberGroup/controllers.ts index 3d775e11c1be..3620a8b24f1d 100644 --- a/packages/service/support/permission/memberGroup/controllers.ts +++ b/packages/service/support/permission/memberGroup/controllers.ts @@ -1,6 +1,5 @@ import { type MemberGroupSchemaType } from '@fastgpt/global/support/permission/memberGroup/type'; import { MongoGroupMemberModel } from './groupMemberSchema'; -import { parseHeaderCert } from '../controller'; import { MongoMemberGroupModel } from './memberGroupSchema'; import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; import { type ClientSession } from 'mongoose'; @@ -9,6 +8,7 @@ import { type AuthModeType, type AuthResponseType } from '../type'; import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; import { getTmbInfoByTmbId } from '../../user/team/controller'; +import { parseHeaderCert } from '../auth/common'; /** * Get the default group of a team diff --git a/packages/service/support/permission/org/orgSchema.ts b/packages/service/support/permission/org/orgSchema.ts index 94fa531e466f..a695ffb1dd9d 100644 --- a/packages/service/support/permission/org/orgSchema.ts +++ b/packages/service/support/permission/org/orgSchema.ts @@ -4,6 +4,7 @@ import type { OrgSchemaType } from '@fastgpt/global/support/user/team/org/type'; import { connectionMongo, getMongoModel } from '../../../common/mongo'; import { OrgMemberCollectionName } from './orgMemberSchema'; import { getNanoid } from '@fastgpt/global/common/string/tools'; +import { DEFAULT_ORG_AVATAR } from '@fastgpt/global/common/system/constants'; const { Schema } = connectionMongo; export const OrgSchema = new Schema( @@ -29,7 +30,12 @@ export const OrgSchema = new Schema( type: String, required: true }, - avatar: String, + avatar: { + type: String, + get: function (value: string) { + return value || DEFAULT_ORG_AVATAR; + } + }, description: String, updateTime: { type: Date, diff --git a/packages/service/support/permission/publish/authLink.ts b/packages/service/support/permission/publish/authLink.ts index 269ce4854aa0..479c3babbc47 100644 --- a/packages/service/support/permission/publish/authLink.ts +++ b/packages/service/support/permission/publish/authLink.ts @@ -1,11 +1,11 @@ import { type AppDetailType } from '@fastgpt/global/core/app/type'; import { type OutlinkAppType, type OutLinkSchema } from '@fastgpt/global/support/outLink/type'; -import { parseHeaderCert } from '../controller'; import { MongoOutLink } from '../../outLink/schema'; import { OutLinkErrEnum } from '@fastgpt/global/common/error/code/outLink'; import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant'; import { authAppByTmbId } from '../app/auth'; import { type AuthModeType, type AuthResponseType } from '../type'; +import { parseHeaderCert } from '../auth/common'; /* crud outlink permission */ export async function authOutLinkCrud({ diff --git a/packages/service/support/permission/schema.ts b/packages/service/support/permission/schema.ts index ae53c7da1d2e..56c5e602d85c 100644 --- a/packages/service/support/permission/schema.ts +++ b/packages/service/support/permission/schema.ts @@ -34,11 +34,18 @@ export const ResourcePermissionSchema = new Schema({ enum: Object.values(PerResourceTypeEnum), required: true }, + /** + * The **Role** of the object to the resource. + */ permission: { type: Number, required: true }, - // Resrouce ID: App or DataSet or any other resource type. + /** + * Optional. Only be set when the resource is *inherited* from the parent resource. + * For recording the self permission. When cancel the inheritance, it will overwrite the permission property and set to `unset`. + */ + // Resource ID: App or DataSet or any other resource type. // It is null if the resourceType is team. resourceId: { type: Schema.Types.ObjectId diff --git a/packages/service/support/permission/user/auth.ts b/packages/service/support/permission/user/auth.ts index 5029008fc13a..88dd38555991 100644 --- a/packages/service/support/permission/user/auth.ts +++ b/packages/service/support/permission/user/auth.ts @@ -1,11 +1,10 @@ import { type TeamTmbItemType } from '@fastgpt/global/support/user/team/type'; -import { parseHeaderCert } from '../controller'; import { getTmbInfoByTmbId } from '../../user/team/controller'; import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import { type AuthModeType, type AuthResponseType } from '../type'; import { NullPermissionVal } from '@fastgpt/global/support/permission/constant'; import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; -import { authCert } from '../auth/common'; +import { authCert, parseHeaderCert } from '../auth/common'; import { MongoUser } from '../../user/schema'; import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; import { type ApiRequestProps } from '../../../type/next'; diff --git a/packages/service/support/permission/utils.ts b/packages/service/support/permission/utils.ts new file mode 100644 index 000000000000..eb4a70b80fe3 --- /dev/null +++ b/packages/service/support/permission/utils.ts @@ -0,0 +1,9 @@ +import type { CollaboratorIdType } from '@fastgpt/global/support/permission/collaborator'; + +export const pickCollaboratorIdFields = (clb: CollaboratorIdType) => { + return { + ...(clb.tmbId && { tmbId: clb.tmbId }), + ...(clb.groupId && { groupId: clb.groupId }), + ...(clb.orgId && { orgId: clb.orgId }) + }; +}; diff --git a/packages/service/support/user/team/controller.ts b/packages/service/support/user/team/controller.ts index a399bd35884d..fa8ed440b390 100644 --- a/packages/service/support/user/team/controller.ts +++ b/packages/service/support/user/team/controller.ts @@ -8,7 +8,7 @@ import { import { MongoTeamMember } from './teamMemberSchema'; import { MongoTeam } from './teamSchema'; import { type UpdateTeamProps } from '@fastgpt/global/support/user/team/controller'; -import { getResourcePermission } from '../../permission/controller'; +import { getTmbPermission } from '../../permission/controller'; import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; import { TeamDefaultRoleVal } from '@fastgpt/global/support/permission/user/constant'; @@ -26,7 +26,7 @@ async function getTeamMember(match: Record): Promise - {({ MemberListCard, onOpenManageModal, onOpenAddMember }) => { + {({ MemberListCard, onOpenManageModal }) => { return ( <> @@ -145,26 +145,15 @@ const FolderSlideCard = ({ {t('common:permission.Collaborator')} {managePer.permission.hasManagePer && ( - - - - - - - - + + + )} - {({ MemberListCard, onOpenManageModal, onOpenAddMember }) => { + {({ MemberListCard, onOpenManageModal }) => { return ( <> {t('common:permission.Collaborator')} - - - - + diff --git a/projects/app/src/components/support/permission/MemberManager/ManageModal.tsx b/projects/app/src/components/support/permission/MemberManager/ManageModal.tsx deleted file mode 100644 index cf84645e7428..000000000000 --- a/projects/app/src/components/support/permission/MemberManager/ManageModal.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { useUserStore } from '@/web/support/user/useUserStore'; -import { Flex, ModalBody, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react'; -import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; -import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; -import Avatar from '@fastgpt/web/components/common/Avatar'; -import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; -import MyIcon from '@fastgpt/web/components/common/Icon'; -import Loading from '@fastgpt/web/components/common/MyLoading'; -import MyModal from '@fastgpt/web/components/common/MyModal'; -import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; -import { useTranslation } from 'next-i18next'; -import React from 'react'; -import { useContextSelector } from 'use-context-selector'; -import RoleSelect from './RoleSelect'; -import RoleTags from './RoleTags'; -import { CollaboratorContext } from './context'; -export type ManageModalProps = { - onClose: () => void; -}; - -function ManageModal({ onClose }: ManageModalProps) { - const { t } = useTranslation(); - const { userInfo } = useUserStore(); - const { permission, collaboratorList, onUpdateCollaborators, onDelOneCollaborator } = - useContextSelector(CollaboratorContext, (v) => v); - - const { runAsync: onDelete, loading: isDeleting } = useRequest2(onDelOneCollaborator); - - const { runAsync: onUpdate, loading: isUpdating } = useRequest2(onUpdateCollaborators, { - successToast: t('common:update_success'), - errorToast: 'Error' - }); - - const loading = isDeleting || isUpdating; - - return ( - - - - - - - - - - - - - - {collaboratorList?.map((item) => { - return ( - - - - - - ); - })} - -
{t('user:name')}{t('user:permissions')} - {t('user:operations')} -
- - - {item.name === DefaultGroupName ? userInfo?.team.teamName : item.name} - - - - - {/* Not self; Not owner and other manager */} - {item.tmbId !== userInfo?.team?.tmbId && - (permission.isOwner || !item.permission.hasManagePer) && ( - - } - value={item.permission.role} - onChange={(permission) => { - onUpdate({ - members: item.tmbId ? [item.tmbId] : undefined, - groups: item.groupId ? [item.groupId] : undefined, - orgs: item.orgId ? [item.orgId] : undefined, - permission - }); - }} - onDelete={() => { - onDelete({ - tmbId: item.tmbId, - groupId: item.groupId, - orgId: item.orgId - } as RequireOnlyOne<{ - tmbId: string; - groupId: string; - orgId: string; - }>); - }} - /> - )} -
- {collaboratorList?.length === 0 && } -
- {loading && } -
-
- ); -} - -export default ManageModal; diff --git a/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx b/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx index 5811fd7a6702..f783b47f1a61 100644 --- a/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx +++ b/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx @@ -1,92 +1,115 @@ import React from 'react'; -import { useTranslation } from 'next-i18next'; -import { Box, Checkbox, HStack, VStack } from '@chakra-ui/react'; +import { Box, Checkbox, Flex, HStack, VStack } from '@chakra-ui/react'; import Avatar from '@fastgpt/web/components/common/Avatar'; import RoleTags from './RoleTags'; import type { RoleValueType } from '@fastgpt/global/support/permission/type'; import MyIcon from '@fastgpt/web/components/common/Icon'; import OrgTags from '../../user/team/OrgTags'; -import Tag from '@fastgpt/web/components/common/Tag'; +import RoleSelect from './RoleSelect'; +import { ChevronDownIcon } from '@chakra-ui/icons'; function MemberItemCard({ avatar, key, - onChange: _onChange, + onChange, isChecked, onDelete, name, role, orgs, - addOnly, - rightSlot + rightSlot, + onRoleChange, + disabled = false }: { avatar: string; key: string; - onChange: () => void; + onChange?: () => void; + onRoleChange?: (role: RoleValueType) => void; isChecked?: boolean; onDelete?: () => void; name: string; role?: RoleValueType; - addOnly?: boolean; orgs?: string[]; rightSlot?: React.ReactNode; + disabled?: boolean; }) { - const isAdded = addOnly && !!role; - const onChange = () => { - if (!isAdded) _onChange(); - }; - const { t } = useTranslation(); + const showRoleSelect = onRoleChange !== undefined; return ( - { + if (disabled) return; + onChange?.(); }} - onClick={onChange} > - {isChecked !== undefined && ( - - )} - - - - + + {isChecked !== undefined && } + + {name} {orgs && orgs.length > 0 && } - - {!isAdded && role && } - {isAdded && ( - - {t('user:team.collaborator.added')} - + + {showRoleSelect && ( + + + + + + + } + onChange={onRoleChange} + /> )} {onDelete !== undefined && ( - + + { + if (disabled) return; + onDelete?.(); + }} + /> + )} {rightSlot} - + ); } diff --git a/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx b/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx index 3755d237e10e..6d4d3a3492f8 100644 --- a/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx +++ b/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx @@ -3,17 +3,13 @@ import { getTeamMembers } from '@/web/support/user/team/api'; import { getGroupList } from '@/web/support/user/team/group/api'; import useOrg from '@/web/support/user/team/org/hooks/useOrg'; import { useUserStore } from '@/web/support/user/useUserStore'; -import { ChevronDownIcon } from '@chakra-ui/icons'; -import { Box, Button, Flex, Grid, HStack, ModalBody, ModalFooter, Text } from '@chakra-ui/react'; +import { Box, Button, Flex, Grid, HStack, ModalBody, ModalFooter } from '@chakra-ui/react'; import { DEFAULT_ORG_AVATAR, DEFAULT_TEAM_AVATAR, DEFAULT_USER_AVATAR } from '@fastgpt/global/common/system/constants'; -import { type UpdateClbPermissionProps } from '@fastgpt/global/support/permission/collaborator'; -import { type MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type'; import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; -import { type OrgListItemType } from '@fastgpt/global/support/user/team/org/type'; import { type TeamMemberItemType } from '@fastgpt/global/support/user/team/type'; import MyAvatar from '@fastgpt/web/components/common/Avatar'; import MyIcon from '@fastgpt/web/components/common/Icon'; @@ -22,28 +18,33 @@ import MyModal from '@fastgpt/web/components/common/MyModal'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; import { useTranslation } from 'next-i18next'; -import { type ValueOf } from 'next/dist/shared/lib/constants'; -import { useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useContextSelector } from 'use-context-selector'; import { CollaboratorContext } from './context'; import MemberItemCard from './MemberItemCard'; -import RoleSelect from './RoleSelect'; +import type { + CollaboratorItemDetailType, + CollaboratorItemType +} from '@fastgpt/global/support/permission/collaborator'; +import type { RoleValueType } from '@fastgpt/global/support/permission/type'; +import { Permission } from '@fastgpt/global/support/permission/controller'; +import { + checkRoleUpdateConflict, + getCollaboratorId +} from '@fastgpt/global/support/permission/utils'; +import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; +import { OwnerRoleVal } from '@fastgpt/global/support/permission/constant'; const HoverBoxStyle = { bgColor: 'myGray.50', cursor: 'pointer' }; -function MemberModal({ - onClose, - addPermissionOnly: addOnly = false -}: { - onClose: () => void; - addPermissionOnly?: boolean; -}) { +function MemberModal({ onClose }: { onClose: () => void }) { const { t } = useTranslation(); const { userInfo } = useUserStore(); - const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList); + const collaboratorDetailList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList); + const defaultRole = useContextSelector(CollaboratorContext, (v) => v.defaultRole); const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>(); const { paths, @@ -91,34 +92,33 @@ function MemberModal({ } ); - const [selectedOrgList, setSelectedOrgIdList] = useState([]); + const [editCollaborators, setCollaboratorList] = useState([]); - const [selectedMemberList, setSelectedMemberList] = useState< - Omit[] - >([]); - - const [selectedGroupList, setSelectedGroupList] = useState[]>([]); - const roleList = useContextSelector(CollaboratorContext, (v) => v.roleList); - const getRoleLabelList = useContextSelector(CollaboratorContext, (v) => v.getRoleLabelList); - const [selectedRole, setSelectedRole] = useState(roleList?.read?.value); - const roleLabel = useMemo(() => { - if (selectedRole === undefined) return ''; - return getRoleLabelList(selectedRole!).join('、'); - }, [getRoleLabelList, selectedRole]); + useEffect(() => { + setCollaboratorList(collaboratorDetailList); + }, [collaboratorDetailList]); const onUpdateCollaborators = useContextSelector( CollaboratorContext, (v) => v.onUpdateCollaborators ); - const { runAsync: onConfirm, loading: isUpdating } = useRequest2( + const parentClbs = useContextSelector(CollaboratorContext, (v) => v.parentClbList); + const myRole = useContextSelector(CollaboratorContext, (v) => v.myRole); + + const { runAsync: _onConfirm, loading: isUpdating } = useRequest2( () => onUpdateCollaborators({ - members: selectedMemberList.map((item) => item.tmbId), - groups: selectedGroupList.map((item) => item._id), - orgs: selectedOrgList.map((item) => item._id), - permission: addOnly ? undefined : selectedRole! - } as UpdateClbPermissionProps>), + collaborators: editCollaborators.map( + (clb) => + ({ + tmbId: clb.tmbId, + groupId: clb.groupId, + orgId: clb.orgId, + permission: clb.permission.role + }) as CollaboratorItemType + ) + }), { successToast: t('common:add_success'), onSuccess() { @@ -127,334 +127,374 @@ function MemberModal({ } ); + // TODO: I18n + const { openConfirm: openConfirmDisableInheritPer, ConfirmModal: ConfirmDisableInheritPer } = + useConfirm({ + content: t('permission.Remove InheritPermission Confirm') + }); + + const onConfirm = useCallback(() => { + const isConflict = checkRoleUpdateConflict({ + parentClbs: parentClbs.map((clb) => ({ + ...clb, + permission: clb.permission.role + })), + oldRealClbs: collaboratorDetailList.map((clb) => ({ + ...clb, + permission: clb.permission.role + })), + newChildClbs: editCollaborators.map((clb) => ({ + ...clb, + permission: clb.permission.role + })) + }); + if (!isConflict) { + return _onConfirm(); + } else { + openConfirmDisableInheritPer(_onConfirm)(); + } + }, [ + _onConfirm, + collaboratorDetailList, + editCollaborators, + openConfirmDisableInheritPer, + parentClbs + ]); + const entryList = useRef([ { label: t('user:team.group.members'), icon: DEFAULT_USER_AVATAR, value: 'member' }, { label: t('user:team.org.org'), icon: DEFAULT_ORG_AVATAR, value: 'org' }, { label: t('user:team.group.group'), icon: DEFAULT_TEAM_AVATAR, value: 'group' } ]); - const selectedList = useMemo(() => { - return [ - ...selectedOrgList.map((item) => ({ - id: `org-${item._id}`, - avatar: item.avatar, - name: item.name, - onDelete: () => setSelectedOrgIdList(selectedOrgList.filter((v) => v._id !== item._id)), - orgs: undefined - })), - ...selectedGroupList.map((item) => ({ - id: `group-${item._id}`, - avatar: item.avatar, - name: item.name === DefaultGroupName ? userInfo?.team.teamName : item.name, - onDelete: () => setSelectedGroupList(selectedGroupList.filter((v) => v._id !== item._id)), - orgs: undefined - })), - ...selectedMemberList.map((item) => ({ - id: `member-${item.tmbId}`, - avatar: item.avatar, - name: item.memberName, - onDelete: () => - setSelectedMemberList(selectedMemberList.filter((v) => v.tmbId !== item.tmbId)), - orgs: item.orgs - })) - ]; - }, [selectedOrgList, selectedGroupList, selectedMemberList, userInfo?.team.teamName]); - return ( - - - - + + + - setSearchKey(e.target.value)} - /> + + setSearchKey(e.target.value)} + /> - - {/* Entry */} - {!searchKey && !filterClass && ( - <> - {entryList.current.map((item) => { - return ( - setFilterClass(item.value as any)} - > - - - {item.label} - - - - ); - })} - - )} + + {/* Entry */} + {!searchKey && !filterClass && ( + <> + {entryList.current.map((item) => { + return ( + setFilterClass(item.value as any)} + > + + + {item.label} + + + + ); + })} + + )} - {/* Path */} - {!searchKey && filterClass && ( - - { - if (parentId === '') { - setFilterClass(undefined); - onPathClick(''); - } else if ( - parentId === 'member' || - parentId === 'org' || - parentId === 'group' - ) { - setFilterClass(parentId); - onPathClick(''); - } else { - onPathClick(parentId); - } - }} - rootName={t('common:Team')} - /> - - )} - {(filterClass === 'member' || searchKey) && - (() => { - const Members = members?.map((member) => { - const onChange = () => { - setSelectedMemberList((state) => { - if (state.find((v) => v.tmbId === member.tmbId)) { - return state.filter((v) => v.tmbId !== member.tmbId); + {/* Path */} + {!searchKey && filterClass && ( + + { + if (parentId === '') { + setFilterClass(undefined); + onPathClick(''); + } else if ( + parentId === 'member' || + parentId === 'org' || + parentId === 'group' + ) { + setFilterClass(parentId); + onPathClick(''); + } else { + onPathClick(parentId); } - return [...state, member]; - }); - }; - const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId); - return ( - v.tmbId === member.tmbId)} - orgs={member.orgs} + }} + rootName={t('common:Team')} + /> + + )} + {(filterClass === 'member' || searchKey) && + (() => { + const MemberList = ( + ); - }); - return searchKey ? ( - Members - ) : ( - - {Members} - - ); - })()} - {(filterClass === 'org' || searchKey) && - (() => { - const Orgs = orgs?.map((org) => { - const onChange = () => { - setSelectedOrgIdList((state) => { - if (state.find((v) => v._id === org._id)) { - return state.filter((v) => v._id !== org._id); + return searchKey ? ( + MemberList + ) : ( + + {MemberList} + + ); + })()} + {(filterClass === 'org' || searchKey) && + (() => { + const Orgs = orgs?.map((org) => { + const addTheOrg = () => { + setCollaboratorList((state) => { + if (state.find((v) => v.orgId === org._id)) { + return state.filter((v) => v.orgId !== org._id); + } + return [ + ...state, + { + ...org, + orgId: org._id, + permission: new Permission({ role: defaultRole }) + } + ]; + }); + }; + const isChecked = !!editCollaborators.find((v) => v.orgId === org._id); + return ( + { + onClickOrg(org); + e.stopPropagation(); + }} + /> + ) + } + /> + ); + }); + return searchKey ? ( + Orgs + ) : ( + + {Orgs} + + + ); + })()} + {(filterClass === 'group' || searchKey) && + groups?.map((group) => { + const addGroup = () => { + setCollaboratorList((state) => { + if (state.find((v) => v.groupId === group._id)) { + return state.filter((v) => v.groupId !== group._id); } - return [...state, org]; + return [ + ...state, + { + ...group, + groupId: group._id, + permission: new Permission({ role: defaultRole }) + } + ]; }); }; - const collaborator = collaboratorList?.find((v) => v.orgId === org._id); + const isChecked = !!editCollaborators.find((v) => v.groupId === group._id); return ( String(v._id) === String(org._id))} - rightSlot={ - org.total && ( - { - onClickOrg(org); - // setPath(getOrgChildrenPath(org)); - e.stopPropagation(); - }} - /> - ) + avatar={group.avatar} + key={group._id} + name={ + group.name === DefaultGroupName + ? userInfo?.team.teamName ?? '' + : group.name } + onChange={addGroup} + isChecked={isChecked} /> ); - }); - return searchKey ? ( - Orgs - ) : ( - - {Orgs} - {orgMembers.map((member) => { - const isChecked = !!selectedMemberList.find( - (v) => v.tmbId === member.tmbId - ); - const collaborator = collaboratorList?.find( - (v) => v.tmbId === member.tmbId - ); - return ( - { - setSelectedMemberList((state) => { - if (state.find((v) => v.tmbId === member.tmbId)) { - return state.filter((v) => v.tmbId !== member.tmbId); - } - return [...state, member]; - }); - }} - isChecked={isChecked} - role={collaborator?.permission.role} - addOnly={addOnly && !!member.permission.role} - orgs={member.orgs} - /> - ); - })} - - ); - })()} - {(filterClass === 'group' || searchKey) && - groups?.map((group) => { - const onChange = () => { - setSelectedGroupList((state) => { - if (state.find((v) => v._id === group._id)) { - return state.filter((v) => v._id !== group._id); - } - return [...state, group]; + })} + + + + + + {`${t('user:has_chosen')}: `} + {editCollaborators.length} + + + {editCollaborators.map((clb) => { + const onDelete = () => { + setCollaboratorList((state) => { + return state.filter((v) => getCollaboratorId(v) !== getCollaboratorId(clb)); + }); + }; + const onRoleChange = (role: RoleValueType) => { + setCollaboratorList((state) => { + const index = state.findIndex( + (v) => getCollaboratorId(v) === getCollaboratorId(clb) + ); + if (index === -1) return state; + return [ + ...state.slice(0, index), + { + ...state[index], + permission: new Permission({ role }) + }, + ...state.slice(index + 1) + ]; }); }; - const collaborator = collaboratorList?.find((v) => v.groupId === group._id); return ( v._id === group._id)} - addOnly={addOnly} /> ); })} - - - - - - {`${t('user:has_chosen')}: `} - {selectedMemberList.length + selectedGroupList.length + selectedOrgList.length} - - - {selectedList.map((item) => { - return ( - - ); - })} - - - - - - {!addOnly && !!roleList && ( - - {roleLabel} - - } - onChange={(v) => setSelectedRole(v)} - /> - )} - {addOnly && ( - - - {t('user:permission_add_tip')} - - )} - - - + + + + + + + + + ); } export default MemberModal; + +const RenderMemberList = ({ + members, + setCollaboratorList, + editCollaborators, + defaultRole +}: { + members: TeamMemberItemType[]; + setCollaboratorList: React.Dispatch>; + editCollaborators: CollaboratorItemDetailType[]; + defaultRole: RoleValueType; +}) => { + const { userInfo } = useUserStore(); + const myRole = useContextSelector(CollaboratorContext, (v) => v.myRole); + return ( + <> + {members?.map((member) => { + const addTheMember = () => { + setCollaboratorList((state) => { + if (state.find((v) => v.tmbId === member.tmbId)) { + return state.filter((v) => v.tmbId !== member.tmbId); + } + return [ + ...state, + { + tmbId: member.tmbId, + avatar: member.avatar, + name: member.memberName, + teamId: member.teamId, + permission: new Permission({ role: defaultRole }) + } + ]; + }); + }; + const isChecked = !!editCollaborators.find((v) => v.tmbId === member.tmbId); + return ( + + ); + })} + + ); +}; diff --git a/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx b/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx index 6d4e29700c75..f8f01012cb39 100644 --- a/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx +++ b/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx @@ -18,6 +18,7 @@ import { Permission } from '@fastgpt/global/support/permission/controller'; import { CollaboratorContext } from './context'; import { useTranslation } from 'next-i18next'; import MyDivider from '@fastgpt/web/components/common/MyDivider'; +import { ManageRoleVal } from '@fastgpt/global/support/permission/constant'; export type PermissionSelectProps = { value?: RoleValueType; @@ -47,16 +48,15 @@ function RoleSelect({ offset = [0, 5], Button, width = 'auto', - onDelete + onDelete, + disabled }: PermissionSelectProps) { const { t } = useTranslation(); const ref = useRef(null); const closeTimer = useRef(); - const { permission, roleList: permissionList } = useContextSelector( - CollaboratorContext, - (v) => v - ); + const { roleList: permissionList } = useContextSelector(CollaboratorContext, (v) => v); + const myRole = useContextSelector(CollaboratorContext, (v) => v.myRole); const [isOpen, setIsOpen] = useState(false); @@ -72,15 +72,18 @@ function RoleSelect({ }; }); + const singleOptions = list.filter((item) => item.checkBoxType === 'single'); + const per = new Permission({ role }); + return { - singleOptions: list.filter( - (item) => - item.checkBoxType === 'single' && - (permission.isOwner || item.value !== permissionList['manage'].value) - ), + singleOptions: myRole.isOwner + ? singleOptions + : myRole.hasManagePer && !per.hasManagePer + ? singleOptions.filter((item) => item.value !== ManageRoleVal) + : [], checkboxList: list.filter((item) => item.checkBoxType === 'multiple') }; - }, [permission.isOwner, permissionList]); + }, [myRole.hasManagePer, myRole.isOwner, permissionList, role]); const selectedSingleValue = useMemo(() => { if (!permissionList) return undefined; @@ -120,6 +123,7 @@ function RoleSelect({ ref={ref} w="fit-content" onMouseEnter={() => { + if (disabled) return; if (trigger === 'hover') { setIsOpen(true); } @@ -135,8 +139,10 @@ function RoleSelect({ > { if (trigger === 'click') { + if (disabled) return; setIsOpen(!isOpen); } }} @@ -158,6 +164,9 @@ function RoleSelect({ {/* The list of single select permissions */} {roleOptions.singleOptions.map((item) => { const change = () => { + if (disabled) { + return; + } const per = new Permission({ role }); per.removeRole(selectedSingleValue); per.addRole(item.value); @@ -187,7 +196,7 @@ function RoleSelect({ ); })} - {roleOptions.checkboxList.length > 0 && ( + {roleOptions.checkboxList.length > 0 && roleOptions.singleOptions.length > 0 && ( <> @@ -197,7 +206,8 @@ function RoleSelect({ )} {roleOptions.checkboxList.map((item) => { - const change = () => { + const change = (e: React.MouseEvent) => { + if ((e.target as HTMLElement).tagName === 'INPUT') return; const per = new Permission({ role }); if (per.checkRole(item.value)) { per.removeRole(item.value); @@ -216,9 +226,13 @@ function RoleSelect({ } : {})} {...MenuStyle} + onClick={(e) => { + if (disabled) return; + change(e); + }} > - - + + {t(item.name as any)} {t(item.description as any)} diff --git a/projects/app/src/components/support/permission/MemberManager/RoleTags.tsx b/projects/app/src/components/support/permission/MemberManager/RoleTags.tsx index fd50190ee54b..849cdbaa1e28 100644 --- a/projects/app/src/components/support/permission/MemberManager/RoleTags.tsx +++ b/projects/app/src/components/support/permission/MemberManager/RoleTags.tsx @@ -19,7 +19,7 @@ function RoleTags({ permission }: PermissionTagsProp) { const roleTagList = getRoleLabelList(permission); return ( - + {roleTagList.map((item) => ( import('./MemberModal')); -const ManageModal = dynamic(() => import('./ManageModal')); export type MemberManagerInputPropsType = { permission: Permission; - onGetCollaboratorList: () => Promise; + defaultRole: RoleValueType; + onGetCollaboratorList: () => Promise; roleList?: RoleListType; onUpdateCollaborators: (props: UpdateClbPermissionProps) => Promise; onDelOneCollaborator: ( @@ -35,22 +37,25 @@ export type MemberManagerInputPropsType = { refreshDeps?: any[]; }; -export type MemberManagerPropsType = MemberManagerInputPropsType & { - collaboratorList: CollaboratorItemType[]; +export type CollaboratorContextType = MemberManagerInputPropsType & { + collaboratorList: CollaboratorItemDetailType[]; + parentClbList: CollaboratorItemDetailType[]; + myRole: Permission; refetchCollaboratorList: () => void; isFetchingCollaborator: boolean; getRoleLabelList: (role: RoleValueType) => string[]; }; + export type ChildrenProps = { - onOpenAddMember: () => void; onOpenManageModal: () => void; MemberListCard: (props: MemberListCardProps) => JSX.Element; }; -type CollaboratorContextType = MemberManagerPropsType & {}; - export const CollaboratorContext = createContext({ + myRole: new Permission(), + defaultRole: NullRoleVal, collaboratorList: [], + parentClbList: [], roleList: CommonRoleList, onUpdateCollaborators: () => { throw new Error('Function not implemented.'); @@ -64,7 +69,7 @@ export const CollaboratorContext = createContext({ refetchCollaboratorList: (): void => { throw new Error('Function not implemented.'); }, - onGetCollaboratorList: (): Promise => { + onGetCollaboratorList: (): Promise => { throw new Error('Function not implemented.'); }, isFetchingCollaborator: false, @@ -80,9 +85,7 @@ const CollaboratorContextProvider = ({ children, refetchResource, refreshDeps = [], - isInheritPermission, - hasParent, - addPermissionOnly + defaultRole }: MemberManagerInputPropsType & { children: (props: ChildrenProps) => ReactNode; refetchResource?: () => void; @@ -105,23 +108,31 @@ const CollaboratorContextProvider = ({ const { feConfigs } = useSystemStore(); const { - data: collaboratorList = [], + data: { clbs: collaboratorList = [], parentClbs: parentClbList = [] } = { + clbs: [], + parentClbs: [] + }, runAsync: refetchCollaboratorList, loading: isFetchingCollaborator } = useRequest2( async () => { if (feConfigs.isPlus) { - const data = await onGetCollaboratorList(); - return data.map((item) => { - return { - ...item, - permission: new Permission({ - role: item.permission.role - }) - }; - }); + const { clbs, parentClbs = [] } = await onGetCollaboratorList(); + return { + clbs: clbs.map((clb) => ({ + ...clb, + permission: new Permission({ role: clb.permission.role }) + })), + parentClbs: parentClbs.map((clb) => ({ + ...clb, + permission: new Permission({ role: clb.permission.role }) + })) + }; } - return []; + return { + clbs: [], + parentClbs: [] + }; }, { manual: false, @@ -160,18 +171,22 @@ const CollaboratorContextProvider = ({ [roleList] ); - const { ConfirmModal, openConfirm } = useConfirm({}); - const { - isOpen: isOpenAddMember, - onOpen: onOpenAddMember, - onClose: onCloseAddMember - } = useDisclosure(); const { isOpen: isOpenManageModal, onOpen: onOpenManageModal, onClose: onCloseManageModal } = useDisclosure(); + const { userInfo } = useUserStore(); + const myRole = useMemo(() => { + return ( + collaboratorList.find((v) => v.tmbId === userInfo?.team?.tmbId)?.permission ?? + new Permission({ + isOwner: userInfo?.team.permission.isOwner + }) + ); + }, [collaboratorList, userInfo?.team.permission.isOwner, userInfo?.team?.tmbId]); + const contextValue = { permission, onGetCollaboratorList, @@ -181,60 +196,26 @@ const CollaboratorContextProvider = ({ roleList, onUpdateCollaborators: onUpdateCollaboratorsThen, onDelOneCollaborator: onDelOneCollaboratorThen, - getRoleLabelList + getRoleLabelList, + defaultRole, + parentClbList, + myRole }; - const onOpenAddMemberModal = () => { - if (isInheritPermission && hasParent) { - openConfirm( - () => { - onOpenAddMember(); - }, - undefined, - t('common:permission.Remove InheritPermission Confirm') - )(); - } else { - onOpenAddMember(); - } - }; - const onOpenManageModalModal = () => { - if (isInheritPermission && hasParent) { - openConfirm( - () => { - onOpenManageModal(); - }, - undefined, - t('common:permission.Remove InheritPermission Confirm') - )(); - } else { - onOpenManageModal(); - } - }; return ( {children({ - onOpenAddMember: onOpenAddMemberModal, - onOpenManageModal: onOpenManageModalModal, + onOpenManageModal, MemberListCard })} - {isOpenAddMember && ( - { - onCloseAddMember(); - refetchResource?.(); - }} - addPermissionOnly={addPermissionOnly} - /> - )} {isOpenManageModal && ( - { onCloseManageModal(); refetchResource?.(); }} /> )} - ); }; diff --git a/projects/app/src/pageComponents/account/team/Audit/index.tsx b/projects/app/src/pageComponents/account/team/Audit/index.tsx index 3174790ca9b9..8870552ff01d 100644 --- a/projects/app/src/pageComponents/account/team/Audit/index.tsx +++ b/projects/app/src/pageComponents/account/team/Audit/index.tsx @@ -72,7 +72,7 @@ function AuditLog({ Tabs }: { Tabs: React.ReactNode }) { isLoading: loadingLogs, ScrollData: LogScrollData } = useScrollPagination(getOperationLogs, { - pageSize: 20, + pageSize: 30, refreshDeps: [searchParams], params: searchParams }); diff --git a/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx b/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx index 3feef0e7d0dc..fe6ece087dc8 100644 --- a/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx +++ b/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx @@ -19,7 +19,8 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { deleteMemberPermission, getTeamClbs, - updateMemberPermission + updateMemberPermission, + updateOneMemberPermission } from '@/web/support/user/team/api'; import { useUserStore } from '@/web/support/user/useUserStore'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; @@ -51,6 +52,7 @@ import { GetSearchUserGroupOrg } from '@/web/support/user/api'; import { type PermissionValueType } from '@fastgpt/global/support/permission/type'; import { type CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator'; import type { Permission } from '@fastgpt/global/support/permission/controller'; +import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; function PermissionManage({ Tabs, @@ -66,14 +68,14 @@ function PermissionManage({ CollaboratorContext, (state) => state.collaboratorList ); - const onUpdateCollaborators = useContextSelector( - CollaboratorContext, - (state) => state.onUpdateCollaborators - ); const onDelOneCollaborator = useContextSelector( CollaboratorContext, (state) => state.onDelOneCollaborator ); + const refetchCollaborators = useContextSelector( + CollaboratorContext, + (state) => state.refetchCollaboratorList + ); const [isExpandMember, setExpandMember] = useToggle(true); const [isExpandGroup, setExpandGroup] = useToggle(true); @@ -127,12 +129,15 @@ function PermissionManage({ permission.removeRole(per); } - return onUpdateCollaborators({ - ...(clb.tmbId && { members: [clb.tmbId] }), - ...(clb.groupId && { groups: [clb.groupId] }), - ...(clb.orgId && { orgs: [clb.orgId] }), + return updateOneMemberPermission({ + tmbId: clb.tmbId, + groupId: clb.groupId, + orgId: clb.orgId, permission: permission.role }); + }, + { + onSuccess: refetchCollaborators } ); @@ -268,25 +273,25 @@ function PermissionManage({ { return userInfo?.team ? ( { refreshDeps={[userInfo?.team.teamId]} addPermissionOnly={true} > - {({ onOpenAddMember }) => } + {({ onOpenManageModal }) => ( + + )} ) : null; }; diff --git a/projects/app/src/pageComponents/app/detail/InfoModal.tsx b/projects/app/src/pageComponents/app/detail/InfoModal.tsx index c1ca2cd69017..688324737296 100644 --- a/projects/app/src/pageComponents/app/detail/InfoModal.tsx +++ b/projects/app/src/pageComponents/app/detail/InfoModal.tsx @@ -31,6 +31,8 @@ import { useTranslation } from 'next-i18next'; import React, { useCallback } from 'react'; import { useForm } from 'react-hook-form'; import { useContextSelector } from 'use-context-selector'; +import { postUpdateDatasetCollaborators } from '@/web/core/dataset/api/collaborator'; +import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; const InfoModal = ({ onClose }: { onClose: () => void }) => { const { t } = useTranslation(); @@ -100,25 +102,6 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => { [handleSubmit, onClose, saveSubmitError, saveSubmitSuccess] ); - const onUpdateCollaborators = ({ - members, - groups, - orgs, - permission - }: { - members?: string[]; - groups?: string[]; - orgs?: string[]; - permission: PermissionValueType; - }) => - postUpdateAppCollaborators({ - members, - groups, - permission, - orgs, - appId: appDetail._id - }); - const onDelCollaborator = async ( props: RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string }> ) => @@ -185,15 +168,14 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => { )} getCollaboratorList(appDetail._id)} roleList={AppRoleList} - onUpdateCollaborators={async (props) => - onUpdateCollaborators({ - permission: props.permission, - members: props.members, - groups: props.groups, - orgs: props.orgs + onUpdateCollaborators={async ({ collaborators }) => + postUpdateDatasetCollaborators({ + collaborators, + datasetId: appDetail._id }) } onDelOneCollaborator={onDelCollaborator} @@ -201,7 +183,7 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => { isInheritPermission={appDetail.inheritPermission} hasParent={!!appDetail.parentId} > - {({ MemberListCard, onOpenManageModal, onOpenAddMember }) => { + {({ MemberListCard, onOpenManageModal }) => { return ( <> void }) => { w="full" > {t('common:permission.Collaborator')} - - - - + diff --git a/projects/app/src/pageComponents/dashboard/apps/List.tsx b/projects/app/src/pageComponents/dashboard/apps/List.tsx index 43b9cc32351a..8bf672eb966c 100644 --- a/projects/app/src/pageComponents/dashboard/apps/List.tsx +++ b/projects/app/src/pageComponents/dashboard/apps/List.tsx @@ -17,7 +17,7 @@ import { useFolderDrag } from '@/components/common/folder/useFolderDrag'; import dynamic from 'next/dynamic'; import type { EditResourceInfoFormType } from '@/components/common/Modal/EditResourceModal'; import MyMenu, { type MenuItemType } from '@fastgpt/web/components/common/MyMenu'; -import { AppRoleList } from '@fastgpt/global/support/permission/app/constant'; +import { AppDefaultRoleVal, AppRoleList } from '@fastgpt/global/support/permission/app/constant'; import { deleteAppCollaborators, getCollaboratorList, @@ -38,6 +38,7 @@ import { type RequireOnlyOne } from '@fastgpt/global/common/type/utils'; import UserBox from '@fastgpt/web/components/common/UserBox'; import { type PermissionValueType } from '@fastgpt/global/support/permission/type'; import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants'; +import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; const HttpEditModal = dynamic(() => import('./HttpPluginEditModal')); const ListItem = () => { @@ -435,15 +436,11 @@ const ListItem = () => { avatar={editPerApp.avatar} name={editPerApp.name} managePer={{ + defaultRole: ReadRoleVal, permission: editPerApp.permission, onGetCollaboratorList: () => getCollaboratorList(editPerApp._id), roleList: AppRoleList, - onUpdateCollaborators: (props: { - members?: string[]; - groups?: string[]; - orgs?: string[]; - permission: PermissionValueType; - }) => + onUpdateCollaborators: (props) => postUpdateAppCollaborators({ ...props, appId: editPerApp._id diff --git a/projects/app/src/pageComponents/dataset/MemberManager.tsx b/projects/app/src/pageComponents/dataset/MemberManager.tsx index e2e367bfe4e5..3f0a7f3f02d0 100644 --- a/projects/app/src/pageComponents/dataset/MemberManager.tsx +++ b/projects/app/src/pageComponents/dataset/MemberManager.tsx @@ -11,7 +11,7 @@ function MemberManager({ managePer }: { managePer: MemberManagerInputPropsType } return ( - {({ MemberListCard, onOpenManageModal, onOpenAddMember }) => { + {({ MemberListCard, onOpenManageModal }) => { return ( <> @@ -30,17 +30,6 @@ function MemberManager({ managePer }: { managePer: MemberManagerInputPropsType } _hover={{ color: 'primary.500' }} /> - - - diff --git a/projects/app/src/pageComponents/dataset/detail/Info/index.tsx b/projects/app/src/pageComponents/dataset/detail/Info/index.tsx index 064a63220ba7..15aabe15c9a5 100644 --- a/projects/app/src/pageComponents/dataset/detail/Info/index.tsx +++ b/projects/app/src/pageComponents/dataset/detail/Info/index.tsx @@ -29,6 +29,7 @@ import dynamic from 'next/dynamic'; import type { EditAPIDatasetInfoFormType } from './components/EditApiServiceModal'; import { type EditResourceInfoFormType } from '@/components/common/Modal/EditResourceModal'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; +import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal')); const EditAPIDatasetInfoModal = dynamic(() => import('./components/EditApiServiceModal')); @@ -380,6 +381,7 @@ const Info = ({ datasetId }: { datasetId: string }) => { getCollaboratorList(datasetId), roleList: DatasetRoleList, diff --git a/projects/app/src/pageComponents/dataset/list/List.tsx b/projects/app/src/pageComponents/dataset/list/List.tsx index eaac380819d3..96844b4de86d 100644 --- a/projects/app/src/pageComponents/dataset/list/List.tsx +++ b/projects/app/src/pageComponents/dataset/list/List.tsx @@ -32,6 +32,7 @@ import { useSystem } from '@fastgpt/web/hooks/useSystem'; import SideTag from './SideTag'; import { getModelProvider } from '@fastgpt/global/core/ai/provider'; import UserBox from '@fastgpt/web/components/common/UserBox'; +import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal')); @@ -55,7 +56,7 @@ function List() { const router = useRouter(); const { parentId = null } = router.query as { parentId?: string | null }; const parentDataset = useMemo( - () => myDatasets.find((item) => String(item._id) === parentId), + () => myDatasets.find((item) => item._id === parentId), [parentId, myDatasets] ); @@ -82,7 +83,7 @@ function List() { }); const editPerDataset = useMemo( - () => myDatasets.find((item) => String(item._id) === String(editPerDatasetId)), + () => myDatasets.find((item) => item._id === editPerDatasetId), [editPerDatasetId, myDatasets] ); @@ -434,6 +435,7 @@ function List() { avatar={editPerDataset.avatar} name={editPerDataset.name} managePer={{ + defaultRole: ReadRoleVal, permission: editPerDataset.permission, onGetCollaboratorList: () => getCollaboratorList(editPerDataset._id), roleList: DatasetRoleList, diff --git a/projects/app/src/pages/api/common/file/read.ts b/projects/app/src/pages/api/common/file/read.ts index 0f84b5e955a0..c81a4b0def0b 100644 --- a/projects/app/src/pages/api/common/file/read.ts +++ b/projects/app/src/pages/api/common/file/read.ts @@ -1,9 +1,9 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { jsonRes } from '@fastgpt/service/common/response'; -import { authFileToken } from '@fastgpt/service/support/permission/controller'; import { getDownloadStream, getFileById } from '@fastgpt/service/common/file/gridfs/controller'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { stream2Encoding } from '@fastgpt/service/common/file/gridfs/utils'; +import { authFileToken } from '@fastgpt/service/support/permission/auth/file'; const previewableExtensions = [ 'jpg', diff --git a/projects/app/src/pages/api/common/file/read/[filename].ts b/projects/app/src/pages/api/common/file/read/[filename].ts index ba715fbe9398..fbd9d20b5879 100644 --- a/projects/app/src/pages/api/common/file/read/[filename].ts +++ b/projects/app/src/pages/api/common/file/read/[filename].ts @@ -1,9 +1,9 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { jsonRes } from '@fastgpt/service/common/response'; -import { authFileToken } from '@fastgpt/service/support/permission/controller'; import { getDownloadStream, getFileById } from '@fastgpt/service/common/file/gridfs/controller'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { stream2Encoding } from '@fastgpt/service/common/file/gridfs/utils'; +import { authFileToken } from '@fastgpt/service/support/permission/auth/file'; const previewableExtensions = [ 'jpg', diff --git a/projects/app/src/pages/api/common/file/upload.ts b/projects/app/src/pages/api/common/file/upload.ts index 63340f78488b..e0845a2b92b3 100644 --- a/projects/app/src/pages/api/common/file/upload.ts +++ b/projects/app/src/pages/api/common/file/upload.ts @@ -4,7 +4,6 @@ import { uploadFile } from '@fastgpt/service/common/file/gridfs/controller'; import { getUploadModel } from '@fastgpt/service/common/file/multer'; import { removeFilesByPaths } from '@fastgpt/service/common/file/utils'; import { NextAPI } from '@/service/middleware/entry'; -import { createFileToken } from '@fastgpt/service/support/permission/controller'; import { ReadFileBaseUrl } from '@fastgpt/global/common/file/constants'; import { addLog } from '@fastgpt/service/common/system/log'; import { authFrequencyLimit } from '@/service/common/frequencyLimit/api'; @@ -13,6 +12,7 @@ import { authChatCrud } from '@/service/support/permission/auth/chat'; import { authDataset } from '@fastgpt/service/support/permission/dataset/auth'; import { type OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; +import { createFileToken } from '@fastgpt/service/support/permission/auth/file'; export type UploadChatFileProps = { appId: string; diff --git a/projects/app/src/pages/api/core/app/create.ts b/projects/app/src/pages/api/core/app/create.ts index bcfdb9f58588..dda285c0cc90 100644 --- a/projects/app/src/pages/api/core/app/create.ts +++ b/projects/app/src/pages/api/core/app/create.ts @@ -6,7 +6,10 @@ import type { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppFolderTypeList } from '@fastgpt/global/core/app/constants'; import type { AppSchema } from '@fastgpt/global/core/app/type'; import { type ShortUrlParams } from '@fastgpt/global/support/marketing/type'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; +import { + PerResourceTypeEnum, + WritePermissionVal +} from '@fastgpt/global/support/permission/constant'; import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller'; import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils'; @@ -22,6 +25,7 @@ import { type ApiRequestProps } from '@fastgpt/service/type/next'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { getI18nAppType } from '@fastgpt/service/support/user/audit/util'; +import { createResourceDefaultCollaborators } from '@fastgpt/service/support/permission/controller'; export type CreateAppBody = { parentId?: ParentIdType; @@ -113,7 +117,7 @@ export const onCreateApp = async ({ session?: ClientSession; }) => { const create = async (session: ClientSession) => { - const [{ _id: appId }] = await MongoApp.create( + const [app] = await MongoApp.create( [ { ...parseParentIdInMongo(parentId), @@ -133,6 +137,8 @@ export const onCreateApp = async ({ { session, ordered: true } ); + const appId = app._id; + if (!AppFolderTypeList.includes(type!)) { await MongoAppVersion.create( [ @@ -151,6 +157,13 @@ export const onCreateApp = async ({ { session, ordered: true } ); } + + await createResourceDefaultCollaborators({ + tmbId, + session, + resource: app, + resourceType: PerResourceTypeEnum.app + }); (async () => { addAuditLog({ tmbId, diff --git a/projects/app/src/pages/api/core/app/folder/create.ts b/projects/app/src/pages/api/core/app/folder/create.ts index 88ff0d7e9b1c..b986a7b19812 100644 --- a/projects/app/src/pages/api/core/app/folder/create.ts +++ b/projects/app/src/pages/api/core/app/folder/create.ts @@ -3,9 +3,8 @@ import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { FolderImgUrl } from '@fastgpt/global/common/file/image/constants'; import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'; -import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; +import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { - OwnerPermissionVal, PerResourceTypeEnum, WritePermissionVal } from '@fastgpt/global/support/permission/constant'; @@ -13,9 +12,7 @@ import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/u import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { MongoApp } from '@fastgpt/service/core/app/schema'; import { authApp } from '@fastgpt/service/support/permission/app/auth'; -import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller'; -import { syncCollaborators } from '@fastgpt/service/support/permission/inheritPermission'; -import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; +import { createResourceDefaultCollaborators } from '@fastgpt/service/support/permission/controller'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { type ApiRequestProps } from '@fastgpt/service/type/next'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; @@ -50,39 +47,12 @@ async function handler(req: ApiRequestProps) { type: AppTypeEnum.folder }); - if (parentId) { - const parentClbsAndGroups = await getResourceClbsAndGroups({ - teamId, - resourceId: parentId, - resourceType: PerResourceTypeEnum.app, - session - }); - - await syncCollaborators({ - resourceType: PerResourceTypeEnum.app, - teamId, - resourceId: app._id, - collaborators: parentClbsAndGroups, - session - }); - } else { - // Create default permission - await MongoResourcePermission.create( - [ - { - resourceType: PerResourceTypeEnum.app, - teamId, - resourceId: app._id, - tmbId, - permission: OwnerPermissionVal - } - ], - { - session, - ordered: true - } - ); - } + await createResourceDefaultCollaborators({ + tmbId, + session, + resource: app, + resourceType: PerResourceTypeEnum.app + }); }); (async () => { addAuditLog({ diff --git a/projects/app/src/pages/api/core/app/update.ts b/projects/app/src/pages/api/core/app/update.ts index a58f8f86f3ce..7be66b30dd93 100644 --- a/projects/app/src/pages/api/core/app/update.ts +++ b/projects/app/src/pages/api/core/app/update.ts @@ -18,7 +18,10 @@ import { import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { type ClientSession } from 'mongoose'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; -import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller'; +import { + getResourceClbs, + getResourceOwnedClbs +} from '@fastgpt/service/support/permission/controller'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; import { AppErrEnum } from '@fastgpt/global/common/error/code/app'; @@ -150,38 +153,30 @@ async function handler(req: ApiRequestProps) { if (isMove) { await mongoSessionRun(async (session) => { // Inherit folder: Sync children permission and it's clbs - if (AppFolderTypeList.includes(app.type)) { - const parentClbsAndGroups = await getResourceClbsAndGroups({ - teamId: app.teamId, - resourceId: parentId, - resourceType: PerResourceTypeEnum.app, - session - }); - // sync self - await syncCollaborators({ - resourceId: app._id, - resourceType: PerResourceTypeEnum.app, - collaborators: parentClbsAndGroups, - session, - teamId: app.teamId - }); - // sync the children - await syncChildrenPermission({ - resource: app, - resourceType: PerResourceTypeEnum.app, - resourceModel: MongoApp, - folderTypeList: AppFolderTypeList, - collaborators: parentClbsAndGroups, - session - }); - } else { - logAppMove({ tmbId, teamId, app, targetName }); - // Not folder, delete all clb - await MongoResourcePermission.deleteMany( - { resourceType: PerResourceTypeEnum.app, teamId: app.teamId, resourceId: app._id }, - { session } - ); - } + const parentClbs = await getResourceOwnedClbs({ + teamId: app.teamId, + resourceId: parentId, + resourceType: PerResourceTypeEnum.app, + session + }); + // sync self + await syncCollaborators({ + resourceId: app._id, + resourceType: PerResourceTypeEnum.app, + collaborators: parentClbs, + session, + teamId: app.teamId + }); + // sync the children + await syncChildrenPermission({ + resource: app, + resourceType: PerResourceTypeEnum.app, + resourceModel: MongoApp, + folderTypeList: AppFolderTypeList, + collaborators: parentClbs, + session + }); + logAppMove({ tmbId, teamId, app, targetName }); return onUpdate(session); }); } else { diff --git a/projects/app/src/pages/api/core/dataset/collection/read.ts b/projects/app/src/pages/api/core/dataset/collection/read.ts index 19935b3c2c2f..ed510848a04f 100644 --- a/projects/app/src/pages/api/core/dataset/collection/read.ts +++ b/projects/app/src/pages/api/core/dataset/collection/read.ts @@ -2,7 +2,6 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; import { authDatasetCollection } from '@fastgpt/service/support/permission/dataset/auth'; import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constants'; -import { createFileToken } from '@fastgpt/service/support/permission/controller'; import { BucketNameEnum, ReadFileBaseUrl } from '@fastgpt/global/common/file/constants'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { type OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; @@ -10,6 +9,7 @@ import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset'; import { authChatCrud, authCollectionInChat } from '@/service/support/permission/auth/chat'; import { getCollectionWithDataset } from '@fastgpt/service/core/dataset/controller'; import { getApiDatasetRequest } from '@fastgpt/service/core/dataset/apiDataset'; +import { createFileToken } from '@fastgpt/service/support/permission/auth/file'; export type readCollectionSourceQuery = {}; @@ -48,7 +48,7 @@ async function handler( }); } - /* + /* 1. auth chat read permission 2. auth collection quote in chat 3. auth outlink open show quote diff --git a/projects/app/src/pages/api/core/dataset/create.ts b/projects/app/src/pages/api/core/dataset/create.ts index 71ec9b53a9f0..0fae1212e3f7 100644 --- a/projects/app/src/pages/api/core/dataset/create.ts +++ b/projects/app/src/pages/api/core/dataset/create.ts @@ -2,7 +2,10 @@ import type { CreateDatasetParams } from '@/global/core/dataset/api.d'; import { NextAPI } from '@/service/middleware/entry'; import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'; import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; +import { + PerResourceTypeEnum, + WritePermissionVal +} from '@fastgpt/global/support/permission/constant'; import { TeamDatasetCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller'; import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils'; @@ -21,6 +24,7 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { getI18nDatasetType } from '@fastgpt/service/support/user/audit/util'; +import { createResourceDefaultCollaborators } from '@fastgpt/service/support/permission/controller'; export type DatasetCreateQuery = {}; export type DatasetCreateBody = CreateDatasetParams; @@ -71,7 +75,7 @@ async function handler( await checkTeamDatasetLimit(teamId); const datasetId = await mongoSessionRun(async (session) => { - const [{ _id }] = await MongoDataset.create( + const [dataset] = await MongoDataset.create( [ { ...parseParentIdInMongo(parentId), @@ -89,9 +93,17 @@ async function handler( ], { session, ordered: true } ); + + await createResourceDefaultCollaborators({ + tmbId, + session, + resource: dataset, + resourceType: PerResourceTypeEnum.dataset + }); + await refreshSourceAvatar(avatar, undefined, session); - return _id; + return dataset._id; }); pushTrack.createDataset({ diff --git a/projects/app/src/pages/api/core/dataset/folder/create.ts b/projects/app/src/pages/api/core/dataset/folder/create.ts index 3bb34ad2252e..426db0a7f864 100644 --- a/projects/app/src/pages/api/core/dataset/folder/create.ts +++ b/projects/app/src/pages/api/core/dataset/folder/create.ts @@ -4,17 +4,14 @@ import { FolderImgUrl } from '@fastgpt/global/common/file/image/constants'; import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'; import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { - OwnerPermissionVal, PerResourceTypeEnum, WritePermissionVal } from '@fastgpt/global/support/permission/constant'; import { TeamDatasetCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { MongoDataset } from '@fastgpt/service/core/dataset/schema'; -import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller'; +import { createResourceDefaultCollaborators } from '@fastgpt/service/support/permission/controller'; import { authDataset } from '@fastgpt/service/support/permission/dataset/auth'; -import { syncCollaborators } from '@fastgpt/service/support/permission/inheritPermission'; -import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; @@ -62,37 +59,12 @@ async function handler( type: DatasetTypeEnum.folder }); - if (parentId) { - const parentClbsAndGroups = await getResourceClbsAndGroups({ - teamId, - resourceId: parentId, - resourceType: PerResourceTypeEnum.dataset, - session - }); - - await syncCollaborators({ - resourceType: PerResourceTypeEnum.dataset, - teamId, - resourceId: dataset._id, - collaborators: parentClbsAndGroups, - session - }); - } - - if (!parentId) { - await MongoResourcePermission.create( - [ - { - resourceType: PerResourceTypeEnum.dataset, - teamId, - resourceId: dataset._id, - tmbId, - permission: OwnerPermissionVal - } - ], - { session, ordered: true } - ); - } + await createResourceDefaultCollaborators({ + tmbId, + session, + resource: dataset, + resourceType: PerResourceTypeEnum.dataset + }); }); (async () => { addAuditLog({ diff --git a/projects/app/src/pages/api/core/dataset/update.ts b/projects/app/src/pages/api/core/dataset/update.ts index 0d679c4a09c3..25eba406d1e7 100644 --- a/projects/app/src/pages/api/core/dataset/update.ts +++ b/projects/app/src/pages/api/core/dataset/update.ts @@ -17,7 +17,6 @@ import { import { type ClientSession } from 'mongoose'; import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; -import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller'; import { syncChildrenPermission, syncCollaborators @@ -26,10 +25,7 @@ import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { TeamDatasetCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset'; import { MongoDatasetTraining } from '@fastgpt/service/core/dataset/training/schema'; -import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema'; -import { addDays } from 'date-fns'; import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller'; -import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; import { type DatasetSchemaType } from '@fastgpt/global/core/dataset/type'; import { removeDatasetSyncJobScheduler, @@ -42,6 +38,10 @@ import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { getI18nDatasetType } from '@fastgpt/service/support/user/audit/util'; import { getEmbeddingModel, getLLMModel } from '@fastgpt/service/core/ai/model'; import { computedCollectionChunkSettings } from '@fastgpt/global/core/dataset/training/utils'; +import { + getResourceClbs, + getResourceOwnedClbs +} from '@fastgpt/service/support/permission/controller'; export type DatasetUpdateQuery = {}; export type DatasetUpdateResponse = any; @@ -233,39 +233,30 @@ async function handler( await mongoSessionRun(async (session) => { if (isMove) { - if (isFolder && dataset.inheritPermission) { - const parentClbsAndGroups = await getResourceClbsAndGroups({ - teamId: dataset.teamId, - resourceId: parentId, - resourceType: PerResourceTypeEnum.dataset, - session - }); + const parentClbs = await getResourceOwnedClbs({ + teamId: dataset.teamId, + resourceId: parentId, + resourceType: PerResourceTypeEnum.dataset, + session + }); - await syncCollaborators({ - teamId: dataset.teamId, - resourceId: id, - resourceType: PerResourceTypeEnum.dataset, - collaborators: parentClbsAndGroups, - session - }); + await syncCollaborators({ + teamId: dataset.teamId, + resourceId: id, + resourceType: PerResourceTypeEnum.dataset, + collaborators: parentClbs, + session + }); - await syncChildrenPermission({ - resource: dataset, - resourceType: PerResourceTypeEnum.dataset, - resourceModel: MongoDataset, - folderTypeList: [DatasetTypeEnum.folder], - collaborators: parentClbsAndGroups, - session - }); - logDatasetMove({ tmbId, teamId, dataset, targetName }); - } else { - logDatasetMove({ tmbId, teamId, dataset, targetName }); - // Not folder, delete all clb - await MongoResourcePermission.deleteMany( - { resourceId: id, teamId: dataset.teamId, resourceType: PerResourceTypeEnum.dataset }, - { session } - ); - } + await syncChildrenPermission({ + resource: dataset, + resourceType: PerResourceTypeEnum.dataset, + resourceModel: MongoDataset, + folderTypeList: [DatasetTypeEnum.folder], + collaborators: parentClbs, + session + }); + logDatasetMove({ tmbId, teamId, dataset, targetName }); return onUpdate(session); } else { logDatasetUpdate({ tmbId, teamId, dataset }); diff --git a/projects/app/src/pages/api/support/user/account/loginByPassword.ts b/projects/app/src/pages/api/support/user/account/loginByPassword.ts index deaa82583435..d2b11d9faf53 100644 --- a/projects/app/src/pages/api/support/user/account/loginByPassword.ts +++ b/projects/app/src/pages/api/support/user/account/loginByPassword.ts @@ -1,6 +1,5 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { MongoUser } from '@fastgpt/service/support/user/schema'; -import { setCookie } from '@fastgpt/service/support/permission/controller'; import { getUserDetail } from '@fastgpt/service/support/user/controller'; import type { PostLoginProps } from '@fastgpt/global/support/user/api.d'; import { UserStatusEnum } from '@fastgpt/global/support/user/constant'; @@ -15,6 +14,7 @@ import { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants'; import { authCode } from '@fastgpt/service/support/user/auth/controller'; import { createUserSession } from '@fastgpt/service/support/user/session'; import requestIp from 'request-ip'; +import { setCookie } from '@fastgpt/service/support/permission/auth/common'; async function handler(req: NextApiRequest, res: NextApiResponse) { const { username, password, code } = req.body as PostLoginProps; diff --git a/projects/app/src/pages/api/support/user/account/loginout.ts b/projects/app/src/pages/api/support/user/account/loginout.ts index 423c5a49bde9..5e86d3c66ba1 100644 --- a/projects/app/src/pages/api/support/user/account/loginout.ts +++ b/projects/app/src/pages/api/support/user/account/loginout.ts @@ -1,7 +1,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; -import { clearCookie } from '@fastgpt/service/support/permission/controller'; import { NextAPI } from '@/service/middleware/entry'; -import { authCert } from '@fastgpt/service/support/permission/auth/common'; +import { authCert, clearCookie } from '@fastgpt/service/support/permission/auth/common'; import { delUserAllSession } from '@fastgpt/service/support/user/session'; async function handler(req: NextApiRequest, res: NextApiResponse) { diff --git a/projects/app/src/pages/api/support/user/account/updatePasswordByOld.ts b/projects/app/src/pages/api/support/user/account/updatePasswordByOld.ts index 19052a30286c..f0b1da3f7d91 100644 --- a/projects/app/src/pages/api/support/user/account/updatePasswordByOld.ts +++ b/projects/app/src/pages/api/support/user/account/updatePasswordByOld.ts @@ -8,7 +8,6 @@ import { NextAPI } from '@/service/middleware/entry'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { delUserAllSession } from '@fastgpt/service/support/user/session'; -import { parseHeaderCert } from '@fastgpt/service/support/permission/controller'; async function handler(req: NextApiRequest, res: NextApiResponse) { const { oldPsw, newPsw } = req.body as { oldPsw: string; newPsw: string }; diff --git a/projects/app/src/pages/dashboard/apps/index.tsx b/projects/app/src/pages/dashboard/apps/index.tsx index 3079ed7df9fc..c6ebbcd620e4 100644 --- a/projects/app/src/pages/dashboard/apps/index.tsx +++ b/projects/app/src/pages/dashboard/apps/index.tsx @@ -34,6 +34,7 @@ import MCPToolsEditModal from '@/pageComponents/dashboard/apps/MCPToolsEditModal import { getUtmWorkflow } from '@/web/support/marketing/utils'; import { useMount } from 'ahooks'; import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; +import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; const CreateModal = dynamic(() => import('@/pageComponents/dashboard/apps/CreateModal')); const EditFolderModal = dynamic( @@ -282,6 +283,7 @@ const MyApps = ({ MenuIcon }: { MenuIcon: JSX.Element }) => { deleteTip={t('app:confirm_delete_folder_tip')} onDelete={() => onDeleFolder(folderDetail._id)} managePer={{ + defaultRole: ReadRoleVal, permission: folderDetail.permission, onGetCollaboratorList: () => getCollaboratorList(folderDetail._id), roleList: AppRoleList, diff --git a/projects/app/src/pages/dataset/list/index.tsx b/projects/app/src/pages/dataset/list/index.tsx index 4b8b5759e776..dedb5f17f8fb 100644 --- a/projects/app/src/pages/dataset/list/index.tsx +++ b/projects/app/src/pages/dataset/list/index.tsx @@ -30,6 +30,7 @@ import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { useToast } from '@fastgpt/web/hooks/useToast'; import MyBox from '@fastgpt/web/components/common/MyBox'; import { useSystemStore } from '@/web/common/system/useSystemStore'; +import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; const EditFolderModal = dynamic( () => import('@fastgpt/web/components/common/MyModal/EditFolderModal') @@ -254,6 +255,7 @@ const Dataset = () => { }) } managePer={{ + defaultRole: ReadRoleVal, permission: folderDetail.permission, onGetCollaboratorList: () => getCollaboratorList(folderDetail._id), roleList: DatasetRoleList, diff --git a/projects/app/src/service/core/app/utils.ts b/projects/app/src/service/core/app/utils.ts index 2d7ba601d8df..228365865e43 100644 --- a/projects/app/src/service/core/app/utils.ts +++ b/projects/app/src/service/core/app/utils.ts @@ -24,6 +24,11 @@ import { type UserChatItemValueItemType } from '@fastgpt/global/core/chat/type'; import { saveChat } from '@fastgpt/service/core/chat/saveChat'; import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller'; import { getErrText } from '@fastgpt/global/common/error/utils'; +import { AppItemType } from '@/types/app'; +import { AppSchema } from '@fastgpt/global/core/app/type'; +import { getResourceClbs } from '@fastgpt/service/support/permission/controller'; +import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; +import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; export const getScheduleTriggerApp = async () => { addLog.info('Schedule trigger app'); diff --git a/projects/app/src/web/core/app/api/collaborator.ts b/projects/app/src/web/core/app/api/collaborator.ts index fda0564c33a2..27b6ff33cfe3 100644 --- a/projects/app/src/web/core/app/api/collaborator.ts +++ b/projects/app/src/web/core/app/api/collaborator.ts @@ -3,10 +3,10 @@ import type { AppCollaboratorDeleteParams } from '@fastgpt/global/core/app/collaborator'; import { DELETE, GET, POST } from '@/web/common/api/request'; -import type { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator'; +import type { CollaboratorListType } from '@fastgpt/global/support/permission/collaborator'; export const getCollaboratorList = (appId: string) => - GET('/proApi/core/app/collaborator/list', { appId }); + GET('/proApi/core/app/collaborator/list', { appId }); export const postUpdateAppCollaborators = (body: UpdateAppCollaboratorBody) => POST('/proApi/core/app/collaborator/update', body); diff --git a/projects/app/src/web/core/dataset/api/collaborator.ts b/projects/app/src/web/core/dataset/api/collaborator.ts index 6c69d73b50c7..6a8336805cf8 100644 --- a/projects/app/src/web/core/dataset/api/collaborator.ts +++ b/projects/app/src/web/core/dataset/api/collaborator.ts @@ -3,10 +3,10 @@ import type { DatasetCollaboratorDeleteParams } from '@fastgpt/global/core/dataset/collaborator'; import { DELETE, GET, POST } from '@/web/common/api/request'; -import type { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator'; +import type { CollaboratorListType } from '@fastgpt/global/support/permission/collaborator'; export const getCollaboratorList = (datasetId: string) => - GET('/proApi/core/dataset/collaborator/list', { datasetId }); + GET('/proApi/core/dataset/collaborator/list', { datasetId }); export const postUpdateDatasetCollaborators = (body: UpdateDatasetCollaboratorBody) => POST('/proApi/core/dataset/collaborator/update', body); diff --git a/projects/app/src/web/support/user/team/api.ts b/projects/app/src/web/support/user/team/api.ts index 5231f1204a5b..5a07ebda02ba 100644 --- a/projects/app/src/web/support/user/team/api.ts +++ b/projects/app/src/web/support/user/team/api.ts @@ -1,6 +1,7 @@ import { GET, POST, PUT, DELETE } from '@/web/common/api/request'; import type { CollaboratorItemType, + CollaboratorListType, DeletePermissionQuery, UpdateClbPermissionProps } from '@fastgpt/global/support/permission/collaborator'; @@ -26,6 +27,7 @@ import type { InvitationLinkCreateType, InvitationType } from '@fastgpt/service/support/user/team/invitationLink/type'; +import type { PermissionValueType } from '@fastgpt/global/support/permission/type'; /* --------------- team ---------------- */ export const getTeamList = (status: `${TeamMemberSchema['status']}`) => @@ -83,9 +85,15 @@ export const putForbidInvitationLink = (linkId: string) => /* -------------- team collaborator -------------------- */ export const getTeamClbs = () => - GET(`/proApi/support/user/team/collaborator/list`); + GET(`/proApi/support/user/team/collaborator/list`); export const updateMemberPermission = (data: UpdateClbPermissionProps) => - PUT('/proApi/support/user/team/collaborator/update', data); + POST('/proApi/support/user/team/collaborator/update', data); +export const updateOneMemberPermission = (data: { + tmbId?: string; + orgId?: string; + groupId?: string; + permission: PermissionValueType; +}) => PUT('/proApi/support/user/team/collaborator/updateOne', data); export const deleteMemberPermission = (id: DeletePermissionQuery) => DELETE('/proApi/support/user/team/collaborator/delete', id); diff --git a/projects/app/src/web/support/user/team/group/api.ts b/projects/app/src/web/support/user/team/group/api.ts index 9a2975e8328d..aba4f93feee8 100644 --- a/projects/app/src/web/support/user/team/group/api.ts +++ b/projects/app/src/web/support/user/team/group/api.ts @@ -10,11 +10,7 @@ import type { } from '@fastgpt/global/support/user/team/group/api'; export const getGroupList = (data: GetGroupListBody) => - POST[]>('/proApi/support/user/team/group/list', data).then((res) => { - console.log(res); - return res; - }); - + POST[]>('/proApi/support/user/team/group/list', data); export const postCreateGroup = (data: postCreateGroupData) => POST('/proApi/support/user/team/group/create', data); diff --git a/projects/app/test/api/core/app/create.test.ts b/projects/app/test/api/core/app/create.test.ts index 0e470a1d9772..69ae40d7158b 100644 --- a/projects/app/test/api/core/app/create.test.ts +++ b/projects/app/test/api/core/app/create.test.ts @@ -11,13 +11,18 @@ import { describe, expect, it } from 'vitest'; describe('create api', () => { it('should return 200 when create app success', async () => { const users = await getFakeUsers(2); - await MongoResourcePermission.create({ - resourceType: 'team', - teamId: users.members[0].teamId, - resourceId: null, - tmbId: users.members[0].tmbId, - permission: TeamAppCreatePermissionVal - }); + await MongoResourcePermission.findOneAndUpdate( + { + resourceType: 'team', + teamId: users.members[0].teamId, + resourceId: null, + tmbId: users.members[0].tmbId + }, + { + permission: TeamAppCreatePermissionVal + }, + { upsert: true } + ); const res = await Call(createapi.default, { auth: users.members[0], @@ -56,13 +61,18 @@ describe('create api', () => { expect(res3.error).toBe(AppErrEnum.unAuthApp); expect(res3.code).toBe(500); - await MongoResourcePermission.create({ - resourceType: 'app', - teamId: users.members[1].teamId, - resourceId: String(folderId), - tmbId: users.members[1].tmbId, - permission: WritePermissionVal - }); + await MongoResourcePermission.findOneAndUpdate( + { + resourceType: 'app', + teamId: users.members[1].teamId, + resourceId: String(folderId), + tmbId: users.members[1].tmbId + }, + { + permission: WritePermissionVal + }, + { upsert: true } + ); const res4 = await Call(createapi.default, { auth: users.members[1], diff --git a/projects/app/test/api/core/dataset/training/getTrainingDataDetail.test.ts b/projects/app/test/api/core/dataset/training/getTrainingDataDetail.test.ts index d4a0b2793ec5..2c3f672bc29e 100644 --- a/projects/app/test/api/core/dataset/training/getTrainingDataDetail.test.ts +++ b/projects/app/test/api/core/dataset/training/getTrainingDataDetail.test.ts @@ -51,8 +51,8 @@ describe('get training data detail test', () => { expect(res.code).toBe(200); expect(res.data).toBeDefined(); - expect(res.data?._id).toStrictEqual(trainingData._id); - expect(res.data?.datasetId).toStrictEqual(dataset._id); + expect(res.data?._id).toStrictEqual(String(trainingData._id)); + expect(res.data?.datasetId).toStrictEqual(String(dataset._id)); expect(res.data?.mode).toBe(TrainingModeEnum.chunk); expect(res.data?.q).toBe('test'); expect(res.data?.a).toBe('test'); diff --git a/test/cases/global/support/permission/common.test.ts b/test/cases/global/support/permission/common.test.ts index b65577662ce3..09d1c437d200 100644 --- a/test/cases/global/support/permission/common.test.ts +++ b/test/cases/global/support/permission/common.test.ts @@ -1,5 +1,10 @@ -import { CommonPerList, CommonRoleList } from '@fastgpt/global/support/permission/constant'; +import { + CommonPerList, + CommonRoleList, + OwnerRoleVal +} from '@fastgpt/global/support/permission/constant'; import { Permission } from '@fastgpt/global/support/permission/controller'; +import type { PermissionValueType } from '@fastgpt/global/support/permission/type'; import { sumPer } from '@fastgpt/global/support/permission/utils'; import { describe, expect, it } from 'vitest'; describe('Permission Helper Class Test', () => { @@ -14,6 +19,22 @@ describe('Permission Helper Class Test', () => { permission.removeRole(CommonRoleList.read.value); expect(permission.checkPer(CommonPerList.manage)).toBe(true); }); + it('Owner Permission Test', () => { + const permission = new Permission({ isOwner: true }); + expect(permission.checkPer(CommonPerList.owner)).toBe(true); + expect(permission.checkPer(CommonPerList.read)).toBe(true); + expect(permission.checkPer(CommonPerList.write)).toBe(true); + expect(permission.checkPer(CommonPerList.manage)).toBe(true); + expect(permission.checkRole(CommonRoleList.read.value)).toBe(true); + expect(permission.checkRole(CommonRoleList.manage.value)).toBe(true); + expect(permission.checkRole(CommonRoleList.write.value)).toBe(true); + expect(permission.checkRole(OwnerRoleVal)).toBe(true); + + permission.addRole(CommonRoleList.read.value); + expect(permission.checkPer(CommonPerList.owner)).toBe(true); + permission.removeRole(CommonRoleList.read.value); + expect(permission.checkPer(CommonPerList.owner)).toBe(true); + }); }); describe('Tool Functions', () => { @@ -22,7 +43,9 @@ describe('Tool Functions', () => { expect(sumPer(0b000, 0b000)).toBe(0b000); expect(sumPer(0b100, 0b001)).toBe(0b101); expect(sumPer(0b111, 0b010)).toBe(0b111); - expect(sumPer(sumPer(0b001, 0b010), 0b100)).toBe(0b111); + expect(sumPer(sumPer(0b001, 0b010) as PermissionValueType, 0b100)).toBe(0b111); expect(sumPer(0b10000000, 0b01000000)).toBe(0b11000000); + expect(sumPer()).toBe(undefined); + expect(sumPer() || 0b111).toBe(0b111); }); }); diff --git a/test/cases/global/support/permission/utils.test.ts b/test/cases/global/support/permission/utils.test.ts new file mode 100644 index 000000000000..2048ba1301bf --- /dev/null +++ b/test/cases/global/support/permission/utils.test.ts @@ -0,0 +1,64 @@ +import { checkRoleUpdateConflict } from '@fastgpt/global/support/permission/utils'; +import { describe, expect, it } from 'vitest'; + +describe('Test checkRoleUpdateConflict', () => { + it('There is no any old collaborator, should return false', () => { + const result = checkRoleUpdateConflict({ + parentClbs: [], + oldRealClbs: [], + newChildClbs: [{ permission: 0b001, tmbId: 'fakeTmbId1' }] + }); + expect(result).toBe(false); + }); + it('There is no parent collaborator, should return false', () => { + const result = checkRoleUpdateConflict({ + parentClbs: [], + oldRealClbs: [{ permission: 0b011, tmbId: 'fakeTmbId1' }], + newChildClbs: [{ permission: 0b001, tmbId: 'fakeTmbId2' }] + }); + expect(result).toBe(false); + }); + it("Edit parent's permission, should return true", () => { + const result = checkRoleUpdateConflict({ + parentClbs: [{ permission: 0b011, tmbId: 'fakeTmbId1' }], + oldRealClbs: [{ permission: 0b011, tmbId: 'fakeTmbId1' }], + newChildClbs: [{ permission: 0b010, tmbId: 'fakeTmbId1' }] + }); + expect(result).toBe(true); + }); + it("Edit permission but parent's permission bit is not set, should return false", () => { + const result = checkRoleUpdateConflict({ + parentClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }], + oldRealClbs: [{ permission: 0b1111, tmbId: 'fakeTmbId1' }], + newChildClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }] + }); + expect(result).toBe(false); + }); + it('add new clb, should return false', () => { + const result = checkRoleUpdateConflict({ + parentClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }], + oldRealClbs: [{ permission: 0b1111, tmbId: 'fakeTmbId1' }], + newChildClbs: [ + { permission: 0b1111, tmbId: 'fakeTmbId1' }, + { permission: 0b1001, tmbId: 'fakeTmbId2' } + ] + }); + expect(result).toBe(false); + }); + it('add clb, no oldRealClbs', () => { + const result = checkRoleUpdateConflict({ + parentClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }], + oldRealClbs: [], + newChildClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }] + }); + expect(result).toBe(true); + }); + it('add clb, no oldRealClbs, false', () => { + const result = checkRoleUpdateConflict({ + parentClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }], + oldRealClbs: [], + newChildClbs: [{ permission: 0b0110, tmbId: 'fakeTmbId1' }] + }); + expect(result).toBe(false); + }); +}); diff --git a/test/mocks/request.ts b/test/mocks/request.ts index 564b2e3d6d26..09574c203926 100644 --- a/test/mocks/request.ts +++ b/test/mocks/request.ts @@ -56,6 +56,7 @@ export type parseHeaderCertRet = { sourceName: string | undefined; apikey: string; isRoot: boolean; + sessionId: string; }; export type MockReqType = { @@ -66,7 +67,7 @@ export type MockReqType = { [key: string]: any; }; -vi.mock(import('@fastgpt/service/support/permission/controller'), async (importOriginal) => { +vi.mock(import('@fastgpt/service/support/permission/auth/common'), async (importOriginal) => { const mod = await importOriginal(); const parseHeaderCert = vi.fn( ({ @@ -87,9 +88,20 @@ vi.mock(import('@fastgpt/service/support/permission/controller'), async (importO return Promise.resolve(auth); } ); + + const authCert = async (props: any) => { + const result = await parseHeaderCert(props); + + return { + ...result, + isOwner: true, + canWrite: true + }; + }; return { ...mod, - parseHeaderCert + parseHeaderCert, + authCert }; }); diff --git a/test/setup.ts b/test/setup.ts index d3793afde633..b58b1e90e97a 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -31,7 +31,7 @@ vi.mock(import('@/service/common/system'), async (importOriginal) => { return '0.0.0'; }, readConfigData: async () => { - return readFileSync('@/data/config.json', 'utf-8'); + return readFileSync('projects/app/data/config.json', 'utf-8'); }, initSystemConfig: async () => { // read env from projects/app/.env