From e86b42f86017fd163226bb5135489ed11df2cce7 Mon Sep 17 00:00:00 2001 From: Boris Vasilenko Date: Wed, 3 Jul 2024 00:05:08 +0300 Subject: [PATCH 1/4] NAS-129686: Fix smb acl form (cherry picked from commit 1d76687cb8be7978cf52b5f95240d6a7d8480b63) --- src/app/enums/nfs-acl.enum.ts | 1 + src/app/interfaces/smb-share.interface.ts | 2 +- .../smb/smb-acl/smb-acl.component.html | 2 + .../smb/smb-acl/smb-acl.component.spec.ts | 82 +++++++++++++++---- .../sharing/smb/smb-acl/smb-acl.component.ts | 20 +++-- 5 files changed, 87 insertions(+), 20 deletions(-) diff --git a/src/app/enums/nfs-acl.enum.ts b/src/app/enums/nfs-acl.enum.ts index 0c8b5626a69..6b74c9d5171 100644 --- a/src/app/enums/nfs-acl.enum.ts +++ b/src/app/enums/nfs-acl.enum.ts @@ -6,6 +6,7 @@ export enum NfsAclTag { Everyone = 'everyone@', User = 'USER', UserGroup = 'GROUP', + Both = 'BOTH', } export const nfsAclTagLabels = new Map([ diff --git a/src/app/interfaces/smb-share.interface.ts b/src/app/interfaces/smb-share.interface.ts index 2077bcf21b5..d73f688434c 100644 --- a/src/app/interfaces/smb-share.interface.ts +++ b/src/app/interfaces/smb-share.interface.ts @@ -63,7 +63,7 @@ export interface SmbSharesecAce { ae_perm: SmbSharesecPermission; ae_type: SmbSharesecType; ae_who_id: { - id_type: NfsAclTag.Everyone | NfsAclTag.UserGroup | NfsAclTag.User | null; + id_type: NfsAclTag.Everyone | NfsAclTag.UserGroup | NfsAclTag.User | NfsAclTag.Both | null; id: number; }; ae_who_sid?: string; diff --git a/src/app/pages/sharing/smb/smb-acl/smb-acl.component.html b/src/app/pages/sharing/smb/smb-acl/smb-acl.component.html index 22c1b8991b1..804cb2f4b01 100644 --- a/src/app/pages/sharing/smb/smb-acl/smb-acl.component.html +++ b/src/app/pages/sharing/smb/smb-acl/smb-acl.component.html @@ -39,6 +39,7 @@ formControlName="user" [label]="'User' | translate" [provider]="userProvider" + [allowCustomValue]="true" [required]="true" > @@ -47,6 +48,7 @@ formControlName="group" [label]="'Group' | translate" [provider]="groupProvider" + [allowCustomValue]="true" [required]="true" > diff --git a/src/app/pages/sharing/smb/smb-acl/smb-acl.component.spec.ts b/src/app/pages/sharing/smb/smb-acl/smb-acl.component.spec.ts index 6e9157a56fa..5038b29ba92 100644 --- a/src/app/pages/sharing/smb/smb-acl/smb-acl.component.spec.ts +++ b/src/app/pages/sharing/smb/smb-acl/smb-acl.component.spec.ts @@ -109,26 +109,80 @@ describe('SmbAclComponent', () => { expect(title).toHaveText('Share ACL for myshare'); }); - it('shows user combobox when Who is user', async () => { - await entriesList.pressAddButton(); - const newListItem = await entriesList.getLastListItem(); - await newListItem.fillForm({ - Who: 'User', + describe('user ace', () => { + it('shows user combobox when Who is user', async () => { + await entriesList.pressAddButton(); + const newListItem = await entriesList.getLastListItem(); + await newListItem.fillForm({ + Who: 'User', + }); + + const userSelect = await loader.getHarness(IxComboboxHarness.with({ label: 'User' })); + expect(userSelect).toExist(); + + const entries = spectator.component.form.value.entries; + expect(entries[entries.length - 1]).toEqual( + expect.not.objectContaining({ user: 0 }), + ); }); - const userSelect = await loader.getHarness(IxComboboxHarness.with({ label: 'User' })); - expect(userSelect).toExist(); + it('allows custom values in User combobox', async () => { + const newListItem = await entriesList.getLastListItem(); + await newListItem.fillForm({ + Who: 'User', + }); + + const fields = await newListItem.getControlHarnessesDict(); + + const userCombobox = fields['User'] as IxComboboxHarness; + await userCombobox.writeCustomValue('root'); + + const userSelect = await loader.getHarness(IxComboboxHarness.with({ label: 'User' })); + expect(userSelect).toExist(); + + const entries = spectator.component.form.value.entries; + expect(entries[entries.length - 1]).toEqual( + expect.objectContaining({ user: 0 }), + ); + }); }); - it('shows group combobox when Who is group', async () => { - await entriesList.pressAddButton(); - const newListItem = await entriesList.getLastListItem(); - await newListItem.fillForm({ - Who: 'Group', + describe('group ace', () => { + it('shows group combobox when Who is group', async () => { + await entriesList.pressAddButton(); + const newListItem = await entriesList.getLastListItem(); + await newListItem.fillForm({ + Who: 'Group', + }); + + const groupSelect = await loader.getHarness(IxComboboxHarness.with({ label: 'Group' })); + expect(groupSelect).toExist(); + + const entries = spectator.component.form.value.entries; + expect(entries[entries.length - 1]).toEqual( + expect.not.objectContaining({ group: 1 }), + ); }); - const groupSelect = await loader.getHarness(IxComboboxHarness.with({ label: 'Group' })); - expect(groupSelect).toExist(); + it('allows custom values in Group combobox', async () => { + const newListItem = await entriesList.getLastListItem(); + await newListItem.fillForm({ + Who: 'Group', + }); + + const fields = await newListItem.getControlHarnessesDict(); + + const groupCombobox = fields['Group'] as IxComboboxHarness; + await groupCombobox.writeCustomValue('wheel'); + + const groupSelect = await loader.getHarness(IxComboboxHarness.with({ label: 'Group' })); + expect(groupSelect).toExist(); + + const entries = spectator.component.form.value.entries; + expect(entries[entries.length - 1]).toEqual( + expect.objectContaining({ group: 1 }), + ); + }); }); it('loads and shows current acl for a share', async () => { diff --git a/src/app/pages/sharing/smb/smb-acl/smb-acl.component.ts b/src/app/pages/sharing/smb/smb-acl/smb-acl.component.ts index 2ce2f26eacf..f27fee260ca 100644 --- a/src/app/pages/sharing/smb/smb-acl/smb-acl.component.ts +++ b/src/app/pages/sharing/smb/smb-acl/smb-acl.component.ts @@ -5,7 +5,7 @@ import { FormBuilder } from '@ngneat/reactive-forms'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { TranslateService } from '@ngx-translate/core'; import { - concatMap, forkJoin, from, Observable, of, + concatMap, forkJoin, from, map, Observable, of, } from 'rxjs'; import { NfsAclTag, smbAclTagLabels } from 'app/enums/nfs-acl.enum'; import { Role } from 'app/enums/role.enum'; @@ -141,16 +141,26 @@ export class SmbAclComponent implements OnInit { private loadSmbAcl(shareName: string): void { this.isLoading = true; - this.ws.call('sharing.smb.getacl', [{ share_name: shareName }]) - .pipe(untilDestroyed(this)) + forkJoin([ + this.ws.call('sharing.smb.getacl', [{ share_name: shareName }]), + this.userService.smbUserQueryDsCache().pipe(map((users) => users.map((user) => user.uid))), + ]).pipe(untilDestroyed(this)) .subscribe({ - next: (shareAcl) => { + next: ([shareAcl, userIds]) => { this.shareAclName = shareAcl.share_name; shareAcl.share_acl.forEach((ace, i) => { this.addAce(); + + let aeWho: FormAclEntry['ae_who']; + if (ace.ae_who_id?.id_type === NfsAclTag.Both) { + aeWho = userIds.includes(ace.ae_who_id.id) ? NfsAclTag.User : NfsAclTag.UserGroup; + } else { + aeWho = ace.ae_who_id?.id_type || ace.ae_who_str as NfsAclTag.Everyone; + } + this.form.controls.entries.at(i).patchValue({ ae_who_sid: ace.ae_who_sid, - ae_who: ace.ae_who_id?.id_type || ace.ae_who_str as NfsAclTag.Everyone, + ae_who: aeWho, ae_perm: ace.ae_perm, ae_type: ace.ae_type, group: ace.ae_who_id?.id_type !== NfsAclTag.Everyone ? ace.ae_who_id?.id : null, From f0033e83bf90dd0812bfdc1df9f612b0a3c670c1 Mon Sep 17 00:00:00 2001 From: Boris Vasilenko Date: Wed, 3 Jul 2024 03:25:25 +0300 Subject: [PATCH 2/4] NAS-129686: Validate and convert user and group names to ids and prepare an SetACl object (cherry picked from commit b89ad1970b8b7f0edb86a458bff37d7bd4d40638) --- .../sharing/smb/smb-acl/smb-acl.component.ts | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/src/app/pages/sharing/smb/smb-acl/smb-acl.component.ts b/src/app/pages/sharing/smb/smb-acl/smb-acl.component.ts index f27fee260ca..040ae59cbf5 100644 --- a/src/app/pages/sharing/smb/smb-acl/smb-acl.component.ts +++ b/src/app/pages/sharing/smb/smb-acl/smb-acl.component.ts @@ -4,8 +4,9 @@ import { import { FormBuilder } from '@ngneat/reactive-forms'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { TranslateService } from '@ngx-translate/core'; +import _ from 'lodash'; import { - concatMap, forkJoin, from, map, Observable, of, + concatMap, firstValueFrom, forkJoin, map, mergeMap, Observable, of, from, } from 'rxjs'; import { NfsAclTag, smbAclTagLabels } from 'app/enums/nfs-acl.enum'; import { Role } from 'app/enums/role.enum'; @@ -30,8 +31,8 @@ interface FormAclEntry { ae_who: NfsAclTag.Everyone | NfsAclTag.UserGroup | NfsAclTag.User | null; ae_perm: SmbSharesecPermission; ae_type: SmbSharesecType; - user: number | null; - group: number | null; + user: string | number | null; + group: string | number | null; } @UntilDestroy() @@ -120,11 +121,11 @@ export class SmbAclComponent implements OnInit { this.form.controls.entries.removeAt(index); } - onSubmit(): void { + async onSubmit(): Promise { this.isLoading = true; - const acl = this.getAclEntriesFromForm(); - this.ws.call('sharing.smb.setacl', [{ share_name: this.shareAclName, share_acl: acl }]) + of(await this.getAclEntriesFromForm()) + .pipe(mergeMap((acl) => this.ws.call('sharing.smb.setacl', [{ share_name: this.shareAclName, share_acl: acl }]))) .pipe(untilDestroyed(this)) .subscribe({ next: () => { @@ -152,6 +153,7 @@ export class SmbAclComponent implements OnInit { this.addAce(); let aeWho: FormAclEntry['ae_who']; + if (ace.ae_who_id?.id_type === NfsAclTag.Both) { aeWho = userIds.includes(ace.ae_who_id.id) ? NfsAclTag.User : NfsAclTag.UserGroup; } else { @@ -176,22 +178,32 @@ export class SmbAclComponent implements OnInit { }); } - private getAclEntriesFromForm(): SmbSharesecAce[] { - return this.form.value.entries.map((ace) => { - const whoId = ace.ae_who === NfsAclTag.UserGroup ? ace.group : ace.user; + private async getAclEntriesFromForm(): Promise { + const results = [] as SmbSharesecAce[]; + for (const ace of this.form.value.entries) { + const whoIdOrName = ace.ae_who === NfsAclTag.UserGroup ? ace.group : ace.user; const result = { ae_perm: ace.ae_perm, ae_type: ace.ae_type } as SmbSharesecAce; - if (ace.ae_who !== this.nfsAclTag.Everyone) { - result.ae_who_id = { id_type: ace.ae_who, id: whoId }; - } - if (ace.ae_who === NfsAclTag.Everyone) { result.ae_who_sid = 'S-1-1-0'; - } + } else { + let id: number; + if (_.isNumber(whoIdOrName)) { + id = Number(whoIdOrName); + } else if (ace.ae_who === NfsAclTag.UserGroup) { + id = (await firstValueFrom(this.userService.getGroupByName(whoIdOrName.toString()))) + .gr_gid; + } else { + id = (await firstValueFrom(this.userService.getUserByName(whoIdOrName.toString()))) + .pw_uid; + } - return result; - }); + result.ae_who_id = { id_type: ace.ae_who, id }; + } + results.push(result); + } + return results; } private initialValueDataFromAce( From 8b2384444e4365f0dba74822f12f132d7cba8af0 Mon Sep 17 00:00:00 2001 From: Boris Vasilenko Date: Wed, 3 Jul 2024 14:35:39 +0300 Subject: [PATCH 3/4] NAS-129686: Error handling fix and explanative commentary for `BOTH` (cherry picked from commit 715a321fdb4d029351a3179d3619eb394d784340) --- src/app/enums/nfs-acl.enum.ts | 2 +- src/app/pages/sharing/smb/smb-acl/smb-acl.component.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/enums/nfs-acl.enum.ts b/src/app/enums/nfs-acl.enum.ts index 6b74c9d5171..e176767a6ab 100644 --- a/src/app/enums/nfs-acl.enum.ts +++ b/src/app/enums/nfs-acl.enum.ts @@ -6,7 +6,7 @@ export enum NfsAclTag { Everyone = 'everyone@', User = 'USER', UserGroup = 'GROUP', - Both = 'BOTH', + Both = 'BOTH', // middleware returns `ID_TYPE_BOTH` when it is not possible to determine whether an AD entity is a user or a group } export const nfsAclTagLabels = new Map([ diff --git a/src/app/pages/sharing/smb/smb-acl/smb-acl.component.ts b/src/app/pages/sharing/smb/smb-acl/smb-acl.component.ts index 040ae59cbf5..bd0f2b8ceee 100644 --- a/src/app/pages/sharing/smb/smb-acl/smb-acl.component.ts +++ b/src/app/pages/sharing/smb/smb-acl/smb-acl.component.ts @@ -121,10 +121,11 @@ export class SmbAclComponent implements OnInit { this.form.controls.entries.removeAt(index); } - async onSubmit(): Promise { + onSubmit(): void { this.isLoading = true; - of(await this.getAclEntriesFromForm()) + of(undefined) + .pipe(mergeMap(() => this.getAclEntriesFromForm())) .pipe(mergeMap((acl) => this.ws.call('sharing.smb.setacl', [{ share_name: this.shareAclName, share_acl: acl }]))) .pipe(untilDestroyed(this)) .subscribe({ From 16a4bf04da04b0d714c4e79f738ab2dbfd29f59d Mon Sep 17 00:00:00 2001 From: Evgeny Stepanovych Date: Fri, 5 Jul 2024 12:45:47 +0200 Subject: [PATCH 4/4] NAS-129686: Fix SMB ACL form --- src/app/enums/nfs-acl.enum.ts | 1 + .../classes/smb-both-combobox-provider.ts | 61 +++++++++++++++++++ .../smb/smb-acl/smb-acl.component.html | 10 +++ .../sharing/smb/smb-acl/smb-acl.component.ts | 37 +++++------ 4 files changed, 91 insertions(+), 18 deletions(-) create mode 100644 src/app/modules/ix-forms/classes/smb-both-combobox-provider.ts diff --git a/src/app/enums/nfs-acl.enum.ts b/src/app/enums/nfs-acl.enum.ts index e176767a6ab..ba139022cdc 100644 --- a/src/app/enums/nfs-acl.enum.ts +++ b/src/app/enums/nfs-acl.enum.ts @@ -20,6 +20,7 @@ export const nfsAclTagLabels = new Map([ export const smbAclTagLabels = new Map([ [NfsAclTag.User, T('User')], [NfsAclTag.UserGroup, T('Group')], + [NfsAclTag.Both, T('Unknown')], [NfsAclTag.Everyone, T('everyone@')], ]); diff --git a/src/app/modules/ix-forms/classes/smb-both-combobox-provider.ts b/src/app/modules/ix-forms/classes/smb-both-combobox-provider.ts new file mode 100644 index 00000000000..24b0b129640 --- /dev/null +++ b/src/app/modules/ix-forms/classes/smb-both-combobox-provider.ts @@ -0,0 +1,61 @@ +import { Observable, forkJoin } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { Group } from 'app/interfaces/group.interface'; +import { Option } from 'app/interfaces/option.interface'; +import { User } from 'app/interfaces/user.interface'; +import { IxComboboxProvider } from 'app/modules/ix-forms/components/ix-combobox/ix-combobox-provider'; +import { UserService } from 'app/services/user.service'; + +export class SmbBothComboboxProvider implements IxComboboxProvider { + protected page = 1; + readonly pageSize = 50; + + excludeInitialOptions(options: Option[]): Option[] { + return options.filter((option) => { + return !this.initialOptions.find((initialOption) => initialOption.value === option.value); + }); + } + + queryResToOptions(users: User[], groups: Group[]): Option[] { + const userOptions = users + .filter((user) => user.id_type_both) + .map((user) => ({ label: user.username, value: user[this.userOptionsValueField] })); + const groupOptions = groups + .filter((user) => user.id_type_both) + .map((group) => ({ label: group.group, value: group[this.groupOptionsValueField] })); + + return [...userOptions, ...groupOptions]; + } + + fetch(filterValue: string): Observable { + this.page = 0; + const offset = this.page * this.pageSize; + + return forkJoin([ + this.userService.smbUserQueryDsCache(filterValue, offset), + this.userService.smbGroupQueryDsCache(filterValue, false, offset), + ]).pipe( + map(([users, groups]) => this.queryResToOptions(users, groups)), + map((options) => [...this.initialOptions, ...this.excludeInitialOptions(options)]), + ); + } + + nextPage(filterValue: string): Observable { + this.page++; + const offset = this.page * this.pageSize; + return forkJoin([ + this.userService.smbUserQueryDsCache(filterValue, offset), + this.userService.smbGroupQueryDsCache(filterValue, false, offset), + ]).pipe( + map(([users, groups]) => this.queryResToOptions(users, groups)), + map((options) => this.excludeInitialOptions(options)), + ); + } + + constructor( + protected userService: UserService, + private userOptionsValueField: 'username' | 'uid' | 'id' = 'username', + private groupOptionsValueField: 'group' | 'gid' | 'id' = 'group', + protected initialOptions: Option[] = [], + ) {} +} diff --git a/src/app/pages/sharing/smb/smb-acl/smb-acl.component.html b/src/app/pages/sharing/smb/smb-acl/smb-acl.component.html index 804cb2f4b01..e35d4d6f5d6 100644 --- a/src/app/pages/sharing/smb/smb-acl/smb-acl.component.html +++ b/src/app/pages/sharing/smb/smb-acl/smb-acl.component.html @@ -51,6 +51,16 @@ [allowCustomValue]="true" [required]="true" > + + +
diff --git a/src/app/pages/sharing/smb/smb-acl/smb-acl.component.ts b/src/app/pages/sharing/smb/smb-acl/smb-acl.component.ts index bd0f2b8ceee..2adb1274769 100644 --- a/src/app/pages/sharing/smb/smb-acl/smb-acl.component.ts +++ b/src/app/pages/sharing/smb/smb-acl/smb-acl.component.ts @@ -6,7 +6,7 @@ import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { TranslateService } from '@ngx-translate/core'; import _ from 'lodash'; import { - concatMap, firstValueFrom, forkJoin, map, mergeMap, Observable, of, from, + concatMap, firstValueFrom, forkJoin, mergeMap, Observable, of, from, } from 'rxjs'; import { NfsAclTag, smbAclTagLabels } from 'app/enums/nfs-acl.enum'; import { Role } from 'app/enums/role.enum'; @@ -18,6 +18,7 @@ import { Option } from 'app/interfaces/option.interface'; import { QueryFilter } from 'app/interfaces/query-api.interface'; import { SmbSharesecAce } from 'app/interfaces/smb-share.interface'; import { User } from 'app/interfaces/user.interface'; +import { SmbBothComboboxProvider } from 'app/modules/ix-forms/classes/smb-both-combobox-provider'; import { SmbGroupComboboxProvider } from 'app/modules/ix-forms/classes/smb-group-combobox-provider'; import { SmbUserComboboxProvider } from 'app/modules/ix-forms/classes/smb-user-combobox-provider'; import { IxSlideInRef } from 'app/modules/ix-forms/components/ix-slide-in/ix-slide-in-ref'; @@ -28,11 +29,12 @@ import { WebSocketService } from 'app/services/ws.service'; interface FormAclEntry { ae_who_sid: string; - ae_who: NfsAclTag.Everyone | NfsAclTag.UserGroup | NfsAclTag.User | null; + ae_who: NfsAclTag.Everyone | NfsAclTag.UserGroup | NfsAclTag.User | NfsAclTag.Both | null; ae_perm: SmbSharesecPermission; ae_type: SmbSharesecType; user: string | number | null; group: string | number | null; + both: string | number | null; } @UntilDestroy() @@ -80,6 +82,7 @@ export class SmbAclComponent implements OnInit { readonly helptext = helptextSharingSmb; readonly nfsAclTag = NfsAclTag; + readonly bothProvider = new SmbBothComboboxProvider(this.userService, 'uid', 'gid'); readonly userProvider = new SmbUserComboboxProvider(this.userService, 'uid'); protected groupProvider: SmbGroupComboboxProvider; @@ -109,6 +112,7 @@ export class SmbAclComponent implements OnInit { this.formBuilder.group({ ae_who_sid: [''], ae_who: [null as never], + both: [null as never], user: [null as never], group: [null as never], ae_perm: [null as SmbSharesecPermission], @@ -143,29 +147,20 @@ export class SmbAclComponent implements OnInit { private loadSmbAcl(shareName: string): void { this.isLoading = true; - forkJoin([ - this.ws.call('sharing.smb.getacl', [{ share_name: shareName }]), - this.userService.smbUserQueryDsCache().pipe(map((users) => users.map((user) => user.uid))), - ]).pipe(untilDestroyed(this)) + this.ws.call('sharing.smb.getacl', [{ share_name: shareName }]) + .pipe(untilDestroyed(this)) .subscribe({ - next: ([shareAcl, userIds]) => { + next: (shareAcl) => { this.shareAclName = shareAcl.share_name; shareAcl.share_acl.forEach((ace, i) => { this.addAce(); - let aeWho: FormAclEntry['ae_who']; - - if (ace.ae_who_id?.id_type === NfsAclTag.Both) { - aeWho = userIds.includes(ace.ae_who_id.id) ? NfsAclTag.User : NfsAclTag.UserGroup; - } else { - aeWho = ace.ae_who_id?.id_type || ace.ae_who_str as NfsAclTag.Everyone; - } - this.form.controls.entries.at(i).patchValue({ ae_who_sid: ace.ae_who_sid, - ae_who: aeWho, + ae_who: ace.ae_who_id?.id_type || ace.ae_who_str as NfsAclTag.Everyone, ae_perm: ace.ae_perm, ae_type: ace.ae_type, + both: ace.ae_who_id?.id_type !== NfsAclTag.Everyone ? ace.ae_who_id?.id : null, group: ace.ae_who_id?.id_type !== NfsAclTag.Everyone ? ace.ae_who_id?.id : null, user: ace.ae_who_id?.id_type !== NfsAclTag.Everyone ? ace.ae_who_id?.id : null, }); @@ -182,7 +177,12 @@ export class SmbAclComponent implements OnInit { private async getAclEntriesFromForm(): Promise { const results = [] as SmbSharesecAce[]; for (const ace of this.form.value.entries) { - const whoIdOrName = ace.ae_who === NfsAclTag.UserGroup ? ace.group : ace.user; + let whoIdOrName = ace.both; + if (ace.ae_who === NfsAclTag.User) { + whoIdOrName = ace.user; + } else if (ace.ae_who === NfsAclTag.UserGroup) { + whoIdOrName = ace.group; + } const result = { ae_perm: ace.ae_perm, ae_type: ace.ae_type } as SmbSharesecAce; @@ -200,7 +200,8 @@ export class SmbAclComponent implements OnInit { .pw_uid; } - result.ae_who_id = { id_type: ace.ae_who, id }; + // TODO: Backend does not yet support BOTH value + result.ae_who_id = { id_type: ace.ae_who === NfsAclTag.Both ? NfsAclTag.UserGroup : ace.ae_who, id }; } results.push(result); }