import {ParsedArgs, ZabbixErrorResult, ZabbixRequest, ZabbixResult} from "./zabbix-request.js"; import {ZabbixAPI} from "./zabbix-api.js"; import {InputMaybe, Permission, QueryHasPermissionsArgs, UserPermission} from "../schema/generated/graphql.js"; import {ApiErrorCode, PermissionNumber} from "../model/model_enum_values.js"; import {Config} from "../common_utils.js"; export class ZabbixRequestWithPermissions extends ZabbixRequest { constructor(public path: string, public authToken?: string | null, public cookie?: string | null, protected permissionsNeeded?: QueryHasPermissionsArgs) { super(path, authToken, cookie); } async prepare(zabbixAPI: ZabbixAPI, _args?: A): Promise { // If prepare returns something else than undefined, the execution will be skipped and the // result returned this.prepResult = await this.assureUserPermissions(zabbixAPI); return this.prepResult; } async assureUserPermissions(zabbixAPI: ZabbixAPI) { if (this.permissionsNeeded && !await ZabbixPermissionsHelper.hasUserPermissions(zabbixAPI, this.permissionsNeeded, this.authToken, this.cookie)) { return { error: { message: "User does not have the required permissions", code: ApiErrorCode.PERMISSION_ERROR, path: this.path, args: this.permissionsNeeded } } } else { return undefined } } } class ZabbixQueryTemplateGroupPermissionsRequest extends ZabbixRequest< { groupid: string, name: string }[]> { constructor(authToken?: string | null, cookie?: string | null) { super("templategroup.get.permissions", authToken, cookie); } createZabbixParams(args?: ParsedArgs) { return { ...super.createZabbixParams(args), output: [ "groupid", "name" ], searchWildcardsEnabled: true, search: { name: [ ZabbixPermissionsHelper.ZABBIX_PERMISSION_TEMPLATE_GROUP_NAME_PREFIX + "/*" ] } }; } } interface ZabbixUserGroupResponse { usrgrpid: string, name: string, gui_access: string, users_status: string, templategroup_rights: { id: string, permission: Permission }[] } class ZabbixQueryUserGroupPermissionsRequest extends ZabbixRequest { constructor(authToken?: string | null, cookie?: string | null) { super("usergroup.get.permissions", authToken, cookie); } createZabbixParams(args?: ParsedArgs) { return { ...super.createZabbixParams(args), "output": [ "usrgrpid", "name", "gui_access", "users_status" ], ...args?.zabbix_params, "selectTemplateGroupRights": [ "id", "permission" ] }; } } export class ZabbixPermissionsHelper { private static permissionObjectNameCache: Map = new Map() public static ZABBIX_PERMISSION_TEMPLATE_GROUP_NAME_PREFIX = Config.ZABBIX_PERMISSION_TEMPLATE_GROUP_NAME_PREFIX public static async getUserPermissions(zabbixAPI: ZabbixAPI, zabbixAuthToken?: string, cookie?: string, objectNames?: InputMaybe | undefined): Promise { return Array.from((await this.getUserPermissionNumbers(zabbixAPI, zabbixAuthToken, cookie, objectNames)).entries()).map(value => { return { objectName: value[0], permission: this.mapPermissionToZabbixEnum(value[1]) } }); } public static async getUserPermissionNumbers(zabbixAPI: ZabbixAPI, zabbixAuthToken?: string | null, cookie?: string | null, objectNamesFilter?: InputMaybe | undefined): Promise> { const userGroupPermissions = await new ZabbixQueryUserGroupPermissionsRequest(zabbixAuthToken, cookie).executeRequestThrowError(zabbixAPI) // Prepare the list of templateIds that are not loaded yet const templateIdsToLoad = new Set(userGroupPermissions.flatMap(usergroup => usergroup.templategroup_rights.map(templateGroupRight => templateGroupRight.id))); // Remove all templateIds that are already in the permissionObjectNameCache templateIdsToLoad.forEach(id => { if (this.permissionObjectNameCache.has(id)) { templateIdsToLoad.delete(id); } }) if (templateIdsToLoad.size > 0) { // Load all templateIds that are not in the permissionObjectNameCache const missingPermissionGroupNames = await new ZabbixQueryTemplateGroupPermissionsRequest(zabbixAuthToken, cookie) .executeRequestThrowError(zabbixAPI, new ParsedArgs({groupids: Array.from(templateIdsToLoad)})); missingPermissionGroupNames.forEach(group => { this.permissionObjectNameCache.set(group.groupid, group.name.replace(ZabbixPermissionsHelper.ZABBIX_PERMISSION_TEMPLATE_GROUP_NAME_PREFIX + "/", "")) }) } // Merge the permissions from the user groups. The merge function will first merge the permissions from the template groups let permissions = new Map(); userGroupPermissions.forEach(usergroup => { permissions = this.mergeTemplateGroupPermissions(usergroup, permissions, objectNamesFilter); }) return permissions; } private static mergeTemplateGroupPermissions(usergroup: ZabbixUserGroupResponse, currentTemplateGroupPermissions: Map, objectNames: InputMaybe | undefined): Map { // First, we have to find the minimum permission for each template group as this is always superseeding the higher permission if it is set within a user group let minPermissionsInUserGroup: Map = new Map(); let objectNamesFilter = this.createMatcherFromWildcardArray(objectNames); usergroup.templategroup_rights.forEach(templateGroupPermission => { const objectName = this.permissionObjectNameCache.get(templateGroupPermission.id); if (objectName && (objectNamesFilter == undefined || objectNamesFilter.test(objectName))) { const permissionValue = Number(templateGroupPermission.permission) as PermissionNumber; const minPermissionWithinThisGroup = minPermissionsInUserGroup.get(objectName); if (minPermissionWithinThisGroup == undefined || minPermissionWithinThisGroup > permissionValue) { minPermissionsInUserGroup.set(objectName, permissionValue); } } }) // Then we have to find the highest permission compared to the permissions resulting from other user groups, as on a // user group level the higher permission is always superseeding the lower permission minPermissionsInUserGroup.forEach((minPermissionInUserGroup, objectName) => { const maxPermissionBetweenGroups = currentTemplateGroupPermissions.get(objectName); if (maxPermissionBetweenGroups == undefined || maxPermissionBetweenGroups < minPermissionInUserGroup) { currentTemplateGroupPermissions.set(objectName, minPermissionInUserGroup); } }) return currentTemplateGroupPermissions; } private static mapZabbixPermission(zabbixPermission: Permission): PermissionNumber { switch (zabbixPermission) { case Permission.Read: return PermissionNumber.Read case Permission.ReadWrite: return PermissionNumber.ReadWrite case Permission.Deny: default: return PermissionNumber.Deny } } private static mapPermissionToZabbixEnum(permission: PermissionNumber): Permission { switch (permission) { case PermissionNumber.Read: return Permission.Read case PermissionNumber.ReadWrite: return Permission.ReadWrite case PermissionNumber.Deny: default: return Permission.Deny } } private static createMatcherFromWildcardArray(array: InputMaybe | undefined): RegExp | undefined { if (!array) { return undefined; } // Escape all values in the array and create regexp that allows the * wildcard which will be a .* in the regexp return new RegExp(array.map(value => "^" + this.escapeRegExp(value).replace(/\\\*/gi, ".*") + "$").join("|")); } private static escapeRegExp(value: string) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string } public static async hasUserPermissions(zabbixAPI: ZabbixAPI, args: QueryHasPermissionsArgs, zabbixAuthToken?: string | null, cookie?: string | null): Promise { let permissions = await this.getUserPermissionNumbers(zabbixAPI, zabbixAuthToken, cookie); for (const permission of args.permissions) { const existingPermission = permissions.get(permission.objectName); if (permission.permission != Permission.Deny) { if (existingPermission == undefined || existingPermission < this.mapZabbixPermission(permission.permission)) { return false; } } } return true; } }