From b941108cd84463ece2eb137b676142421dbab3ae Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 22 Mar 2024 13:59:38 +0100 Subject: [PATCH 1/2] add json schema `allowPublicAccess` --- .../src/packages/core/extension-registry/umbraco-package.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/umbraco-package.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/umbraco-package.ts index 7663b53c2c..3a05a2d7f0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/umbraco-package.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/umbraco-package.ts @@ -27,6 +27,12 @@ export interface UmbracoPackage { */ allowTelemetry?: boolean; + /** + * @title Decides if the package is allowed to be accessed by the public, e.g. on the login screen + * @default false + */ + allowPublicAccess?: boolean; + /** * @title An array of Umbraco package manifest types that will be installed * @required From daede7ab8a243c420d6a6c1b8739b91053b31055 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 22 Mar 2024 14:00:09 +0100 Subject: [PATCH 2/2] add method to allow loading public extensions and ensure the backoffice still loads all extensions --- .../src/apps/backoffice/backoffice.element.ts | 2 +- ...server-extension-registrator.controller.ts | 108 ++++++++++-------- 2 files changed, 62 insertions(+), 48 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts b/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts index 51c2146779..4a5456b67f 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts @@ -55,7 +55,7 @@ export class UmbBackofficeElement extends UmbLitElement { new UmbBackofficeContext(this); new UmbBundleExtensionInitializer(this, umbExtensionsRegistry); new UmbEntryPointExtensionInitializer(this, umbExtensionsRegistry); - new UmbServerExtensionRegistrator(this, umbExtensionsRegistry); + new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerAllExtensions(); // So far local packages are this simple to registerer, so no need for a manager to do that: CORE_PACKAGES.forEach(async (packageImport) => { diff --git a/src/Umbraco.Web.UI.Client/src/apps/backoffice/server-extension-registrator.controller.ts b/src/Umbraco.Web.UI.Client/src/apps/backoffice/server-extension-registrator.controller.ts index 0f13864082..58226d9dea 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/backoffice/server-extension-registrator.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/backoffice/server-extension-registrator.controller.ts @@ -1,4 +1,8 @@ -import { PackageResource, OpenAPI } from '@umbraco-cms/backoffice/external/backend-api'; +import { + PackageResource, + OpenAPI, + type PackageManifestResponseModel, +} from '@umbraco-cms/backoffice/external/backend-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbBackofficeExtensionRegistry } from '@umbraco-cms/backoffice/extension-registry'; @@ -14,56 +18,66 @@ export class UmbServerExtensionRegistrator extends UmbControllerBase { constructor(host: UmbControllerHost, extensionRegistry: UmbBackofficeExtensionRegistry) { super(host, UmbServerExtensionRegistrator.name); this.#extensionRegistry = extensionRegistry; - this.#loadServerPackages(); } - async #loadServerPackages() { - /* TODO: we need a new endpoint here, to remove the dependency on the package repository, to get the modules available for the backoffice scope - / we will need a similar endpoint for the login, installer etc at some point. - We should expose more information about the packages when not authorized so the end point should only return a list of modules from the manifest with - with the correct scope. - - This code is copy pasted from the package repository. We probably don't need this is the package repository anymore. - */ + /** + * Registers all extensions from the server. + * This is used to register all extensions that are available to the user (including private extensions). + * @remark Users must have the BACKOFFICE_ACCESS permission to access this method. + */ + public async registerAllExtensions() { const { data: packages } = await tryExecuteAndNotify(this, PackageResource.getPackageManifest()); - if (packages) { - // Append packages to the store but only if they have a name - //store.appendItems(packages.filter((p) => p.name?.length)); - const extensions: ManifestBase[] = []; - - packages.forEach((p) => { - p.extensions?.forEach((e) => { - // Crudely validate that the extension at least follows a basic manifest structure - // Idea: Use `Zod` to validate the manifest - if (isManifestBaseType(e)) { - /** - * Crude check to see if extension is of type "js" since it is safe to assume we do not - * need to load any other types of extensions in the backoffice (we need a js file to load) - */ - - // TODO: add helper to check for relative paths - // Add base url if the js path is relative - if ('js' in e && typeof e.js === 'string' && !e.js.startsWith('http')) { - e.js = `${this.#apiBaseUrl}${e.js}`; - } - - // Add base url if the element path is relative - if ('element' in e && typeof e.element === 'string' && !e.element.startsWith('http')) { - e.element = `${this.#apiBaseUrl}${e.element}`; - } - - // Add base url if the element path api relative - if ('api' in e && typeof e.api === 'string' && !e.api.startsWith('http')) { - e.api = `${this.#apiBaseUrl}${e.api}`; - } - - extensions.push(e); - } - }); - }); - - this.#extensionRegistry.registerMany(extensions); + await this.#loadServerPackages(packages); } } + + /** + * Registers all public extensions from the server. + * This is used to register all extensions that are available to the user (excluding private extensions) such as login extensions. + * @remark Any user can access this method without any permissions. + */ + public async registerPublicExtensions() { + const { data: packages } = await tryExecuteAndNotify(this, PackageResource.getPackageManifestPublic()); + if (packages) { + await this.#loadServerPackages(packages); + } + } + + async #loadServerPackages(packages: PackageManifestResponseModel[]) { + const extensions: ManifestBase[] = []; + + packages.forEach((p) => { + p.extensions?.forEach((e) => { + // Crudely validate that the extension at least follows a basic manifest structure + // Idea: Use `Zod` to validate the manifest + if (isManifestBaseType(e)) { + /** + * Crude check to see if extension is of type "js" since it is safe to assume we do not + * need to load any other types of extensions in the backoffice (we need a js file to load) + */ + + // TODO: add helper to check for relative paths + // Add base url if the js path is relative + if ('js' in e && typeof e.js === 'string' && !e.js.startsWith('http')) { + e.js = `${this.#apiBaseUrl}${e.js}`; + } + + // Add base url if the element path is relative + if ('element' in e && typeof e.element === 'string' && !e.element.startsWith('http')) { + e.element = `${this.#apiBaseUrl}${e.element}`; + } + + // Add base url if the element path api relative + if ('api' in e && typeof e.api === 'string' && !e.api.startsWith('http')) { + e.api = `${this.#apiBaseUrl}${e.api}`; + } + + extensions.push(e); + } + }); + }); + + this.#extensionRegistry.registerMany(extensions); + } }