@microsoft/windows-admin-center-sdk
Version:
Microsoft - Windows Admin Center Shell
619 lines (617 loc) • 27.1 kB
JavaScript
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