V4.12.4 features (#5626)

* fix: push again, user select option button and form input radio content overflow (#5601)

* fix: push again, user select option button and form input radio content overflow

* fix: use useCallback instead of useMemo, fix unnecessary delete

* fix: Move the variable inside the component

* fix: do not pass valueLabel to MySelect

* ui

* del collection api adapt

* refactor: inherit permission (#5529)

* refactor: permission update conflict check function

* refactor(permission): app collaborator update api

* refactor(permission): support app update collaborator

* feat: support fe permission conflict check

* refactor(permission): app permission

* refactor(permission): dataset permission

* refactor(permission): team permission

* chore: fe adjust

* fix: type error

* fix: audit pagiation

* fix: tc

* chore: initv4130

* fix: app/dataset auth logic

* chore: move code

* refactor(permission): remove selfPermission

* fix: mock

* fix: test

* fix: app & dataset auth

* fix: inherit

* test(inheritPermission): test syncChildrenPermission

* prompt editor add list plugin (#5620)

* perf: search result (#5608)

* fix: table size (#5598)

* temp: list value

* backspace

* optimize code

---------

Co-authored-by: Archer <545436317@qq.com>
Co-authored-by: 伍闲犬 <whoeverimf5@gmail.com>

* fix: fe & member list (#5619)

* chore: initv4130

* fix: MemberItemCard

* fix: MemberItemCard

* chore: fe adjust & init script

* perf: test code

* doc

* fix debug variables (#5617)

* perf: search result (#5608)

* fix: table size (#5598)

* fix debug variables

* fix

---------

Co-authored-by: Archer <545436317@qq.com>
Co-authored-by: 伍闲犬 <whoeverimf5@gmail.com>

* perf: member ui

* fix: inherit bug (#5624)

* refactor(permission): remove getClbsWithInfo, which is useless

* fix: app list privateApp

* fix: get infos

* perf(fe): remove delete icon when it is disable in MemberItemCard

* fix: dataset private dataset

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Archer <545436317@qq.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* perf: auto coupon

* chore: upgrade script & get infos avatar  (#5625)

* fix: get infos

* chore: initv4130

* feat: support WecomRobot publish, and fix AesKey can not save bug (#5526)

* feat: resolve conflicts

* fix: add param 'show_publish_wecom'

* feat: abstract out WecomCrypto type

* doc: wecom robot document

* fix: solve instability in AI output

* doc: update some pictures

* feat: remove functions from request.ts to chat.ts and toolCall.ts

* doc: wecom robot doc update

* fix

* delete unused code

* doc: update version and prompt

* feat: remove wecom crypto, delete wecom code in workflow

* feat: delete unused codes

---------

Co-authored-by: heheer <zhiyu44@qq.com>

* remove test

* rename init shell

* feat: collection page store

* reload sandbox

* pysandbox

* remove log

* chore: remove useless code (#5629)

* chore: remove useless code

* fix: checkConflict

* perf: support hidden type for RoleList

* fix: copy node

* update doc

* fix(permission): some bug (#5632)

* fix: app/dataset list

* fix: inherit bug

* perf: del app;i18n;save chat

* fix: test

* i18n

* fix: sumper overflow return OwnerRoleVal (#5633)

* remove invalid code

* fix: scroll

* fix: objectId

* update next

* update package

* object id

* mock redis

* feat: add redis append to resolve wecom stream response  (#5643)

* feat: resolve conflicts

* fix: add param 'show_publish_wecom'

* feat: abstract out WecomCrypto type

* doc: wecom robot document

* fix: solve instability in AI output

* doc: update some pictures

* feat: remove functions from request.ts to chat.ts and toolCall.ts

* doc: wecom robot doc update

* fix

* delete unused code

* doc: update version and prompt

* feat: remove wecom crypto, delete wecom code in workflow

* feat: delete unused codes

* feat: add redis append method

---------

Co-authored-by: heheer <zhiyu44@qq.com>

* cache per

* fix(test): init team sub when creating mocked user (#5646)

* fix: button is not vertically centered (#5647)

* doc

* fix: gridFs objectId (#5649)

---------

Co-authored-by: Zeng Qingwen <143274079+fishwww-ww@users.noreply.github.com>
Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: 伍闲犬 <whoeverimf5@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: heheer <zhiyu44@qq.com>
This commit is contained in:
Archer
2025-09-15 20:02:54 +08:00
committed by GitHub
parent c8934e3d22
commit 2ed1545eb5
187 changed files with 3701 additions and 2221 deletions

View File

@@ -1,5 +1,5 @@
import { retryFn } from '@fastgpt/global/common/system/utils';
import { connectionMongo } from '../../mongo';
import { connectionMongo, Types } from '../../mongo';
import { MongoRawTextBufferSchema, bucketName } from './schema';
import { addLog } from '../../system/log';
import { setCron } from '../../system/cron';
@@ -86,7 +86,7 @@ export const getRawTextBuffer = async (sourceId: string) => {
}
// Read file content
const downloadStream = gridBucket.openDownloadStream(bufferData._id);
const downloadStream = gridBucket.openDownloadStream(new Types.ObjectId(bufferData._id));
const fileBuffers = await gridFsStream2Buffer(downloadStream);
@@ -120,7 +120,7 @@ export const deleteRawTextBuffer = async (sourceId: string): Promise<boolean> =>
return false;
}
await gridBucket.delete(buffer._id);
await gridBucket.delete(new Types.ObjectId(buffer._id));
return true;
});
};
@@ -155,7 +155,7 @@ export const clearExpiredRawTextBufferCron = async () => {
for (const item of data) {
try {
await gridBucket.delete(item._id);
await gridBucket.delete(new Types.ObjectId(item._id));
} catch (error) {
addLog.error('Delete expired raw text buffer error', error);
}

View File

@@ -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;

View File

@@ -4,3 +4,10 @@ export const readFromSecondary = {
readPreference: ReadPreference.SECONDARY_PREFERRED, // primary | primaryPreferred | secondary | secondaryPreferred | nearest
readConcern: 'local' as any // local | majority | linearizable | available
};
export const writePrimary = {
writeConcern: {
w: 1,
journal: false
}
};

View File

@@ -56,3 +56,20 @@ export const delRedisCache = async (key: string) => {
const redis = getGlobalRedisConnection();
await retryFn(() => redis.del(getCacheKey(key)));
};
export const appendRedisCache = async (
key: string,
value: string | Buffer | number,
expireSeconds?: number
) => {
try {
const redis = getGlobalRedisConnection();
await retryFn(() => redis.append(getCacheKey(key), value));
if (expireSeconds) {
await redis.expire(getCacheKey(key), expireSeconds);
}
} catch (error) {
addLog.error('Append cache error:', error);
return Promise.reject(error);
}
};

View File

@@ -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<T = any> {
code: number;

View File

@@ -645,4 +645,4 @@ const createChatCompletion = async ({
}
return Promise.reject(error);
}
};
};

View File

@@ -157,23 +157,18 @@ export const onDelOneApp = async ({
).lean();
await Promise.all(evalJobs.map((evalJob) => removeEvaluationJob(evalJob._id)));
// Delete chats
await deleteChatFiles({ appId });
await MongoChatItem.deleteMany({
appId
});
await MongoChat.deleteMany({
appId
});
const del = async (session: ClientSession) => {
for await (const app of apps) {
const appId = app._id;
// Chats
await deleteChatFiles({ appId });
await MongoChatItem.deleteMany(
{
appId
},
{ session }
);
await MongoChat.deleteMany(
{
appId
},
{ session }
);
// 删除分享链接
await MongoOutLink.deleteMany({
@@ -205,6 +200,7 @@ export const onDelOneApp = async ({
{ $pull: { quickAppIds: { id: String(appId) } } }
).session(session);
// Del permission
await MongoResourcePermission.deleteMany({
resourceType: PerResourceTypeEnum.app,
teamId,

View File

@@ -15,6 +15,7 @@ import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils';
import { MongoAppChatLog } from '../app/logs/chatLogsSchema';
import { writePrimary } from '../../common/mongo/utils';
type Props = {
chatId: string;
@@ -115,7 +116,7 @@ export async function saveChat({
});
await mongoSessionRun(async (session) => {
const [{ _id: chatItemIdHuman }, { _id: chatItemIdAi }] = await MongoChatItem.insertMany(
const [{ _id: chatItemIdHuman }, { _id: chatItemIdAi }] = await MongoChatItem.create(
processedContent.map((item) => ({
chatId,
teamId,
@@ -123,7 +124,7 @@ export async function saveChat({
appId,
...item
})),
{ session }
{ session, ordered: true, ...writePrimary }
);
await MongoChat.updateOne(
@@ -152,7 +153,8 @@ export async function saveChat({
},
{
session,
upsert: true
upsert: true,
...writePrimary
}
);
@@ -215,7 +217,8 @@ export async function saveChat({
}
},
{
upsert: true
upsert: true,
...writePrimary
}
);
} catch (error) {
@@ -223,9 +226,15 @@ export async function saveChat({
}
if (isUpdateUseTime) {
await MongoApp.findByIdAndUpdate(appId, {
updateTime: new Date()
}).catch();
await MongoApp.updateOne(
{ _id: appId },
{
updateTime: new Date()
},
{
...writePrimary
}
).catch();
}
} catch (error) {
addLog.error(`update chat history error`, error);

View File

@@ -142,7 +142,7 @@ export const clearExpiredDatasetImageCron = async () => {
for (const item of data) {
try {
await gridBucket.delete(item._id);
await gridBucket.delete(new Types.ObjectId(item._id));
} catch (error) {
addLog.error('Delete expired dataset image error', error);
}

View File

@@ -15,7 +15,7 @@
"@vercel/otel": "^1.13.0",
"@xmldom/xmldom": "^0.8.10",
"@zilliz/milvus2-sdk-node": "2.4.10",
"axios": "^1.8.2",
"axios": "^1.12.1",
"bullmq": "^5.52.2",
"chalk": "^5.3.0",
"cheerio": "1.0.0-rc.12",
@@ -38,7 +38,7 @@
"mongoose": "^8.10.1",
"multer": "2.0.2",
"mysql2": "^3.11.3",
"next": "14.2.28",
"next": "14.2.32",
"nextjs-cors": "^2.2.0",
"node-cron": "^3.0.3",
"node-xlsx": "^0.24.0",

View File

@@ -1,15 +1,15 @@
/* Auth app permission */
import { MongoApp } from '../../../core/app/schema';
import { type AppDetailType } from '@fastgpt/global/core/app/type.d';
import { parseHeaderCert } from '../controller';
import {
NullRoleVal,
PerResourceTypeEnum,
ReadPermissionVal,
ReadRoleVal
} 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 +18,8 @@ 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';
import { sumPer } from '@fastgpt/global/support/permission/utils';
export const authPluginByTmbId = async ({
tmbId,
@@ -90,53 +92,27 @@ 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 })
};
}
const isGetParentClb =
app.inheritPermission && !AppFolderTypeList.includes(app.type) && !!app.parentId;
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 });
const [folderPer = NullRoleVal, myPer = NullRoleVal] = await Promise.all([
isGetParentClb
? getTmbPermission({
teamId,
tmbId,
resourceId: app.parentId!,
resourceType: PerResourceTypeEnum.app
})
: NullRoleVal,
getTmbPermission({
teamId,
tmbId,
resourceId: appId,
resourceType: PerResourceTypeEnum.app
})
]);
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 Per = new AppPermission({ role: sumPer(folderPer, myPer), isOwner });
if (!Per.checkPer(per)) {
return Promise.reject(AppErrEnum.unAuthApp);

View File

@@ -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`);
};

View File

@@ -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<FileTokenQuery>((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
});
});
});

View File

@@ -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,

View File

@@ -1,27 +1,24 @@
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, 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';
import { DEFAULT_ORG_AVATAR, DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants';
/** get resource permission for a team member
* If there is no permission for the team member, it will return undefined
@@ -31,7 +28,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 +103,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<PerResourceTypeEnum, 'team'>;
resourceId: ParentIdType;
}
)) {
return MongoResourcePermission.find(
{
resourceId,
@@ -124,282 +131,110 @@ export async function getResourceClbsAndGroups({
teamId
},
undefined,
{ session }
{ ...(session ? { session } : {}) }
).lean();
}
export const getClbsAndGroupsWithInfo = async ({
resourceId,
resourceType,
teamId
export const getClbsInfo = async ({
clbs,
teamId,
ownerTmbId
}: {
clbs: CollaboratorItemType[];
teamId: string;
} & (
| {
resourceId: ParentIdType;
resourceType: Omit<`${PerResourceTypeEnum}`, 'team'>;
}
| {
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()
]);
ownerTmbId?: string;
}): Promise<CollaboratorItemDetailType[]> => {
const tmbIds = [];
const orgIds = [];
const groupIds = [];
export const delResourcePermissionById = (id: string) => {
return MongoResourcePermission.findByIdAndDelete(id);
};
export const delResourcePermission = ({
session,
tmbId,
groupId,
orgId,
...props
}: {
resourceType: PerResourceTypeEnum;
teamId: string;
resourceId: string;
session?: ClientSession;
tmbId?: string;
groupId?: string;
orgId?: string;
}) => {
// either tmbId or groupId or orgId must be provided
if (!tmbId && !groupId && !orgId) {
return Promise.reject(CommonErrEnum.missingParams);
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);
}
return MongoResourcePermission.deleteOne(
{
...(tmbId ? { tmbId } : {}),
...(groupId ? { groupId } : {}),
...(orgId ? { orgId } : {}),
...props
},
{ session }
);
};
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();
/* 下面代码等迁移 */
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 clbs.map((clb) => {
const info = infos.find((info) => info._id === getCollaboratorId(clb));
return {
uid: '',
...clb,
teamId,
tmbId,
apikey,
appId: apiKeyAppId || authorizationAppid,
sourceName
permission: new Permission({
role: clb.permission,
isOwner: Boolean(ownerTmbId && clb.tmbId && ownerTmbId === clb.tmbId)
}),
name: info?.name ?? 'Unknown name',
avatar: info?.avatar || (clb.orgId ? DEFAULT_ORG_AVATAR : DEFAULT_TEAM_AVATAR)
};
}
// 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`);
};
/* 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<FileTokenQuery>((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
});
});
});
};
export const createResourceDefaultCollaborators = async ({
resource,
resourceType,
session,
tmbId
}: {
resource: SyncChildrenPermissionResourceType;
resourceType: PerResourceTypeEnum;
// 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
const collaborators: CollaboratorItemType[] = [
...parentClbs
.filter((item) => item.tmbId !== tmbId)
.map((clb) => {
if (clb.permission === OwnerRoleVal) {
clb.permission = ManageRoleVal;
}
return clb;
}),
{
tmbId,
permission: OwnerRoleVal
}
];
const ops: AnyBulkWriteOperation<ResourcePermissionType>[] = [];
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
}
});
}
await MongoResourcePermission.bulkWrite(ops, { session });
};

View File

@@ -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,
@@ -9,6 +9,7 @@ import { getTmbInfoByTmbId } from '../../user/team/controller';
import { MongoDataset } from '../../../core/dataset/schema';
import {
NullPermissionVal,
NullRoleVal,
PerResourceTypeEnum
} from '@fastgpt/global/support/permission/constant';
import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
@@ -21,6 +22,8 @@ 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';
import { sumPer } from '@fastgpt/global/support/permission/utils';
export const authDatasetByTmbId = async ({
tmbId,
@@ -61,54 +64,27 @@ 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 [folderPer = NullRoleVal, myPer = NullRoleVal] = await Promise.all([
isGetParentClb
? getTmbPermission({
teamId,
tmbId,
resourceId: dataset.parentId!,
resourceType: PerResourceTypeEnum.dataset
})
: NullRoleVal,
getTmbPermission({
teamId,
tmbId,
resourceId: datasetId,
resourceType: PerResourceTypeEnum.dataset
})
]);
const Per = new DatasetPermission({
role: parent.permission.role,
isOwner
});
return {
Per
};
}
})();
const Per = new DatasetPermission({ role: sumPer(folderPer, myPer), isOwner });
if (!Per.checkPer(per)) {
return Promise.reject(DatasetErrEnum.unAuthDataset);

View File

@@ -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,

View File

@@ -1,11 +1,22 @@
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 {
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 +24,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,
@@ -29,7 +35,7 @@ export async function syncChildrenPermission({
resourceModel,
session,
collaborators
collaborators: latestClbList
}: {
resource: SyncChildrenPermissionResourceType;
@@ -42,55 +48,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<SyncChildrenPermissionResourceType[]>()
.session(session);
// bfs to get all children
const queue = [String(resource._id)];
const children: string[] = [];
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 allClbs = await MongoResourcePermission.find({
resourceType,
teamId,
resourceId: {
$in: allFolders.map((folder) => folder._id)
}
})
.lean()
.session(session);
// 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
});
/** ResourceMap<resourceId, resourceType> */
const resourceMap = new Map<string, SyncChildrenPermissionResourceType>();
/** parentChildrenMap<parentId, resourceType[]> */
const parentChildrenMap = new Map<string, SyncChildrenPermissionResourceType[]>();
// 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<resourceId, CollaboratorItemType[]>
* save the clb virtual state, not the real state at present in the DB.
*/
const resourceIdClbMap = new Map<string, ResourcePermissionType[]>();
// Initialize the resourceIdPermissionMap
for (const clb of allClbs) {
const resourceId = clb.resourceId;
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 ops: AnyBulkWriteOperation<ResourcePermissionType>[] = [];
const latestClbMap = new Map(latestClbList.map((clb) => [getCollaboratorId(clb), { ...clb }]));
while (queue.length) {
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 myClbsIdSet = new Set(myClbs.map((clb) => getCollaboratorId(clb)));
// add or update
for (const latestClb of latestClbList) {
if (latestClb.permission === OwnerRoleVal) {
continue;
}
if (!myClbsIdSet.has(getCollaboratorId(latestClb))) {
ops.push({
insertOne: {
document: {
resourceId: child._id,
resourceType,
teamId,
permission: latestClb.permission,
...pickCollaboratorIdFields(latestClb)
} as ResourcePermissionType
}
});
} else {
const myclb = myClbs.find(
(clb) => getCollaboratorId(latestClb) === getCollaboratorId(clb)
)!;
ops.push({
updateOne: {
filter: {
resourceId: child._id,
teamId,
...pickCollaboratorIdFields(latestClb),
resourceType
},
update: {
permission: sumPer(myclb.permission, latestClb.permission)
}
}
});
}
}
// delele
for (const myClb of myClbs) {
const parentClb = parentClbs.find(
(clb) => getCollaboratorId(clb) === getCollaboratorId(myClb)
);
// the new collaborators doesnt have it, and the permission is same.
// remove it
if (
!latestClbMap.get(getCollaboratorId(myClb)) &&
parentClb &&
myClb.permission === parentClb.permission
) {
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,9 +214,54 @@ export async function resumeInheritPermission({
session?: ClientSession;
}) {
const isFolder = folderTypeList.includes(resource.type);
// Folder resource, need to sync children
const [parentClbs, oldMyClbs] = await Promise.all([
getResourceOwnedClbs({
resourceId: resource.parentId,
teamId: resource.teamId,
resourceType
}),
getResourceOwnedClbs({
resourceId: resource._id,
teamId: resource.teamId,
resourceType
})
]);
const parentOwner = parentClbs.find((clb) => clb.permission === OwnerRoleVal);
const collaborators = mergeCollaboratorList({
parentClbs,
childClbs: oldMyClbs
});
const parentManage = collaborators.find(
(clb) => parentOwner?.tmbId && clb.tmbId && parentOwner?.tmbId === clb.tmbId
);
if (parentManage) parentManage.permission = ManageRoleVal;
console.log(collaborators);
const fn = async (session: ClientSession) => {
// update the resource permission
if (isFolder) {
// sync self
await syncCollaborators({
resourceType,
collaborators,
teamId: resource.teamId,
resourceId: resource._id,
session
});
// sync children
await syncChildrenPermission({
resource,
resourceModel,
folderTypeList,
resourceType,
session,
collaborators
});
}
await resourceModel.updateOne(
{
_id: resource._id
@@ -120,39 +271,6 @@ export async function resumeInheritPermission({
},
{ session }
);
// Folder resource, need to sync children
if (isFolder) {
const parentClbsAndGroups = await getResourceClbsAndGroups({
resourceId: resource.parentId,
teamId: resource.teamId,
resourceType,
session
});
// sync self
await syncCollaborators({
resourceType,
collaborators: parentClbsAndGroups,
teamId: resource.teamId,
resourceId: resource._id,
session
});
// sync children
await syncChildrenPermission({
resource: {
...resource
},
resourceModel,
folderTypeList,
resourceType,
session,
collaborators: parentClbsAndGroups
});
} else {
// Not folder, delete all clb
await MongoResourcePermission.deleteMany({ resourceId: resource._id }, { session });
}
};
if (session) {
@@ -162,9 +280,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 +293,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<ResourcePermissionType>[] = [];
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 });
}

View File

@@ -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

View File

@@ -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,9 @@ export const OrgSchema = new Schema(
type: String,
required: true
},
avatar: String,
avatar: {
type: String
},
description: String,
updateTime: {
type: Date,

View File

@@ -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({

View File

@@ -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

View File

@@ -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';

View File

@@ -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 })
};
};

View File

@@ -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<string, any>): Promise<TeamTmbItemTyp
}
const role =
(await getResourcePermission({
(await getTmbPermission({
resourceType: PerResourceTypeEnum.team,
teamId: tmb.teamId,
tmbId: tmb._id