app/api/permissions/entitiesPermissions.ts
import { WithId } from 'mongodb';
import { model as entityModel } from 'api/entities';
import entities from 'api/entities/entities';
import { search } from 'api/search';
import users from 'api/users/users';
import userGroups from 'api/usergroups/userGroups';
import { unique } from 'api/utils/filters';
import { EntitySchema, EntityWithFilesSchema } from 'shared/types/entityType';
import {
AccessLevels,
PermissionType,
MixedAccess,
validateUniquePermissions,
} from 'shared/types/permissionSchema';
import { PermissionSchema, PermissionsDataSchema } from 'shared/types/permissionType';
import { MemberWithPermission } from 'shared/types/entityPermisions';
import { ObjectIdSchema } from 'shared/types/commonTypes';
import { permissionsContext } from './permissionsContext';
import { PUBLIC_PERMISSION } from './publicPermission';
type PermissionUpdate = WithId<Pick<EntitySchema, '_id' | 'permissions' | 'published'>>;
const setAdditionalData = (
referencedList: { _id: ObjectIdSchema }[],
permission: PermissionSchema,
additional: (data: { _id: ObjectIdSchema }) => {}
) => {
const referencedData = referencedList.find(
u => u._id!.toString() === permission.refId.toString()
);
return referencedData ? { ...permission, ...additional(referencedData) } : undefined;
};
async function setAccessLevelAndPermissionData(
grantedPermissions: { [p: string]: { permission: PermissionSchema; access: AccessLevels[] } },
entitiesPermissionsData: { permissions: PermissionSchema[] | undefined; published: boolean }[],
publishedData: boolean[]
) {
const grantedIds = Object.keys(grantedPermissions);
const [usersData, groupsData] = await Promise.all([
users.get({ _id: { $in: grantedIds } }),
userGroups.get({ _id: { $in: grantedIds } }),
]);
const permissionsData = Object.keys(grantedPermissions).map(id => {
const differentLevels = grantedPermissions[id].access.filter(unique);
const level =
grantedPermissions[id].access.length !== entitiesPermissionsData.length ||
differentLevels.length > 1
? MixedAccess.MIXED
: differentLevels[0];
const sourceData =
grantedPermissions[id].permission.type === PermissionType.USER ? usersData : groupsData;
const additional =
grantedPermissions[id].permission.type.toString() === PermissionType.USER
? (p: any) => ({ label: p.username })
: (g: any) => ({ label: g.name });
return {
...setAdditionalData(sourceData, grantedPermissions[id].permission, additional),
level,
} as MemberWithPermission;
});
const publishedEntities = publishedData.filter(published => published).length;
const totalEntities = publishedData.length;
if (publishedEntities) {
permissionsData.push({
...PUBLIC_PERMISSION,
level: totalEntities === publishedEntities ? AccessLevels.READ : MixedAccess.MIXED,
});
}
return permissionsData.filter(p => p.refId !== undefined);
}
const publishingChanged = (newPublishedValue: boolean, currentEntities: EntitySchema[]) =>
currentEntities.reduce(
(changed, entity) => changed || !!entity.published !== newPublishedValue,
false
);
const replaceMixedAccess = (
entity: EntitySchema,
newPermissions: PermissionSchema[]
): PermissionSchema[] =>
newPermissions
.map(newPermission => {
if (newPermission.level !== MixedAccess.MIXED) return newPermission;
return entity.permissions?.find(p => p.refId.toString() === newPermission.refId.toString());
})
.filter(p => p) as PermissionSchema[];
const getPublishingQuery = (newPublicPermission?: PermissionSchema) => {
if (newPublicPermission && newPublicPermission.level === MixedAccess.MIXED) return {};
return { published: !!newPublicPermission };
};
async function saveEntities(updates: PermissionUpdate[]) {
const response = await entityModel.saveMultiple(updates);
await search.indexEntities({ _id: { $in: response.map(d => d._id) } }, '+fullText');
return response;
}
export const entitiesPermissions = {
set: async (permissionsData: PermissionsDataSchema) => {
await validateUniquePermissions(permissionsData);
const user = permissionsContext.getUserInContext();
const currentEntities: EntityWithFilesSchema[] = await entities.get(
{ sharedId: { $in: permissionsData.ids } },
{ published: 1, permissions: 1 }
);
const nonPublicPermissions = permissionsData.permissions.filter(
p => p.type !== PermissionType.PUBLIC
);
const publicPermission = permissionsData.permissions.find(
p => p.type === PermissionType.PUBLIC
);
if (
!['admin', 'editor'].includes(user!.role) &&
publicPermission?.level !== MixedAccess.MIXED &&
publishingChanged(!!publicPermission, currentEntities)
) {
throw new Error('Insuficient permissions to share/unshare publicly');
}
const toSave: PermissionUpdate[] = currentEntities.map(entity => ({
_id: entity._id!,
permissions: replaceMixedAccess(entity, nonPublicPermissions),
...getPublishingQuery(publicPermission),
}));
await saveEntities(toSave);
},
get: async (sharedIds: string[]) => {
const entitiesPermissionsData: { permissions: PermissionSchema[]; published: boolean }[] = (
await entities.get(
{ sharedId: { $in: sharedIds } },
{ permissions: 1, published: 1 },
{ withoutDocuments: true }
)
).map((entity: EntitySchema) => ({
permissions: entity.permissions || [],
published: !!entity.published,
}));
const grantedPermissions: {
[k: string]: { permission: PermissionSchema; access: AccessLevels[] };
} = {};
const publishedStatuses: boolean[] = [];
entitiesPermissionsData.forEach(entityPermissions => {
entityPermissions.permissions.forEach(permission => {
const grantedPermission = grantedPermissions[permission.refId.toString()];
if (grantedPermission) {
grantedPermission.access.push(permission.level as AccessLevels);
} else {
grantedPermissions[permission.refId.toString()] = {
permission,
access: [permission.level as AccessLevels],
};
}
});
publishedStatuses.push(entityPermissions.published);
});
const permissions: MemberWithPermission[] = await setAccessLevelAndPermissionData(
grantedPermissions,
entitiesPermissionsData,
publishedStatuses
);
return permissions.sort((a: MemberWithPermission, b: MemberWithPermission) =>
(a.type + a.label).localeCompare(b.type + b.label)
);
},
};