Skip to content

Commit

Permalink
Add "elevated privileges" to extensions and make managing extensions …
Browse files Browse the repository at this point in the history
…one (#1012)

Adds the notion of "elevated privileges" for extensions to give them abilities if requested in their manifest. The first of these privileges gives extensions the right to manage new extensions via installing, enabling, and disabling them.
  • Loading branch information
lyonsil authored Jul 18, 2024
1 parent 9e44d10 commit fcae883
Show file tree
Hide file tree
Showing 10 changed files with 524 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ const registerCheck = async (
// #region Initialize the check runner

let initializationPromise: Promise<void> | undefined;
const unsubscribers = new UnsubscriberAsyncList();
async function initialize(): Promise<void> {
if (!initializationPromise) {
initializationPromise = new Promise<void>((resolve, reject) => {
Expand All @@ -318,7 +319,10 @@ async function initialize(): Promise<void> {
checkRunnerEngine,
CHECK_RUNNER_NETWORK_OBJECT_TYPE,
);
await papi.commands.registerCommand('platformScripture.registerCheck', registerCheck);
unsubscribers.add(dataProvider.dispose);
unsubscribers.add(
await papi.commands.registerCommand('platformScripture.registerCheck', registerCheck),
);
resolve();
} catch (error) {
reject(error);
Expand All @@ -334,7 +338,7 @@ async function initialize(): Promise<void> {

const checkHostingService: ICheckHostingService = {
initialize,
dispose: async () => dataProvider.dispose(),
dispose: async () => unsubscribers.runAllUnsubscribers(),
getCheckRunner: async () => {
await initialize();
return dataProvider;
Expand Down
130 changes: 129 additions & 1 deletion lib/papi-dts/papi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4333,6 +4333,13 @@ declare module 'node/services/node-file-system.service' {
destinationUri: Uri,
mode?: Parameters<typeof fs.promises.copyFile>[2],
): Promise<void>;
/**
* Moves a file from one location to another
*
* @param sourceUri The location of the file to move
* @param destinationUri The uri where the file should be moved
*/
export function moveFile(sourceUri: Uri, destinationUri: Uri): Promise<void>;
/**
* Delete a file if it exists
*
Expand Down Expand Up @@ -4405,6 +4412,19 @@ declare module 'node/utils/crypto-util' {
* @returns Cryptographically secure, pseudo-randomly generated value encoded as a string
*/
export function createNonce(encoding: 'base64url' | 'hex', numberOfBytes?: number): string;
/**
* Calculates the hash of a given data buffer
*
* @param hashAlgorithm Name of the hash algorithm to use, such as "sha512"
* @param encodingType String encoding to use for returning the binary hash value that is calculated
* @param buffer Raw data to be fed into the hash algorithm
* @returns String encoded value of the digest (https://csrc.nist.gov/glossary/term/hash_digest)
*/
export function generateHashFromBuffer(
hashAlgorithm: string,
encodingType: 'base64' | 'base64url' | 'hex' | 'binary',
buffer: Buffer,
): string;
}
declare module 'node/models/execution-token.model' {
/** For now this is just for extensions, but maybe we will want to expand this in the future */
Expand Down Expand Up @@ -4762,15 +4782,120 @@ declare module 'shared/services/dialog.service' {
const dialogService: DialogService;
export default dialogService;
}
declare module 'shared/models/manage-extensions-privilege.model' {
/** Base64 encoded hash values */
export type HashValues = Partial<{
sha256: string;
sha512: string;
}>;
/** Represents an extension that can be enabled or disabled */
export type ExtensionIdentifier = {
extensionName: string;
extensionVersion: string;
};
/**
* Represents all extensions that are installed. Note that packaged extensions cannot be disabled,
* so they are implied to always be enabled.
*/
export type InstalledExtensions = {
/**
* Extensions that are explicitly bundled to be part of the application. They cannot be disabled.
* At runtime no extensions can be added or removed from the set of packaged extensions.
*/
packaged: ExtensionIdentifier[];
/**
* Extensions that are running but can be dynamically disabled. At runtime extensions can be added
* or removed from the set of enabled extensions.
*/
enabled: ExtensionIdentifier[];
/**
* Extensions that are not running but can be dynamically enabled. At runtime extensions can be
* added or removed from the set of disabled extensions.
*
* The only difference between a disabled extension and an extension that isn't installed is that
* disabled extensions do not need to be downloaded again to run them.
*/
disabled: ExtensionIdentifier[];
};
/**
* Download an extension from a given URL and enable it
*
* @param extensionUrlToDownload URL to the extension ZIP file to download
* @param fileSize Expected size of the file
* @param fileHashes Hash value(s) of the file to download. Note that only one hash value may be
* validated, but multiple hash values may be provided so the installer can choose any of them for
* validation. For example, if you provide a sha256 hash value and a sha512 hash value, the
* installer may only use the sha512 hash value for validation.
* @returns Promise that resolves when the extension has been installed
*/
export type InstallExtensionFunction = (
extensionUrlToDownload: string,
fileSize: number,
fileHashes: HashValues,
) => Promise<void>;
/**
* Start running an extension that had been previously downloaded and disabled
*
* @param extensionIdentifier Details of the extension to enable
* @returns Promise that resolves when the extension has been enabled, throws if enabling fails
*/
export type EnableExtensionFunction = (extensionIdentifier: ExtensionIdentifier) => Promise<void>;
/**
* Stop running an extension that had been previously downloaded and enabled
*
* @param extensionIdentifier Details of the extension to disable
* @returns Promise that resolves when the extension has been enabled, throws if enabling fails
*/
export type DisableExtensionFunction = (
extensionIdentifier: ExtensionIdentifier,
) => Promise<void>;
/** Get extension identifiers of all extensions on the system */
export type GetInstalledExtensionsFunction = () => Promise<InstalledExtensions>;
/** Functions needed to manage extensions */
export type ManageExtensions = {
/** Function to download an extension and enable it */
installExtension: InstallExtensionFunction;
/** Function to start running an extension that had been previously downloaded and disabled */
enableExtension: EnableExtensionFunction;
/** Function to stop running an extension that had been previously downloaded and enabled */
disableExtension: DisableExtensionFunction;
/** Function to retrieve details about all installed extensions */
getInstalledExtensions: GetInstalledExtensionsFunction;
};
}
declare module 'shared/models/elevated-privileges.model' {
import { ManageExtensions } from 'shared/models/manage-extensions-privilege.model';
/** String constants that are listed in an extension's manifest.json to state needed privileges */
export enum ElevatedPrivilegeNames {
manageExtensions = 'manageExtensions',
}
/** Object that contains properties with special capabilities for extensions that required them */
export type ElevatedPrivileges = {
/** Functions that can be run to manage what extensions are running */
manageExtensions: ManageExtensions | undefined;
};
}
declare module 'extension-host/extension-types/extension-activation-context.model' {
import { ExecutionToken } from 'node/models/execution-token.model';
import { UnsubscriberAsyncList } from 'platform-bible-utils';
import { ElevatedPrivileges } from 'shared/models/elevated-privileges.model';
/** An object of this type is passed into `activate()` for each extension during initialization */
export type ExecutionActivationContext = {
/** Canonical name of the extension */
name: string;
/** Used to save and load data from the storage service. */
/** Used to save and load data by the storage service. */
executionToken: ExecutionToken;
/**
* Objects that provide special capabilities required by an extension based on the
* `elevatedPrivileges` values listed in its manifest. For example, if an extension needs to be
* able to manage other extensions, then it should include `manageExtensions` in the
* `elevatedPrivileges` array in `manifest.json`. Then when the extension is activated this
* {@link ElevatedPrivileges} object will have the `manageExtensions` property set to an object
* with functions used to manage extensions.
*
* See {@link ElevatedPrivilegeNames} for the full list of elevated privileges available.
*/
elevatedPrivileges: ElevatedPrivileges;
/** Tracks all registrations made by an extension so they can be cleaned up when it is unloaded */
registrations: UnsubscriberAsyncList;
};
Expand Down Expand Up @@ -5867,6 +5992,7 @@ declare module 'extension-host/extension-types/extension.interface' {
}
}
declare module 'extension-host/extension-types/extension-manifest.model' {
import { ElevatedPrivilegeNames } from 'shared/models/elevated-privileges.model';
/** Information about an extension provided by the extension developer. */
export type ExtensionManifest = {
/** Name of the extension */
Expand All @@ -5884,6 +6010,8 @@ declare module 'extension-host/extension-types/extension-manifest.model' {
* Must be specified. Can be an empty string if the extension does not have any JavaScript to run.
*/
main: string;
/** List of special permissions required by the extension to work as intended */
elevatedPrivileges: `${ElevatedPrivilegeNames}`[];
/**
* Path to the TypeScript type declaration file that describes this extension and its interactions
* on the PAPI. Relative to the extension's root folder.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
import { ExecutionToken } from '@node/models/execution-token.model';
import { UnsubscriberAsyncList } from 'platform-bible-utils';
import {
ElevatedPrivileges,
// Needed for documentation links to work
// eslint-disable-next-line @typescript-eslint/no-unused-vars
ElevatedPrivilegeNames,
} from '@shared/models/elevated-privileges.model';

/** An object of this type is passed into `activate()` for each extension during initialization */
export type ExecutionActivationContext = {
/** Canonical name of the extension */
name: string;
/** Used to save and load data from the storage service. */
/** Used to save and load data by the storage service. */
executionToken: ExecutionToken;
/**
* Objects that provide special capabilities required by an extension based on the
* `elevatedPrivileges` values listed in its manifest. For example, if an extension needs to be
* able to manage other extensions, then it should include `manageExtensions` in the
* `elevatedPrivileges` array in `manifest.json`. Then when the extension is activated this
* {@link ElevatedPrivileges} object will have the `manageExtensions` property set to an object
* with functions used to manage extensions.
*
* See {@link ElevatedPrivilegeNames} for the full list of elevated privileges available.
*/
elevatedPrivileges: ElevatedPrivileges;
/** Tracks all registrations made by an extension so they can be cleaned up when it is unloaded */
registrations: UnsubscriberAsyncList;
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ElevatedPrivilegeNames } from '@shared/models/elevated-privileges.model';

/** Information about an extension provided by the extension developer. */
export type ExtensionManifest = {
/** Name of the extension */
Expand All @@ -15,6 +17,8 @@ export type ExtensionManifest = {
* Must be specified. Can be an empty string if the extension does not have any JavaScript to run.
*/
main: string;
/** List of special permissions required by the extension to work as intended */
elevatedPrivileges: `${ElevatedPrivilegeNames}`[];
/**
* Path to the TypeScript type declaration file that describes this extension and its interactions
* on the PAPI. Relative to the extension's root folder.
Expand Down
Loading

0 comments on commit fcae883

Please sign in to comment.