UNPKG

@microsoft/windows-admin-center-sdk

Version:

Microsoft - Windows Admin Center Shell

619 lines (617 loc) 27.1 kB
import { EMPTY, of } from 'rxjs'; import { catchError, expand, filter, map, take } from 'rxjs/operators'; import { Net } from '../data/net'; import { PowerShell } from '../data/powershell'; import { LogLevel } from '../diagnostics/log-level'; import { Logging } from '../diagnostics/logging'; import { EnvironmentModule, EnvironmentModuleToolState } from '../manifest/environment-modules'; import { GatewayMode } from '../shared/gateway-inventory/gateway-inventory'; import { ToolInventoryCache } from '../shared/tool-inventory/tool-inventory-cache'; /** * The class handles conditions of tools to be presented on tools' menu. * @dynamic */ export class ToolConditionValidator { appContext; /** * Support the following condition name. * It can be a string, number, boolean and version string. */ static serverInventoryProperties = [ // string { conditionName: 'computerManufacturer', dataName: 'computerManufacturer' }, // string { conditionName: 'computerModel', dataName: 'computerModel' }, // number { conditionName: 'operatingSystemSKU', dataName: 'operatingSystemSKU' }, // version string { conditionName: 'operatingSystemVersion', dataName: 'operatingSystemVersion' }, // number { conditionName: 'windowsProductType', dataName: 'productType' }, // string { conditionName: 'clusterFqdn', dataName: 'clusterFqdn' }, // string { conditionName: 'systemLockdownPolicy', dataName: 'systemLockdownPolicy' }, // boolean { conditionName: 'isHyperVRoleInstalled', dataName: 'isHyperVRoleInstalled' }, // boolean { conditionName: 'isHyperVPowershellInstalled', dataName: 'isHyperVPowershellInstalled' }, // boolean { conditionName: 'isManagementToolsAvailable', dataName: 'isManagementToolsAvailable' }, // boolean { conditionName: 'isWmfInstalled', dataName: 'isWmfInstalled' }, // boolean { conditionName: 'isRemoteAppEnabled', dataName: 'isRemoteAppEnabled' }, // boolean { conditionName: 'isDomainController', dataName: 'isDomainController' }, // boolean { conditionName: 'isS2dEnabled', dataName: 'isS2dEnabled' }, // boolean { conditionName: 'isBritannicaEnabled', dataName: 'isBritannicaEnabled' }, // boolean { conditionName: 'isHciServer', dataName: 'isHciServer' } ]; /** * The following operators are supported. * String comparison uses caseinsesitive pattern. */ static operators = { /** * Greater than: number, string (caseinsesitive), version */ gt: 'gt', /** * Greater or equal: number, string (caseinsesitive), version */ ge: 'ge', /** * Less than: number, string (caseinsesitive), version */ lt: 'lt', /** * Less or equal: number, string (caseinsesitive), version */ le: 'le', /** * Equal: number, string (caseinsesitive), version (Accept '*' like "1.2.*") */ eq: 'eq', /** * Not equal: number, string (caseinsesitive), version (Accept '*' like "1.2.*") */ ne: 'ne', /** * Test true: number, string, boolean */ is: 'is', /** * Test false: number, string, boolean */ not: 'not', /** * Contains a string: string (caseinsesitive) */ contains: 'contains', /** * Not contains a string: string (caseinsesitive) */ notContains: 'notContains', /** * Any one of value (number or string) equal: numberArray, stringArray */ anyEq: 'anyEq', /** * Any one of value (number or string) not equal: numberArray, stringArray */ anyNe: 'anyNe', /** * Any one of string contains: stringArray */ anyContains: 'anyContains', /** * Any one of string not contains: stringArray */ anyNotContains: 'anyNotContains' }; static internalCurrent; caches; toolInventoryCache; errorStrings = MsftSme.getStrings().MsftSmeShell.Core.Error; /** * Gets the current object of the ToolConditionValidator class, and maintains as singleton. * * @param appContext the application context. * @param caches the instance of the inventory query caches to share the resource. */ static current(appContext, caches) { if (!ToolConditionValidator.internalCurrent) { ToolConditionValidator.internalCurrent = new ToolConditionValidator(appContext, caches); } return ToolConditionValidator.internalCurrent; } /** * Initializes a new instance of the ToolConditionValidator class. * * @param appContext the application context. * @param caches the instance of the inventory query caches to share the resource. */ constructor(appContext, caches) { this.appContext = appContext; const statusExpiration = 4 * 60 * 1000; // 4 min this.caches = caches; this.toolInventoryCache = new ToolInventoryCache(appContext, { expiration: statusExpiration }); } /** * Scan the tool condition to be present or not. * * @param connection the connection object. * @param solution The entry point object of solution. * @param tool The entry point object of tool. * @param scanMode The mode of scanning. * @return the result observable. */ scanToolCondition(connection, solution, tool) { if (!tool.requirements) { // tool is missing requirements, never show. return { weight: 0 /* ToolConditionWeight.NonObservable */, result: { ...tool, ...{ show: false, detail: EnvironmentModuleToolState.NotSupported } } }; } const solutionId = EnvironmentModule.createFormattedEntrypoint(solution); const toolId = EnvironmentModule.createFormattedEntrypoint(tool); const checkersCollection = []; let weight = 0 /* ToolConditionWeight.NonObservable */; for (const requirement of tool.requirements) { const checkers = []; // tool must specify the solutions it can show in if (!requirement.solutionIds || requirement.solutionIds.every(id => id !== solutionId)) { continue; } // if ths solution is a connections type, then the tool must specify the connection type if (solution.rootNavigationBehavior === 'connections' && (!requirement.connectionTypes || requirement.connectionTypes.every(type => type !== connection.type))) { continue; } // if there are no conditions to check, then this tool has been satisfied. if (!requirement.conditions || requirement.conditions.length === 0) { checkers.push(this.installationTypeValidate(connection)); weight = Math.max(weight, 2 /* ToolConditionWeight.Gateway */); } else { for (const condition of requirement.conditions) { checkers.push(this.installationTypeValidate(connection, condition.installationTypes)); weight = Math.max(weight, 2 /* ToolConditionWeight.Gateway */); if (condition.localhost !== undefined && !condition.localhost) { // if connection is localhost and not supported. checkers.push(this.localhostValidate(connection)); weight = Math.max(weight, 2 /* ToolConditionWeight.Gateway */); } if (condition.inventory) { checkers.push(this.inventoryValidate(connection, condition.inventory)); weight = Math.max(weight, 3 /* ToolConditionWeight.Server */); } if (condition.properties) { checkers.push(this.propertyValidate(connection, condition.properties)); weight = Math.max(weight, 1 /* ToolConditionWeight.Property */); } if (condition.powerShell) { // powerShell { command, script } checkers.push(this.toolInventoryValidate(connection, toolId, condition.powerShell)); weight = Math.max(weight, 4 /* ToolConditionWeight.Script */); } else if (condition.script) { // deprecated checkers.push(this.toolInventoryValidate(connection, toolId, condition.script)); weight = Math.max(weight, 4 /* ToolConditionWeight.Script */); } } } checkersCollection.push(checkers); } if (checkersCollection.length === 0) { return { weight: 0 /* ToolConditionWeight.NonObservable */, result: { ...tool, ...{ show: false, detail: EnvironmentModuleToolState.NotSupported, connectionId: connection ? connection.id : null } } }; } let collectionIndex = 0; let checkerIndex = 0; let lastDetail = null; let lastMessage = null; return { weight: weight, result: this.runChecker(checkersCollection[collectionIndex][checkerIndex]) .pipe(catchError(() => { Logging.log({ level: LogLevel.Error, message: this.errorStrings.ToolValidationResult.message.format(tool.parentModule.name, tool.displayName), source: 'ToolConditionValidator' }); return of({ show: false, ends: true }); }), expand((value) => { checkerIndex++; if (value.ends) { return EMPTY; } if (value.detail !== undefined && lastDetail == null) { lastDetail = value.detail; } if (value.message !== undefined && lastMessage == null) { lastMessage = value.message; } if (!value.show) { // failed result. increment collection index and reset checkerIndex. if (checkersCollection.length > ++collectionIndex) { // still has another condition, try next checker set. return this.runChecker(checkersCollection[collectionIndex][checkerIndex = 0]); } else { // no more condition, end to return 'false'. return of({ show: false, detail: lastDetail, message: lastMessage, ends: true }); } } if (checkersCollection[collectionIndex].length > checkerIndex) { // check next checker. return this.runChecker(checkersCollection[collectionIndex][checkerIndex]); } else { // all state/succeeded within the checker set. return of({ show: true, detail: lastDetail, message: lastMessage, ends: true }); } }), filter(combined => combined.ends), map(combined => { return { ...tool, ...{ show: combined.show, detail: combined.detail, message: combined.message, connectionId: connection ? connection.id : null } }; })) }; } runChecker(checker) { return checker.pipe(take(1), map(result => ({ ...result, ends: false }))); } localhostValidate(connection) { return this.caches.gatewayCache.createObservable({}) .pipe(map(instance => ({ show: !(instance.mode !== GatewayMode.Service && connection.properties && connection.properties.displayName === 'localhost') }))); } installationTypeValidate(connection, installationType) { if (MsftSme.getValue(MsftSme.self().Environment.configuration, 'gateway.disabled')) { return of({ show: true }); } if (!installationType || !Array.isArray(installationType) || installationType.length === 0) { // Default to standard if property doesnt exist in the manifest. installationType = ['Standard']; } return this.caches.gatewayCache.createObservable({ params: {} }) .pipe(catchError((error) => { const errorStrings = MsftSme.getStrings().MsftSmeShell.Core.Error; const notification = this.appContext.notification.create(connection.name); notification.showError(errorStrings.ToolValidationGatewayInventoryError.title, errorStrings.ToolValidationGatewayInventoryError.message.format(Net.getErrorMessage(error))); return of(null); }), map(instance => ({ show: installationType.some(t => instance.installationType === t) }))); } inventoryValidate(connection, condition) { const nodeName = connection.activeAlias ? connection.activeAlias : connection.name; return this.caches.serverCache.createObservable({ params: { name: nodeName } }) .pipe(catchError((error) => { const errorStrings = MsftSme.getStrings().MsftSmeShell.Core.Error; const notification = this.appContext.notification.create(connection.name); notification.showError(errorStrings.ToolValidationInventoryError.title, errorStrings.ToolValidationInventoryError.message.format(Net.getErrorMessage(error))); return of(null); }), filter((instance) => instance.serverName === nodeName), map(instance => ({ show: this.checkServerInventoryCondition(condition, instance) }))); } propertyValidate(connection, condition) { const propertyNames = Object.keys(condition); let result = true; for (const propertyName of propertyNames) { const propertyValue = connection.properties && connection.properties[propertyName]; const conditionItem = condition[propertyName]; if (conditionItem && !this.checkCondition(propertyValue, conditionItem)) { result = false; break; } } return of({ show: result }); } toolInventoryValidate(connection, id, scriptOrCommand) { const command = PowerShell.getPowerShellCommand(scriptOrCommand); return this.toolInventoryCache.query({ ...{ name: connection.activeAlias ? connection.activeAlias : connection.name, id: id }, ...command }) .pipe(map(inventory => ({ show: inventory.instance.state === EnvironmentModuleToolState.Available || inventory.instance.state === EnvironmentModuleToolState.NotConfigured, detail: inventory.instance.state, message: inventory.instance.message })), catchError((error) => { const message = Net.getErrorMessage(error); Logging.logError('ToolConditionValidator', `${id}: ${message}`); return of({ show: false, detail: EnvironmentModuleToolState.NotSupported, message }); })); } checkServerInventoryCondition(condition, instance) { for (const property of ToolConditionValidator.serverInventoryProperties) { const conditionItem = condition[property.conditionName]; if (conditionItem && !this.checkCondition(instance[property.dataName], conditionItem)) { return false; } } return true; } checkCondition(data, condition) { switch (condition.type) { case 'number': const numberValue = this.getNumberOrZero(data); const numberTest = this.getNumberOrZero(condition.value); switch (condition.operator) { case ToolConditionValidator.operators.gt: return numberValue > numberTest; case ToolConditionValidator.operators.ge: return numberValue >= numberTest; case ToolConditionValidator.operators.lt: return numberValue < numberTest; case ToolConditionValidator.operators.le: return numberValue <= numberTest; case ToolConditionValidator.operators.eq: return numberValue === numberTest; case ToolConditionValidator.operators.ne: return numberValue !== numberTest; case ToolConditionValidator.operators.is: return !!numberValue; case ToolConditionValidator.operators.not: return !numberValue; default: throw new Error(this.errorStrings.ToolValidationUnsupportedOperator.message.format(JSON.stringify(condition), JSON.stringify(data))); } case 'string': const stringValue = '' + data; const stringTest = '' + condition.value; switch (condition.operator) { case ToolConditionValidator.operators.gt: return stringValue.toLowerCase() > stringTest.toLowerCase(); case ToolConditionValidator.operators.ge: return stringValue.toLowerCase() >= stringTest.toLowerCase(); case ToolConditionValidator.operators.lt: return stringValue.toLowerCase() < stringTest.toLowerCase(); case ToolConditionValidator.operators.le: return stringValue.toLowerCase() <= stringTest.toLowerCase(); case ToolConditionValidator.operators.eq: return stringValue.toLowerCase() === stringTest.toLowerCase(); case ToolConditionValidator.operators.ne: return stringValue.toLowerCase() !== stringTest.toLowerCase(); case ToolConditionValidator.operators.is: return !!data; case ToolConditionValidator.operators.not: return !data; case ToolConditionValidator.operators.contains: return stringValue.toLowerCase().indexOf(stringTest.toLowerCase()) >= 0; case ToolConditionValidator.operators.notContains: return stringValue.toLowerCase().indexOf(stringTest.toLowerCase()) < 0; default: throw new Error(this.errorStrings.ToolValidationUnsupportedOperator.message.format(JSON.stringify(condition), JSON.stringify(data))); } case 'boolean': switch (condition.operator) { case ToolConditionValidator.operators.is: return !!data; case ToolConditionValidator.operators.not: return !data; default: throw new Error(this.errorStrings.ToolValidationUnsupportedOperator.message.format(JSON.stringify(condition), JSON.stringify(data))); } case 'version': const versionValue = data; const versionTest = condition.value; return this.compareVersion(versionValue, versionTest, condition.operator); case 'numberArray': const checkNumber = condition.value; if (!checkNumber || typeof checkNumber === 'string' || !checkNumber.length || typeof checkNumber[0] !== 'number') { throw new Error(this.errorStrings.ToolValidationUnsupportedDataType.message.format(JSON.stringify(condition), JSON.stringify(data))); } const numberArrayValue = this.getNumberOrZero(data); const numberArray = condition.value; for (const numberItem of numberArray) { const numberArrayTest = this.getNumberOrZero(numberItem); switch (condition.operator) { case ToolConditionValidator.operators.anyEq: if (numberArrayValue === numberArrayTest) { return true; } break; case ToolConditionValidator.operators.anyNe: if (numberArrayValue !== numberArrayTest) { return true; } break; default: throw new Error(this.errorStrings.ToolValidationUnsupportedOperator.message.format(JSON.stringify(condition), JSON.stringify(data))); } } return false; case 'stringArray': const checkString = condition.value; if (!checkString || typeof checkString === 'string' || !checkString.length || typeof checkString[0] !== 'string') { throw new Error(this.errorStrings.ToolValidationUnsupportedDataType.message.format(JSON.stringify(condition), JSON.stringify(data))); } const stringArrayValue = '' + data; const stringArray = condition.value; for (const stringArrayTest of stringArray) { switch (condition.operator) { case ToolConditionValidator.operators.anyEq: if (stringArrayValue.toLowerCase() === stringArrayTest.toLowerCase()) { return true; } break; case ToolConditionValidator.operators.anyNe: if (stringArrayValue.toLowerCase() !== stringArrayTest.toLowerCase()) { return true; } break; case ToolConditionValidator.operators.anyContains: if (stringArrayValue.toLowerCase().indexOf(stringArrayTest.toLowerCase()) >= 0) { return true; } break; case ToolConditionValidator.operators.anyNotContains: if (stringArrayValue.toLowerCase().indexOf(stringArrayTest.toLowerCase()) < 0) { return true; } break; default: throw new Error(this.errorStrings.ToolValidationUnsupportedOperator.message.format(JSON.stringify(condition), JSON.stringify(data))); } } return false; default: throw new Error(this.errorStrings.ToolValidationUnsupportedDataType.message.format(JSON.stringify(condition), JSON.stringify(data))); } } compareVersion(left, right, operator) { const leftSegments = left.split('.'); const rightSegments = right.split('.'); if (!leftSegments || leftSegments.length <= 0 || !rightSegments || rightSegments.length <= 0) { throw new Error(this.errorStrings.ToolValidationVersionFormat.message); } const count = Math.max(leftSegments.length, rightSegments.length); let status; for (let index = 0; index < count; index++) { if (rightSegments[index] === '*') { // quit comparison with wildcard. status = 0; break; } const leftSegment = this.getNumberOrZero(leftSegments[index]); const rightSegment = this.getNumberOrZero(rightSegments[index]); if (leftSegment === rightSegment) { // equal. status = 0; } else if (leftSegment > rightSegment) { // greater. status = 1; break; } else { // lesser. status = -1; break; } } switch (operator) { case ToolConditionValidator.operators.gt: return status > 0; case ToolConditionValidator.operators.ge: return status >= 0; case ToolConditionValidator.operators.lt: return status < 0; case ToolConditionValidator.operators.le: return status <= 0; case ToolConditionValidator.operators.eq: return status === 0; case ToolConditionValidator.operators.ne: return status !== 0; default: throw new Error(this.errorStrings.ToolValidationUnsupportedOperator.message.format(operator, left)); } } getNumberOrZero(data) { const result = Number(data); return isNaN(result) ? 0 : result; } } //# sourceMappingURL=tool-condition-validator.js.map