Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 23 additions & 21 deletions packages/global/support/permission/collaborator.d.ts
Original file line number Diff line number Diff line change
@@ -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<addOnly = false> = {
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[];
};
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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<T = {}> = Readonly<
export type RoleListType<T extends string | number | symbol = CommonRoleKeyEnum> = Readonly<
Record<
T | CommonRoleKeyEnum,
Readonly<{
Expand All @@ -43,7 +41,7 @@ export type RoleListType<T = {}> = Readonly<
* write: 0b110, // bad, should be 0b010
* }
*/
export type PermissionListType<T = {}> = Readonly<
export type PermissionListType<T extends string | number | symbol = CommonPerKeyEnum> = Readonly<
Record<T | CommonPerKeyEnum, PermissionValueType>
>;

Expand Down
214 changes: 170 additions & 44 deletions packages/global/support/permission/utils.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,178 @@
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
return undefined;
}
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 = <T extends CollaboratorItemType>(...clbLists: T[][]): T[] => {
const merge = (list1: T[], list2: T[]): T[] => {
const idToClb = new Map<string, T>();

// 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));
};
27 changes: 27 additions & 0 deletions packages/service/common/mongo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/service/common/response/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading