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
@@ -0,0 +1,4 @@
// eslint-disable-next-line @typescript-eslint/naming-convention
export const EXAMPLE_USER_PERMISSION_DICTIONARY_ACTION_1 = 'Example.Dictionary.Action1';
// eslint-disable-next-line @typescript-eslint/naming-convention
export const EXAMPLE_USER_PERMISSION_DICTIONARY_ACTION_2 = 'Example.Dictionary.Action2';
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';

export class ExampleAction1EntityAction extends UmbEntityActionBase<never> {
override async execute() {
alert('Example action 1 executed');
}
}

export { ExampleAction1EntityAction as default };
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';

export class ExampleAction2EntityAction extends UmbEntityActionBase<never> {
override async execute() {
alert('Example action 2 executed');
}
}

export { ExampleAction2EntityAction as default };
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
EXAMPLE_USER_PERMISSION_DICTIONARY_ACTION_1,
EXAMPLE_USER_PERMISSION_DICTIONARY_ACTION_2,
} from '../constants.js';
import { UMB_DICTIONARY_ENTITY_TYPE } from '@umbraco-cms/backoffice/dictionary';
import { UMB_FALLBACK_USER_PERMISSION_CONDITION_ALIAS } from '@umbraco-cms/backoffice/user-permission';

export const manifests: Array<UmbExtensionManifest> = [
{
type: 'entityAction',
kind: 'default',
alias: 'Example.EntityAction.Action1',
name: 'Action 1 Entity Action',
forEntityTypes: [UMB_DICTIONARY_ENTITY_TYPE],
api: () => import('./action-1.js'),
weight: 10000,
meta: {
label: 'Action 1',
icon: 'icon-car',
},
conditions: [
{
alias: UMB_FALLBACK_USER_PERMISSION_CONDITION_ALIAS,
allOf: [EXAMPLE_USER_PERMISSION_DICTIONARY_ACTION_1],
},
],
},
{
type: 'entityAction',
kind: 'default',
alias: 'Example.EntityAction.Action2',
name: 'Action 2 Entity Action',
forEntityTypes: [UMB_DICTIONARY_ENTITY_TYPE],
api: () => import('./action-2.js'),
weight: 9000,
meta: {
label: 'Action 2',
icon: 'icon-bus',
},
conditions: [
{
alias: UMB_FALLBACK_USER_PERMISSION_CONDITION_ALIAS,
allOf: [EXAMPLE_USER_PERMISSION_DICTIONARY_ACTION_2],
},
],
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {
EXAMPLE_USER_PERMISSION_DICTIONARY_ACTION_1,
EXAMPLE_USER_PERMISSION_DICTIONARY_ACTION_2,
} from '../constants.js';
import { UMB_DICTIONARY_ENTITY_TYPE } from '@umbraco-cms/backoffice/dictionary';

export const manifests: Array<UmbExtensionManifest> = [
{
type: 'entityUserPermission',
alias: 'Example.EntityUserPermission.Entity.Action1',
name: 'Action 1 for Entity User Permission',
forEntityTypes: [UMB_DICTIONARY_ENTITY_TYPE],
meta: {
verbs: [EXAMPLE_USER_PERMISSION_DICTIONARY_ACTION_1],
label: 'Action 1',
description: 'Description for action 1',
},
},
{
type: 'entityUserPermission',
alias: 'Example.EntityUserPermission.Entity.Action2',
name: 'Action 2 for Entity User Permission',
forEntityTypes: [UMB_DICTIONARY_ENTITY_TYPE],
meta: {
verbs: [EXAMPLE_USER_PERMISSION_DICTIONARY_ACTION_2],
label: 'Action 2',
description: 'Description for action 2',
},
},
];
9 changes: 9 additions & 0 deletions src/Umbraco.Web.UI.Client/examples/user-permission/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { manifests as entityUserPermissionManifests } from './entity-user-permission/manifests.js';
import { manifests as entityActionManifests } from './entity-action/manifests.js';
import { manifests as localizationManifests } from './localization/manifests.js';

export const manifests: Array<UmbExtensionManifest> = [
...entityUserPermissionManifests,
...entityActionManifests,
...localizationManifests,
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
user: {
// eslint-disable-next-line @typescript-eslint/naming-convention
permissionsEntityGroup_dictionary: 'Dictionary',
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const manifests: Array<UmbExtensionManifest> = [
{
type: 'localization',
alias: 'Example.UserPermission.Localization.En',
name: 'en-US User Permission Localization Example',
js: () => import('./en.js'),
weight: 1,
meta: {
culture: 'en',
},
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './fallback-permission-condition/constants.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const UMB_FALLBACK_USER_PERMISSION_CONDITION_ALIAS = 'Umb.Condition.UserPermission.Fallback';
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 { UmbFallbackUserPermissionCondition } from './fallback-user-permission.condition';
import { UMB_USER_PERMISSION_DOCUMENT_READ } from '@umbraco-cms/backoffice/document';
import { UMB_FALLBACK_USER_PERMISSION_CONDITION_ALIAS } from './constants';

@customElement('test-controller-host')
class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {
currentUserContext = new UmbCurrentUserContext(this);

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

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

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

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

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

describe('Fallback Permissions', () => {
it('should permit the condition when allOf is satisfied', (done) => {
let callbackCount = 0;

// We expect to find the read permission in the fallback permissions
condition = new UmbFallbackUserPermissionCondition(hostElement, {
host: hostElement,
config: {
alias: UMB_FALLBACK_USER_PERMISSION_CONDITION_ALIAS,
allOf: [UMB_USER_PERMISSION_DOCUMENT_READ],
},
onChange: () => {
callbackCount++;
if (callbackCount === 1) {
expect(condition.permitted).to.be.true;
condition.hostDisconnected();
done();
}
},
});
});

Check warning on line 59 in src/Umbraco.Web.UI.Client/src/packages/user/user-permission/fallback-permission-condition/fallback-user-permission.condition.test.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

❌ New issue: Code Duplication

The module contains 4 functions with similar structure: 'should forbid the condition when allOf is not satisfied','should forbid the condition when oneOf is not satisfied','should permit the condition when allOf is satisfied','should permit the condition when oneOf is satisfied'. Avoid duplicated, aka copy-pasted, code inside the module. More duplication lowers the code health.

it('should forbid the condition when allOf is not satisfied', (done) => {
let callbackCount = 0;

// We expect to find the read permission in the fallback permissions
condition = new UmbFallbackUserPermissionCondition(hostElement, {
host: hostElement,
config: {
alias: UMB_FALLBACK_USER_PERMISSION_CONDITION_ALIAS,
allOf: [UMB_USER_PERMISSION_DOCUMENT_READ, 'non-existing-permission'],
},
onChange: () => {
callbackCount++;
if (callbackCount === 1) {
expect(condition.permitted).to.be.false;
condition.hostDisconnected();
done();
}
},
});

// The onChange callback is not called when the condition is false, so we need to wait and check manually
setTimeout(() => {
expect(condition.permitted).to.be.false;
condition.hostDisconnected();
done();
}, 200);
});

it('should permit the condition when oneOf is satisfied', (done) => {
let callbackCount = 0;

// We expect to find the read permission in the fallback permissions
condition = new UmbFallbackUserPermissionCondition(hostElement, {
host: hostElement,
config: {
alias: UMB_FALLBACK_USER_PERMISSION_CONDITION_ALIAS,
oneOf: [UMB_USER_PERMISSION_DOCUMENT_READ, 'non-existing-permission'],
},
onChange: () => {
/* The onChange callback is not called when the condition is false, so this should never be called
But in case it is, we want to fail the test */
callbackCount++;
if (callbackCount === 1) {
expect(condition.permitted).to.be.true;
condition.hostDisconnected();
done();
}
},
});
});

it('should forbid the condition when oneOf is not satisfied', (done) => {
let callbackCount = 0;

// We expect to find the read permission in the fallback permissions
condition = new UmbFallbackUserPermissionCondition(hostElement, {
host: hostElement,
config: {
alias: UMB_FALLBACK_USER_PERMISSION_CONDITION_ALIAS,
oneOf: ['non-existing-permission', 'another-non-existing-permission'],
},
onChange: () => {
/* The onChange callback is not called when the condition is false, so this should never be called
But in case it is, we want to fail the test */
callbackCount++;
if (callbackCount === 1) {
expect(condition.permitted).to.be.false;
condition.hostDisconnected();
done();
}
},
});

// The onChange callback is not called when the condition is false, so we need to wait and check manually
setTimeout(() => {
expect(condition.permitted).to.be.false;
condition.hostDisconnected();
done();
}, 200);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { UmbFallbackUserPermissionConditionConfig } from './types.js';
import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user';
import type { UmbConditionControllerArguments, UmbExtensionCondition } from '@umbraco-cms/backoffice/extension-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry';

export class UmbFallbackUserPermissionCondition
extends UmbConditionBase<UmbFallbackUserPermissionConditionConfig>
implements UmbExtensionCondition
{
constructor(
host: UmbControllerHost,
args: UmbConditionControllerArguments<UmbFallbackUserPermissionConditionConfig>,
) {
super(host, args);

this.consumeContext(UMB_CURRENT_USER_CONTEXT, (context) => {
this.observe(
context?.currentUser,
(currentUser) => {
const fallbackPermissions = currentUser?.fallbackPermissions || [];
this.#check(fallbackPermissions);
},
'umbUserFallbackPermissionConditionObserver',
);
});
}

#check(verbs: Array<string>) {
/* we default to true se we don't require both allOf and oneOf to be defined
but they can be combined for more complex scenarios */
let allOfPermitted = true;
let oneOfPermitted = true;

// check if all of the verbs are present
if (this.config.allOf?.length) {
allOfPermitted = this.config.allOf.every((verb) => verbs.includes(verb));
}

// check if at least one of the verbs is present
if (this.config.oneOf?.length) {
oneOfPermitted = this.config.oneOf.some((verb) => verbs.includes(verb));
}

// if neither allOf or oneOf is defined we default to false
if (!allOfPermitted && !oneOfPermitted) {
allOfPermitted = false;
oneOfPermitted = false;
}

this.permitted = allOfPermitted && oneOfPermitted;
}
}

export { UmbFallbackUserPermissionCondition as api };
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { UMB_FALLBACK_USER_PERMISSION_CONDITION_ALIAS } from './constants.js';

export const manifests: Array<UmbExtensionManifest> = [
{
type: 'condition',
name: 'User Fallback Permission Condition',
alias: UMB_FALLBACK_USER_PERMISSION_CONDITION_ALIAS,
api: () => import('./fallback-user-permission.condition.js'),
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { UmbConditionConfigBase } from '@umbraco-cms/backoffice/extension-api';

export type UmbFallbackUserPermissionConditionConfig =
UmbConditionConfigBase<'Umb.Condition.UserPermission.Fallback'> & {
/**
* The user must have all of the permissions in this array for the condition to be met.
* @example
* ["Umb.PermissionOne", "Umb.PermissionTwo"]
*/
allOf?: Array<string>;

/**
* The user must have at least one of the permissions in this array for the condition to be met.
* @example
* ["Umb.PermissionOne", "Umb.PermissionTwo"]
*/
oneOf?: Array<string>;
};

declare global {
interface UmbExtensionConditionConfigMap {
UmbFallbackUserPermissionConditionConfig: UmbFallbackUserPermissionConditionConfig;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './components/index.js';
export * from './constants.js';
export * from './modals/index.js';

export type * from './types.js';
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { manifests as userPermissionModalManifests } from './modals/manifests.js';
import { manifests as fallbackConditionPermission } from './fallback-permission-condition/manifests.js';

export const manifests: Array<UmbExtensionManifest> = [...userPermissionModalManifests];
export const manifests: Array<UmbExtensionManifest> = [...userPermissionModalManifests, ...fallbackConditionPermission];
Loading