mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-11 04:07:43 +02:00
Strict RBAC for spaces (#10261)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Vendored
+1
-1
@@ -97,7 +97,7 @@
|
||||
"MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json",
|
||||
// "SERVER_PROVIDER":"uweb"
|
||||
"SERVER_PROVIDER": "ws",
|
||||
"MODEL_VERSION": "0.7.301",
|
||||
"MODEL_VERSION": "0.7.312",
|
||||
// "version": "0.7.0",
|
||||
"COMMUNICATION_API_ENABLED": "true",
|
||||
"ELASTIC_INDEX_NAME": "local_storage_index",
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
"ArchiveSpaceDescription": "Umožňuje uživatelům archivovat prostor",
|
||||
"AutoJoin": "Automatické připojení",
|
||||
"AutoJoinDescr": "Automaticky připojit nové zaměstnance k tomuto prostoru",
|
||||
"RBAC": "Řízení přístupu na základě rolí",
|
||||
"RBACDescr": "Vyžadovat řízení přístupu na základě rolí pro provádění akcí v tomto prostoru",
|
||||
"BlobSize": "Velikost",
|
||||
"BlobContentType": "Typ obsahu",
|
||||
"Relation": "Vztah",
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
"ArchiveSpaceDescription": "Gewährt Benutzern die Möglichkeit, den Arbeitsbereich zu archivieren",
|
||||
"AutoJoin": "Automatisch beitreten",
|
||||
"AutoJoinDescr": "Neue Mitarbeiter automatisch diesem Arbeitsbereich hinzufügen",
|
||||
"RBAC": "Rollenbasierte Zugriffskontrolle",
|
||||
"RBACDescr": "Erfordert rollenbasierten Zugriff, um Aktionen in diesem Arbeitsbereich auszuführen",
|
||||
"BlobSize": "Größe",
|
||||
"BlobContentType": "Inhaltstyp",
|
||||
"Relation": "Beziehung",
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
"ArchiveSpaceDescription": "Grants users ability to archive the space",
|
||||
"AutoJoin": "Auto join",
|
||||
"AutoJoinDescr": "Automatically join new employees to this space",
|
||||
"RBAC": "Role-based access control",
|
||||
"RBACDescr": "Require role-based access to perform actions in this space",
|
||||
"BlobSize": "Size",
|
||||
"BlobContentType": "Content type",
|
||||
"Relation": "Relation",
|
||||
|
||||
@@ -54,6 +54,8 @@
|
||||
"ArchiveSpaceDescription": "Concede a los usuarios la capacidad de archivar el espacio",
|
||||
"AutoJoin": "Auto unirse",
|
||||
"AutoJoinDescr": "Unirse automáticamente a los nuevos empleados a este espacio",
|
||||
"RBAC": "Control de acceso basado en roles",
|
||||
"RBACDescr": "Requerir acceso basado en roles para realizar acciones en este espacio",
|
||||
"BlobSize": "Tamaño",
|
||||
"BlobContentType": "Tipo de contenido",
|
||||
"Relation": "Relación",
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
"ArchiveSpaceDescription": "Accorde aux utilisateurs la capacité d'archiver l'espace",
|
||||
"AutoJoin": "Rejoindre automatiquement",
|
||||
"AutoJoinDescr": "Ajouter automatiquement les nouveaux employés à cet espace",
|
||||
"RBAC": "Contrôle d'accès basé sur les rôles",
|
||||
"RBACDescr": "Exiger un accès basé sur les rôles pour effectuer des actions dans cet espace",
|
||||
"BlobSize": "Taille",
|
||||
"BlobContentType": "Type de contenu",
|
||||
"Relation": "Relation",
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
"ArchiveSpaceDescription": "Concede agli utenti la possibilità di archiviare lo spazio",
|
||||
"AutoJoin": "Partecipazione automatica",
|
||||
"AutoJoinDescr": "Aggiungi automaticamente i nuovi dipendenti a questo spazio",
|
||||
"RBAC": "Controllo accessi basato sui ruoli",
|
||||
"RBACDescr": "Richiedi accesso basato sui ruoli per eseguire azioni in questo spazio",
|
||||
"BlobSize": "Dimensione",
|
||||
"BlobContentType": "Tipo di contenuto",
|
||||
"Relation": "Relazione",
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
"ArchiveSpaceDescription": "ユーザーにスペースをアーカイブする権限を付与します",
|
||||
"AutoJoin": "自動参加",
|
||||
"AutoJoinDescr": "新しいユーザーを自動的にこのスペースに参加させます",
|
||||
"RBAC": "ロールベースアクセス制御",
|
||||
"RBACDescr": "このスペースでの操作を実行するためにロールベースのアクセスを要求します",
|
||||
"BlobSize": "サイズ",
|
||||
"BlobContentType": "コンテンツタイプ",
|
||||
"Relation": "関係",
|
||||
|
||||
@@ -54,6 +54,8 @@
|
||||
"ArchiveSpaceDescription": "Concede aos usuários a capacidade de arquivar o espaço",
|
||||
"AutoJoin": "Auto adesão",
|
||||
"AutoJoinDescr": "Adesão automática de novos funcionários a este espaço",
|
||||
"RBAC": "Controle de acesso baseado em funções",
|
||||
"RBACDescr": "Exigir acesso baseado em funções para executar ações neste espaço",
|
||||
"BlobSize": "Tamanho",
|
||||
"BlobContentType": "Tipo de conteúdo",
|
||||
"Relation": "Relação",
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
"ArchiveSpaceDescription": "Дает пользователям разрешение архивировать пространство",
|
||||
"AutoJoin": "Автоприсоединение",
|
||||
"AutoJoinDescr": "Автоматически присоединять новых сотрудников к этому пространству",
|
||||
"RBAC": "Ролевой доступ",
|
||||
"RBACDescr": "Требовать ролевой доступ для выполнения действий в этом пространстве",
|
||||
"BlobSize": "Размер",
|
||||
"BlobContentType": "Тип контента",
|
||||
"Relation": "Связь",
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
"ArchiveSpaceDescription": "Kullanıcılara alanı arşivleme yetkisi verir",
|
||||
"AutoJoin": "Otomatik katıl",
|
||||
"AutoJoinDescr": "Yeni çalışanları bu alana otomatik olarak ekle",
|
||||
"RBAC": "Rol tabanlı erişim kontrolü",
|
||||
"RBACDescr": "Bu alanda işlemleri gerçekleştirmek için rol tabanlı erişim gerektir",
|
||||
"BlobSize": "Boyut",
|
||||
"BlobContentType": "İçerik türü",
|
||||
"Relation": "İlişki",
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
"ArchiveSpaceDescription": "授予用户归档空间的权限",
|
||||
"AutoJoin": "自动加入",
|
||||
"AutoJoinDescr": "自动将新员工加入此空间",
|
||||
"RBAC": "基于角色的访问控制",
|
||||
"RBACDescr": "要求基于角色的访问权限以在此空间中执行操作",
|
||||
"BlobSize": "大小",
|
||||
"BlobContentType": "內容類型",
|
||||
"Relation": "关系",
|
||||
|
||||
@@ -490,6 +490,7 @@ export interface SystemSpace extends Space {}
|
||||
*/
|
||||
export interface TypedSpace extends Space {
|
||||
type: Ref<SpaceType>
|
||||
restricted?: boolean // if true user must have permission to any txes
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -289,7 +289,9 @@ export default plugin(coreId, {
|
||||
UpdateSpaceDescription: '' as IntlString,
|
||||
ArchiveSpaceDescription: '' as IntlString,
|
||||
AutoJoin: '' as IntlString,
|
||||
AutoJoinDescr: '' as IntlString
|
||||
AutoJoinDescr: '' as IntlString,
|
||||
RBAC: '' as IntlString,
|
||||
RBACDescr: '' as IntlString
|
||||
},
|
||||
descriptor: {
|
||||
SpacesType: '' as Ref<SpaceTypeDescriptor>
|
||||
|
||||
@@ -45,6 +45,7 @@ import { BaseMiddleware } from '@hcengineering/server-core'
|
||||
*/
|
||||
export class SpacePermissionsMiddleware extends BaseMiddleware implements Middleware {
|
||||
private whitelistSpaces = new Set<Ref<Space>>()
|
||||
private readonly restrictedSpaces = new Set<Ref<Space>>()
|
||||
private assignmentBySpace: Record<Ref<Space>, RolesAssignment> = {}
|
||||
private permissionsBySpace: Record<Ref<Space>, Record<AccountUuid, Set<Permission>>> = {}
|
||||
private typeBySpace: Record<Ref<Space>, Ref<SpaceType>> = {}
|
||||
@@ -121,6 +122,12 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
return
|
||||
}
|
||||
|
||||
if (space.restricted === true) {
|
||||
this.restrictedSpaces.add(space._id)
|
||||
} else {
|
||||
this.restrictedSpaces.delete(space._id)
|
||||
}
|
||||
|
||||
this.typeBySpace[space._id] = space.type
|
||||
|
||||
const asMixin: RolesAssignment = this.context.hierarchy.as(
|
||||
@@ -157,7 +164,12 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
*
|
||||
* Checks if the required permission is present in the space for the given context
|
||||
*/
|
||||
private checkPermission (ctx: MeasureContext<SessionData>, space: Ref<TypedSpace>, tx: TxCUD<Doc>): boolean {
|
||||
private checkPermission (
|
||||
ctx: MeasureContext<SessionData>,
|
||||
space: Ref<TypedSpace>,
|
||||
tx: TxCUD<Doc>,
|
||||
isSpace: boolean
|
||||
): boolean {
|
||||
const account = ctx.contextData.account
|
||||
const permissions = this.permissionsBySpace[space]?.[account.uuid] ?? []
|
||||
let withoutMatch: Permission | undefined
|
||||
@@ -185,7 +197,7 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
return withoutMatch.forbid !== undefined ? !withoutMatch.forbid : true
|
||||
}
|
||||
|
||||
return true
|
||||
return isSpace || !this.restrictedSpaces.has(space)
|
||||
}
|
||||
|
||||
private throwForbidden (): void {
|
||||
@@ -273,6 +285,7 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
delete this.typeBySpace[tx.objectId]
|
||||
|
||||
this.whitelistSpaces.delete(tx.objectId)
|
||||
this.restrictedSpaces.delete(tx.objectId)
|
||||
}
|
||||
|
||||
private isSpaceTxCUD (tx: TxCUD<Doc>): tx is TxCUD<Space> {
|
||||
@@ -337,9 +350,8 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
if (this.isSpaceTxCUD(tx)) {
|
||||
if (tx._class === core.class.TxCreateDoc) {
|
||||
this.handleCreate(tx)
|
||||
// } else if (tx._class === core.class.TxUpdateDoc) {
|
||||
// Roles assignment in spaces are managed through the space type mixin
|
||||
// so nothing to handle here
|
||||
} else if (tx._class === core.class.TxUpdateDoc) {
|
||||
this.handleSpaceUpdate(tx)
|
||||
} else if (tx._class === core.class.TxMixin) {
|
||||
this.handleMixin(tx)
|
||||
} else if (tx._class === core.class.TxRemoveDoc) {
|
||||
@@ -350,6 +362,21 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
this.handlePermissionsUpdatesFromRoleTx(ctx, tx)
|
||||
}
|
||||
|
||||
private handleSpaceUpdate (tx: TxCUD<Space>): void {
|
||||
if (!this.isTypedSpaceClass(tx.objectClass)) {
|
||||
return
|
||||
}
|
||||
|
||||
const updateTx = tx as TxUpdateDoc<TypedSpace>
|
||||
if (updateTx.operations.restricted !== undefined) {
|
||||
if (updateTx.operations.restricted) {
|
||||
this.restrictedSpaces.add(tx.objectId)
|
||||
} else {
|
||||
this.restrictedSpaces.delete(tx.objectId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private processPermissionsUpdatesFromTx (ctx: MeasureContext, tx: Tx): void {
|
||||
if (!TxProcessor.isExtendsCUD(tx._class)) {
|
||||
return
|
||||
@@ -362,8 +389,8 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
async tx (ctx: MeasureContext<SessionData>, txes: Tx[]): Promise<TxMiddlewareResult> {
|
||||
await this.init(ctx)
|
||||
for (const tx of txes) {
|
||||
this.processPermissionsUpdatesFromTx(ctx, tx)
|
||||
this.checkPermissions(ctx, tx)
|
||||
this.processPermissionsUpdatesFromTx(ctx, tx)
|
||||
}
|
||||
const res = await this.provideTx(ctx, txes)
|
||||
for (const txd of ctx.contextData.broadcast.txes) {
|
||||
@@ -388,17 +415,22 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
|
||||
this.checkSpacePermissions(ctx, cudTx, cudTx.objectSpace)
|
||||
if (isSpace) {
|
||||
this.checkSpacePermissions(ctx, cudTx, cudTx.objectId as Ref<Space>)
|
||||
this.checkSpacePermissions(ctx, cudTx, cudTx.objectId as Ref<Space>, true)
|
||||
}
|
||||
}
|
||||
|
||||
private checkSpacePermissions (ctx: MeasureContext, cudTx: TxCUD<Doc>, targetSpaceId: Ref<Space>): void {
|
||||
private checkSpacePermissions (
|
||||
ctx: MeasureContext,
|
||||
cudTx: TxCUD<Doc>,
|
||||
targetSpaceId: Ref<Space>,
|
||||
isSpace: boolean = false
|
||||
): void {
|
||||
if (this.whitelistSpaces.has(targetSpaceId)) {
|
||||
return
|
||||
}
|
||||
// NOTE: move this checking logic later to be defined in some server plugins?
|
||||
// so they can contribute checks into the middleware for their custom permissions?
|
||||
if (!this.checkPermission(ctx, targetSpaceId as Ref<TypedSpace>, cudTx)) {
|
||||
if (!this.checkPermission(ctx, targetSpaceId as Ref<TypedSpace>, cudTx, isSpace)) {
|
||||
this.throwForbidden()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,12 +39,13 @@
|
||||
let types: Ref<MasterTag>[] =
|
||||
space?.types !== undefined ? hierarchy.clone(space.types) : topLevelTypes.map((it) => it._id)
|
||||
|
||||
let roles = client.getModel().findAllSync(card.class.Role, { type: { $in: types } })
|
||||
$: roles = client.getModel().findAllSync(card.class.Role, { type: { $in: types } })
|
||||
let roles = client.getModel().findAllSync(card.class.Role, { types: { $in: types } })
|
||||
$: roles = client.getModel().findAllSync(card.class.Role, { types: { $in: types } })
|
||||
|
||||
let name: string = space?.name ?? ''
|
||||
|
||||
let isPrivate: boolean = space?.private ?? false
|
||||
let restricted: boolean = space?.restricted ?? false
|
||||
let members: AccountUuid[] =
|
||||
space?.members !== undefined ? hierarchy.clone(space.members) : [getCurrentAccount().uuid]
|
||||
let owners: AccountUuid[] = space?.owners !== undefined ? hierarchy.clone(space.owners) : [getCurrentAccount().uuid]
|
||||
@@ -86,7 +87,8 @@
|
||||
autoJoin,
|
||||
archived: false,
|
||||
type: card.spaceType.SpaceType,
|
||||
types
|
||||
types,
|
||||
restricted
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,9 +108,9 @@
|
||||
}
|
||||
|
||||
async function create (): Promise<void> {
|
||||
const teamspaceData = getData()
|
||||
const data = getData()
|
||||
|
||||
const id = await client.createDoc(card.class.CardSpace, core.space.Space, { ...teamspaceData })
|
||||
const id = await client.createDoc(card.class.CardSpace, core.space.Space, data)
|
||||
|
||||
if (rolesAssignment && !deepEqual(rolesAssignment, getRolesAssignment(roles))) {
|
||||
await client.updateMixin(id, card.class.CardSpace, core.space.Space, core.mixin.SpacesTypeData, rolesAssignment)
|
||||
@@ -221,7 +223,15 @@
|
||||
<Label label={core.string.AutoJoin} />
|
||||
<span><Label label={core.string.AutoJoinDescr} /></span>
|
||||
</div>
|
||||
<Toggle id={'teamspace-autoJoin'} bind:on={autoJoin} />
|
||||
<Toggle id={'space-autoJoin'} bind:on={autoJoin} />
|
||||
</div>
|
||||
|
||||
<div class="antiGrid-row">
|
||||
<div class="antiGrid-row__header withDesciption">
|
||||
<Label label={core.string.RBAC} />
|
||||
<span><Label label={core.string.RBACDescr} /></span>
|
||||
</div>
|
||||
<Toggle id={'space-restricted'} bind:on={restricted} />
|
||||
</div>
|
||||
|
||||
{#each roles as role}
|
||||
|
||||
Reference in New Issue
Block a user