Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,46 @@ export const data: Array<UmbMockUserGroupModel> = [
},
verbs: ['Umb.Document.Delete', 'Umb.Document.Read'],
},
{
$type: 'ElementPermissionPresentationModel',
element: { id: 'permissions-element-read-only-id' },
verbs: ['Umb.Element.Read'],
},
{
$type: 'ElementContainerPermissionPresentationModel',
elementContainer: { id: 'permissions-element-read-only-id' },
verbs: ['Umb.ElementContainer.Create'],
},
{
$type: 'ElementPermissionPresentationModel',
element: { id: 'permissions-folder-read-only-id' },
verbs: ['Umb.Element.Create'],
},
{
$type: 'ElementContainerPermissionPresentationModel',
elementContainer: { id: 'permissions-folder-read-only-id' },
verbs: ['Umb.ElementContainer.Read'],
},
{
$type: 'ElementPermissionPresentationModel',
element: { id: 'permissions-both-read-id' },
verbs: ['Umb.Element.Read'],
},
{
$type: 'ElementContainerPermissionPresentationModel',
elementContainer: { id: 'permissions-both-read-id' },
verbs: ['Umb.ElementContainer.Read'],
},
{
$type: 'ElementPermissionPresentationModel',
element: { id: 'permissions-neither-read-id' },
verbs: ['Umb.Element.Create'],
},
{
$type: 'ElementContainerPermissionPresentationModel',
elementContainer: { id: 'permissions-neither-read-id' },
verbs: ['Umb.ElementContainer.Create'],
},
],
sections: [
'Umb.Section.Content',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { UmbElementTypeStructureRepository } from '../../repository/structure/index.js';
import { UMB_CREATE_ELEMENT_WORKSPACE_PATH_PATTERN } from '../../paths.js';
import {
UMB_ELEMENT_USER_PERMISSION_CONDITION_ALIAS,
UMB_USER_PERMISSION_ELEMENT_CREATE,
} from '../../user-permissions/constants.js';
import type { UmbAllowedElementTypeModel } from '../../repository/structure/index.js';
import type { UmbElementEntityTypeUnion } from '../../entity.js';
import type {
Expand All @@ -8,7 +12,7 @@ import type {
} from './element-create-options-modal.token.js';
import { css, customElement, html, nothing, repeat, state, when } from '@umbraco-cms/backoffice/external/lit';
import { UmbExtensionsApiInitializer } from '@umbraco-cms/backoffice/extension-api';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { createExtensionApiByAlias, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import type { ManifestEntityCreateOptionAction } from '@umbraco-cms/backoffice/entity-create-option-action';
import type { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api';
Expand All @@ -29,8 +33,24 @@ export class UmbElementCreateOptionsModalElement extends UmbModalBaseElement<
@state()
private _hrefList: Array<string | undefined> = [];

constructor() {
super();
createExtensionApiByAlias(this, UMB_ELEMENT_USER_PERMISSION_CONDITION_ALIAS, [
{
config: {
alias: UMB_ELEMENT_USER_PERMISSION_CONDITION_ALIAS,
allOf: [UMB_USER_PERMISSION_ELEMENT_CREATE],
},
onChange: (permitted: boolean) => {
if (permitted && this._allowedElementTypes.length === 0) {
this.#retrieveAllowedElementTypes();
Comment thread
leekelleher marked this conversation as resolved.
}
},
},
]);
}

override async firstUpdated() {
this.#retrieveAllowedElementTypes();
this.#initCreateOptionActions();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { UMB_ELEMENT_FOLDER_ENTITY_TYPE, UMB_ELEMENT_ROOT_ENTITY_TYPE } from '../../entity.js';
import { UMB_USER_PERMISSION_ELEMENT_FOLDER_CREATE } from '../../folder/user-permissions/constants.js';
import {
UMB_ELEMENT_USER_PERMISSION_CONDITION_ALIAS,
UMB_ELEMENT_OR_ELEMENT_FOLDER_USER_PERMISSION_CONDITION_ALIAS,
UMB_USER_PERMISSION_ELEMENT_CREATE,
} from '../../user-permissions/constants.js';
import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin';
Expand All @@ -21,8 +22,9 @@ export const manifests: Array<UmbExtensionManifest> = [
},
conditions: [
{
alias: UMB_ELEMENT_USER_PERMISSION_CONDITION_ALIAS,
allOf: [UMB_USER_PERMISSION_ELEMENT_CREATE],
alias: UMB_ELEMENT_OR_ELEMENT_FOLDER_USER_PERMISSION_CONDITION_ALIAS,
element: { allOf: [UMB_USER_PERMISSION_ELEMENT_CREATE] },
folder: { allOf: [UMB_USER_PERMISSION_ELEMENT_FOLDER_CREATE] },
},
{
alias: UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import {
UMB_ELEMENT_OR_ELEMENT_FOLDER_USER_PERMISSION_CONDITION_ALIAS,
UMB_USER_PERMISSION_ELEMENT_READ,
} from '../../user-permissions/constants.js';
import { UMB_USER_PERMISSION_ELEMENT_FOLDER_READ } from '../../folder/user-permissions/constants.js';
import { UMB_CURRENT_USER_ALLOW_ELEMENT_RECYCLE_BIN_CONDITION_ALIAS } from '../conditions/allow-element-recycle-bin.condition.js';
import { UMB_ELEMENT_MENU_ALIAS } from '../../menu/constants.js';
import { UMB_ELEMENT_WORKSPACE_ALIAS } from '../../workspace/constants.js';
Expand All @@ -20,7 +25,14 @@ const menuItem: ManifestMenuItemTreeKind = {
menus: [UMB_ELEMENT_MENU_ALIAS],
treeAlias: UMB_ELEMENT_RECYCLE_BIN_TREE_ALIAS,
},
conditions: [{ alias: UMB_CURRENT_USER_ALLOW_ELEMENT_RECYCLE_BIN_CONDITION_ALIAS }],
conditions: [
{ alias: UMB_CURRENT_USER_ALLOW_ELEMENT_RECYCLE_BIN_CONDITION_ALIAS },
{
alias: UMB_ELEMENT_OR_ELEMENT_FOLDER_USER_PERMISSION_CONDITION_ALIAS,
element: { allOf: [UMB_USER_PERMISSION_ELEMENT_READ] },
folder: { allOf: [UMB_USER_PERMISSION_ELEMENT_FOLDER_READ] },
},
],
};

const workspaceContext: ManifestWorkspaceContextMenuStructureKind = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import {
UMB_ELEMENT_OR_ELEMENT_FOLDER_USER_PERMISSION_CONDITION_ALIAS,
UMB_USER_PERMISSION_ELEMENT_READ,
} from '../../../user-permissions/constants.js';
import { UMB_USER_PERMISSION_ELEMENT_FOLDER_READ } from '../../../folder/user-permissions/constants.js';
import { UMB_ELEMENT_RECYCLE_BIN_COLLECTION_ALIAS } from '../../collection/constants.js';
import { UMB_ELEMENT_RECYCLE_BIN_ROOT_ENTITY_TYPE } from '../entity.js';
import { UMB_ELEMENT_RECYCLE_BIN_ROOT_WORKSPACE_ALIAS } from './constants.js';
Expand Down Expand Up @@ -32,6 +37,11 @@ const workspaceView: ManifestWorkspaceViewCollectionKind = {
alias: UMB_WORKSPACE_CONDITION_ALIAS,
match: UMB_ELEMENT_RECYCLE_BIN_ROOT_WORKSPACE_ALIAS,
},
{
alias: UMB_ELEMENT_OR_ELEMENT_FOLDER_USER_PERMISSION_CONDITION_ALIAS,
element: { allOf: [UMB_USER_PERMISSION_ELEMENT_READ] },
folder: { allOf: [UMB_USER_PERMISSION_ELEMENT_FOLDER_READ] },
},
],
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export const UMB_ELEMENT_USER_PERMISSION_CONDITION_ALIAS = 'Umb.Condition.UserPermission.Element';
export const UMB_ELEMENT_OR_ELEMENT_FOLDER_USER_PERMISSION_CONDITION_ALIAS =
'Umb.Condition.UserPermission.ElementOrElementFolder';
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { expect } from '@open-wc/testing';
import { customElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api';
import { UmbCurrentUserContext, UmbCurrentUserStore } from '@umbraco-cms/backoffice/current-user';
import { UmbNotificationContext } from '@umbraco-cms/backoffice/notification';
import { UmbAncestorsEntityContext, UmbEntityContext, type UmbEntityModel } from '@umbraco-cms/backoffice/entity';
import { useMockSet } from '@umbraco-cms/internal/mock-manager';
import { UmbElementOrElementFolderUserPermissionCondition } from './element-or-element-folder-user-permission.condition.js';
import { UMB_ELEMENT_OR_ELEMENT_FOLDER_USER_PERMISSION_CONDITION_ALIAS } from './constants.js';
import { UMB_USER_PERMISSION_ELEMENT_READ } from '../constants.js';
import { UMB_USER_PERMISSION_ELEMENT_FOLDER_READ } from '../../folder/user-permissions/constants.js';
import { UMB_ELEMENT_ENTITY_TYPE } from '../../entity.js';

@customElement('test-controller-host-combined-permission')
class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {
currentUserContext = new UmbCurrentUserContext(this);
entityContext = new UmbEntityContext(this);
ancestorsContext = new UmbAncestorsEntityContext(this);

constructor() {
super();
new UmbNotificationContext(this);
new UmbCurrentUserStore(this);
}

async init() {
await this.currentUserContext.load();
}

setEntity(entity: UmbEntityModel) {
this.entityContext.setUnique(entity.unique);
this.entityContext.setEntityType(entity.entityType);
}

setEntityAncestors(ancestors: Array<UmbEntityModel>) {
this.ancestorsContext.setAncestors(ancestors);
}
}

const CONDITION_CONFIG = {
alias: UMB_ELEMENT_OR_ELEMENT_FOLDER_USER_PERMISSION_CONDITION_ALIAS,
element: { allOf: [UMB_USER_PERMISSION_ELEMENT_READ] },
folder: { allOf: [UMB_USER_PERMISSION_ELEMENT_FOLDER_READ] },
};

describe('UmbElementOrElementFolderUserPermissionCondition', () => {
let hostElement: UmbTestControllerHostElement;
let condition: UmbElementOrElementFolderUserPermissionCondition;

before(async () => {
await useMockSet('userPermissions');
});

beforeEach(async () => {
hostElement = new UmbTestControllerHostElement();
document.body.appendChild(hostElement);
await hostElement.init();
});

afterEach(() => {
document.body.innerHTML = '';
});

describe('Element read only', () => {
it('should be permitted when user has element read but not folder read', (done) => {
hostElement.setEntity({ unique: 'permissions-element-read-only-id', entityType: UMB_ELEMENT_ENTITY_TYPE });
hostElement.setEntityAncestors([]);

condition = new UmbElementOrElementFolderUserPermissionCondition(hostElement, {
host: hostElement,
config: CONDITION_CONFIG,
onChange: () => {
expect(condition.permitted).to.be.true;
condition.hostDisconnected();
done();
},
});
});

Check warning on line 78 in src/Umbraco.Web.UI.Client/src/packages/elements/user-permissions/conditions/element-or-element-folder-user-permission.condition.test.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Code Duplication

The module contains 3 functions with similar structure: 'should be permitted when user has both element read and folder read','should be permitted when user has element read but not folder read','should be permitted when user has folder read but not element read'. Avoid duplicated, aka copy-pasted, code inside the module. More duplication lowers the code health.
});

describe('Folder read only', () => {
it('should be permitted when user has folder read but not element read', (done) => {
hostElement.setEntity({ unique: 'permissions-folder-read-only-id', entityType: UMB_ELEMENT_ENTITY_TYPE });
hostElement.setEntityAncestors([]);

condition = new UmbElementOrElementFolderUserPermissionCondition(hostElement, {
host: hostElement,
config: CONDITION_CONFIG,
onChange: () => {
expect(condition.permitted).to.be.true;
condition.hostDisconnected();
done();
},
});
});
});

describe('Both read', () => {
it('should be permitted when user has both element read and folder read', (done) => {
hostElement.setEntity({ unique: 'permissions-both-read-id', entityType: UMB_ELEMENT_ENTITY_TYPE });
hostElement.setEntityAncestors([]);

condition = new UmbElementOrElementFolderUserPermissionCondition(hostElement, {
host: hostElement,
config: CONDITION_CONFIG,
onChange: () => {
expect(condition.permitted).to.be.true;
condition.hostDisconnected();
done();
},
});
});
});

describe('Neither read', () => {
it('should not be permitted when user has neither element read nor folder read', (done) => {
hostElement.setEntity({ unique: 'permissions-neither-read-id', entityType: UMB_ELEMENT_ENTITY_TYPE });
hostElement.setEntityAncestors([]);

condition = new UmbElementOrElementFolderUserPermissionCondition(hostElement, {
host: hostElement,
config: CONDITION_CONFIG,
onChange: () => {
// onChange fires only when permitted transitions; if it ever transitions to true that is a failure
if (condition.permitted) {
condition.hostDisconnected();
done(new Error('Expected condition to remain not permitted'));
}
},
});

// Safe to assert after 200ms: mock contexts resolve synchronously, so both
// sub-conditions have evaluated well within this window. The initial default
// is also false, so a pass here confirms the condition never became true.
setTimeout(() => {
expect(condition.permitted).to.be.false;
condition.hostDisconnected();
done();
Comment thread
leekelleher marked this conversation as resolved.
}, 200);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { UmbElementOrElementFolderUserPermissionConditionConfig } from './types.js';
import { UMB_ELEMENT_USER_PERMISSION_CONDITION_ALIAS } from './constants.js';
import { UmbElementUserPermissionCondition } from './element-user-permission.condition.js';
import { UmbElementFolderUserPermissionCondition } from '../../folder/user-permissions/conditions/element-folder-user-permission.condition.js';
import { UMB_ELEMENT_FOLDER_USER_PERMISSION_CONDITION_ALIAS } from '../../folder/user-permissions/conditions/constants.js';
import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbConditionControllerArguments, UmbExtensionCondition } from '@umbraco-cms/backoffice/extension-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';

/** Permits when the user has the configured element OR element folder (container) permissions. */
export class UmbElementOrElementFolderUserPermissionCondition
extends UmbConditionBase<UmbElementOrElementFolderUserPermissionConditionConfig>
implements UmbExtensionCondition
{
#elementCondition: UmbElementUserPermissionCondition | undefined;
#folderCondition: UmbElementFolderUserPermissionCondition | undefined;

constructor(
host: UmbControllerHost,
args: UmbConditionControllerArguments<UmbElementOrElementFolderUserPermissionConditionConfig>,
) {
super(host, args);

if (this.config.element) {
this.#elementCondition = new UmbElementUserPermissionCondition(this, {
host: this,
config: { alias: UMB_ELEMENT_USER_PERMISSION_CONDITION_ALIAS, ...this.config.element },
onChange: () => this.#evaluate(),
});
}

if (this.config.folder) {
this.#folderCondition = new UmbElementFolderUserPermissionCondition(this, {
host: this,
config: { alias: UMB_ELEMENT_FOLDER_USER_PERMISSION_CONDITION_ALIAS, ...this.config.folder },
onChange: () => this.#evaluate(),
});
}
}

#evaluate() {
this.permitted = (this.#elementCondition?.permitted ?? false) || (this.#folderCondition?.permitted ?? false);
}
Comment thread
leekelleher marked this conversation as resolved.
}

export { UmbElementOrElementFolderUserPermissionCondition as api };
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { UmbElementOrElementFolderUserPermissionCondition } from './element-or-element-folder-user-permission.condition.js';
export { UmbElementUserPermissionCondition } from './element-user-permission.condition.js';
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { UMB_ELEMENT_USER_PERMISSION_CONDITION_ALIAS } from './constants.js';
import {
UMB_ELEMENT_OR_ELEMENT_FOLDER_USER_PERMISSION_CONDITION_ALIAS,
UMB_ELEMENT_USER_PERMISSION_CONDITION_ALIAS,
} from './constants.js';
import { UmbElementOrElementFolderUserPermissionCondition } from './element-or-element-folder-user-permission.condition.js';
import { UmbElementUserPermissionCondition } from './element-user-permission.condition.js';

export const manifests: Array<UmbExtensionManifest> = [
Expand All @@ -8,4 +12,10 @@ export const manifests: Array<UmbExtensionManifest> = [
alias: UMB_ELEMENT_USER_PERMISSION_CONDITION_ALIAS,
api: UmbElementUserPermissionCondition,
},
{
type: 'condition',
name: 'Element or Element Folder User Permission Condition',
alias: UMB_ELEMENT_OR_ELEMENT_FOLDER_USER_PERMISSION_CONDITION_ALIAS,
api: UmbElementOrElementFolderUserPermissionCondition,
},
];
Loading
Loading