From da86c726dbde4c5b586bd697e8393d2b9224e464 Mon Sep 17 00:00:00 2001 From: Andreas Hilbig Date: Wed, 7 Jan 2026 18:11:47 +0100 Subject: [PATCH] refactor!: Cleanup zabbix api access and remove unused classes --- src/api/resolvers.ts | 13 +- src/api/schema.ts | 2 +- src/datasources/zabbix-api.ts | 93 +++++------ src/datasources/zabbix-history.ts | 78 +-------- src/datasources/zabbix-hostgroups.ts | 27 +--- src/datasources/zabbix-hosts.ts | 86 ---------- src/datasources/zabbix-items.ts | 140 +---------------- src/datasources/zabbix-permissions.ts | 217 ++++++++++++++++++++++---- src/datasources/zabbix-request.ts | 214 +------------------------ 9 files changed, 246 insertions(+), 624 deletions(-) diff --git a/src/api/resolvers.ts b/src/api/resolvers.ts index fb3c79a..0333017 100644 --- a/src/api/resolvers.ts +++ b/src/api/resolvers.ts @@ -1,24 +1,26 @@ import { DeviceCommunicationType, DeviceStatus, + Host, MutationCreateHostArgs, - MutationImportHostsArgs, MutationImportHostGroupsArgs, + MutationImportHostsArgs, MutationImportUserRightsArgs, Permission, - QueryAllHostsArgs, QueryAllHostGroupsArgs, + QueryAllHostsArgs, + QueryExportHostValueHistoryArgs, QueryExportUserRightsArgs, QueryHasPermissionsArgs, QueryUserPermissionsArgs, Resolvers, - StorageItemType, Host, QueryExportHostValueHistoryArgs, Device, + StorageItemType, } from "../schema/generated/graphql.js"; import {HostImporter} from "../execution/host_importer"; import {HostValueExporter} from "../execution/host_exporter"; import {logger} from "../logging/logger.js"; -import {ParsedArgs, ZabbixPermissionsHelper, ZabbixRequest} from "../datasources/zabbix-request.js"; +import {ParsedArgs, ZabbixRequest} from "../datasources/zabbix-request.js"; import {ZabbixCreateHostRequest, ZabbixQueryHostsRequestWithItemsAndInventory,} from "../datasources/zabbix-hosts.js"; import {ZabbixQueryHostgroupsParams, ZabbixQueryHostgroupsRequest} from "../datasources/zabbix-hostgroups.js"; import { @@ -35,6 +37,7 @@ import { import {ZABBIX_EDGE_DEVICE_BASE_GROUP, zabbixAPI} from "../datasources/zabbix-api"; import {GraphQLInterfaceType, GraphQLList} from "graphql/type"; import {isDevice} from "./resolver_helpers"; +import {ZabbixPermissionsHelper} from "../datasources/zabbix-permissions"; export function createResolvers(): Resolvers { @@ -66,7 +69,7 @@ export function createResolvers(): Resolvers { zabbixAPI, new ParsedArgs(args)) }, logout: async (_parent, _args, {zabbixAuthToken, cookie}: any) => { - return await new ZabbixRequest("user.logout", undefined, cookie).executeRequestThrowError(zabbixAPI); + return await new ZabbixRequest("user.logout", zabbixAuthToken, cookie).executeRequestThrowError(zabbixAPI); }, allHosts: async (_parent: any, args: QueryAllHostsArgs, { diff --git a/src/api/schema.ts b/src/api/schema.ts index 6c0fc91..5beef23 100644 --- a/src/api/schema.ts +++ b/src/api/schema.ts @@ -25,7 +25,7 @@ const createZabbixHierarchicalDeviceTagsResolver = } export async function schema_loader(): Promise { const resolvers = createResolvers(); - let typeDefs: string = readFileSync('./schema.graphql', {encoding: 'utf-8'}); + let typeDefs: string = readFileSync('./src/schema/*.graphql', {encoding: 'utf-8'}); if (process.env.ADDITIONAL_SCHEMAS) { for (const schema of process.env.ADDITIONAL_SCHEMAS.split(",")){ typeDefs += readFileSync(schema, {encoding: 'utf-8'}); diff --git a/src/datasources/zabbix-api.ts b/src/datasources/zabbix-api.ts index 7cbc5ee..ce7c119 100644 --- a/src/datasources/zabbix-api.ts +++ b/src/datasources/zabbix-api.ts @@ -1,5 +1,4 @@ import { - CacheOptions, DataSourceConfig, DataSourceFetchResult, DataSourceRequest, @@ -26,65 +25,57 @@ export class ZabbixAPI override async fetch(path: string, incomingRequest: DataSourceRequest = {}): Promise> { logger.debug(`Zabbix request path=${path}, body=${JSON.stringify(incomingRequest.body).substring(0, ZabbixAPI.MAX_LOG_REQUEST_BODY_LIMIT_LENGTH)} (...)`) - let response_promise_original + let response_promise: Promise> = super.fetch("api_jsonrpc.php", incomingRequest); try { - const response_promise: Promise> = super.fetch("api_jsonrpc.php", incomingRequest); - try { - const response = await response_promise; - const body = response.parsedBody; - return await new Promise!>((resolve, reject) => { - if (body && body.hasOwnProperty("result")) { - // @ts-ignore - let result: any = body["result"]; - response.parsedBody = result; - if (result) { - logger.debug(`Found and returned result - length = ${result.length}`); - if (!Array.isArray(result) || !result.length) { - logger.debug(`Result: ${JSON.stringify(result)}`); - } else { - result.forEach((entry: any) => { - if (entry.hasOwnProperty("tags")) { - entry["tags"].forEach((tag: { tag: string; value: string; }) => { - entry[tag.tag] = tag.value; - }); - } - if (entry.hasOwnProperty("inheritedTags")) { - entry["inheritedTags"].forEach((tag_1: { tag: string; value: string; }) => { - entry[tag_1.tag] = tag_1.value; - }); - } - }); - } - } - resolve(response); - } else { - let error_result: any; - if (body && body.hasOwnProperty("error")) { - // @ts-ignore - error_result = body["error"]; + const response = await response_promise; + const body = response.parsedBody; + return await new Promise!>((resolve) => { + if (body && body.hasOwnProperty("result")) { + // @ts-ignore + let result: any = body["result"]; + response.parsedBody = result; + if (result) { + logger.debug(`Found and returned result - length = ${result.length}`); + if (!Array.isArray(result) || !result.length) { + logger.debug(`Result: ${JSON.stringify(result)}`); } else { - error_result = body; + result.forEach((entry: any) => { + if (entry.hasOwnProperty("tags")) { + entry["tags"].forEach((tag: { tag: string; value: string; }) => { + entry[tag.tag] = tag.value; + }); + } + if (entry.hasOwnProperty("inheritedTags")) { + entry["inheritedTags"].forEach((tag_1: { tag: string; value: string; }) => { + entry[tag_1.tag] = tag_1.value; + }); + } + }); } - logger.error(`No result for Zabbix request body=${JSON.stringify(incomingRequest.body)}: ${JSON.stringify(error_result)}`); - resolve(response); } - }); - } catch (reason) { - let msg = `Unable to retrieve response for request body=${JSON.stringify(incomingRequest.body)}: ${JSON.stringify(reason)}`; - logger.error(msg); - return response_promise - } - } catch (e) { - let msg = `Unable to retrieve response for request body=${JSON.stringify(incomingRequest.body)}: ${JSON.stringify(e)}` - logger.error(msg) - // @ts-ignore - return response_promise_original + resolve(response); + } else { + let error_result: any; + if (body && body.hasOwnProperty("error")) { + // @ts-ignore + error_result = body["error"]; + } else { + error_result = body; + } + logger.error(`No result for Zabbix request body=${JSON.stringify(incomingRequest.body)}: ${JSON.stringify(error_result)}`); + resolve(response); + } + }); + } catch (reason) { + let msg = `Unable to retrieve response for request body=${JSON.stringify(incomingRequest.body)}: ${JSON.stringify(reason)}`; + logger.error(msg); + return response_promise } } - public post(path: string, request?: PostRequest): Promise { + public post(path: string, request?: PostRequest): Promise { return super.post(path, request); } diff --git a/src/datasources/zabbix-history.ts b/src/datasources/zabbix-history.ts index 8f6347e..aca7785 100644 --- a/src/datasources/zabbix-history.ts +++ b/src/datasources/zabbix-history.ts @@ -1,11 +1,5 @@ -import {ZabbixAPI} from "./zabbix-api.js"; -import {ApiError, SortOrder, StorageItemType} from "../schema/generated/graphql.js"; -import {ZabbixCreateOrUpdateStorageItemRequest} from "./zabbix-items.js"; -import {ZabbixForceCacheReloadRequest} from "./zabbix-script.js"; -import {logger} from "../logging/logger.js"; -import {ApiErrorCode} from "../model/model_enum_values.js"; +import {SortOrder, StorageItemType} from "../schema/generated/graphql.js"; import {ParsedArgs, ZabbixParams, ZabbixRequest, ZabbixResult} from "./zabbix-request.js"; -import {sleep} from "../common_utils"; export interface ZabbixValue { key?: string, @@ -56,73 +50,3 @@ export class ZabbixQueryHistoryRequest extends ZabbixRequest { - constructor(authToken?: string | null, cookie?: string) { - super("history.push", authToken, cookie); - } -} - -export class ZabbixStoreObjectInItemHistoryRequest extends ZabbixRequest { - // After creating an item or host zabbix needs some time before the created object can be referenced in other - // operations - the reason is the config-cache. In case of having ZBX_CACHEUPDATEFREQUENCY=1 (seconds) set within the - // Zabbix - config the delay of 1 second will be sufficient - private static readonly ZABBIX_DELAY_UNTIL_CONFIG_CHANGED: number = 0 - public itemid: number | undefined - - constructor(authToken?: string | null, cookie?: string) { - super("history.push.jsonobject", authToken, cookie); - } - - async prepare(zabbixAPI: ZabbixAPI, args?: ParsedArgs): Promise { - // Create or update zabbix Item - let success = false; - this.itemid = Number(args?.getParam("itemid")) - let timeoutForValueUpdate = this.itemid ? 0 : ZabbixStoreObjectInItemHistoryRequest.ZABBIX_DELAY_UNTIL_CONFIG_CHANGED; - - // Create or update controlprogram - item - let result: { - "itemids": string[] - } | undefined = await new ZabbixCreateOrUpdateStorageItemRequest( - this.itemid ? "item.update.storeiteminhistory" : "item.create.storeiteminhistory", - this.authToken, this.cookie).executeRequestThrowError(zabbixAPI, args) - - // logger.debug(`Create/update item itemid=${this.itemid}, hostid=${this.zabbixHostId} lead to result=`, JSON.stringify(result)); - - if (result && result.hasOwnProperty("itemids") && result.itemids.length > 0) { - this.itemid = Number(result.itemids[0]); - let scriptExecResult = - await new ZabbixForceCacheReloadRequest(this.authToken, this.cookie).executeRequestThrowError(zabbixAPI) - if (scriptExecResult.response != "success") { - logger.error(`cache reload not successful: ${scriptExecResult.value}`) - } - await sleep(timeoutForValueUpdate).promise - } - - if (!this.itemid) { - this.prepResult = { - error: { - message: "Unable to create/update item", - code: ApiErrorCode.ZABBIX_NO_ITEM_PUSH_ITEM, - path: this.path, - args: args, - } - } - } - } - - createZabbixParams(args?: ParsedArgs): ZabbixParams { - return { - itemid: this.itemid, - value: JSON.stringify(args?.getParam("value")) - } - } - -} diff --git a/src/datasources/zabbix-hostgroups.ts b/src/datasources/zabbix-hostgroups.ts index bc15626..4f18203 100644 --- a/src/datasources/zabbix-hostgroups.ts +++ b/src/datasources/zabbix-hostgroups.ts @@ -7,6 +7,7 @@ import { zabbixSuperAuthToken } from "./zabbix-api"; import {logger} from "../logging/logger"; +import {ZabbixRequestWithPermissions} from "./zabbix-permissions"; export interface CreateHostGroupResult { groupids: string[] @@ -20,34 +21,12 @@ const hostGroupReadWritePermissions = { }] } -const hostGroupReadPermissions = { - permissions: [ - { - objectName: "Hostgroup/ConstructionSite", - permission: Permission.Read - }] -} - -export class ZabbixCreateHostGroupRequest extends ZabbixRequest { +export class ZabbixCreateHostGroupRequest extends ZabbixRequestWithPermissions { constructor(_authToken?: string | null, cookie?: string) { super("hostgroup.create", zabbixSuperAuthToken, cookie, hostGroupReadWritePermissions); } } -export class ZabbixDeleteHostGroupRequest extends ZabbixRequest<{ - "groupids": string [] -}> { - constructor(_authToken?: string | null, cookie?: string) { - super("hostgroup.delete", zabbixSuperAuthToken, cookie, { - permissions: [ - { - objectName: "Hostgroup/ConstructionSite", - permission: Permission.ReadWrite - }] - }); - } -} - export class ZabbixQueryHostgroupsParams extends ParsedArgs { search_name: string | undefined @@ -66,7 +45,7 @@ export type ZabbixQueryHostgroupsResult = { uuid: string } -export class ZabbixQueryHostgroupsRequest extends ZabbixRequest { constructor(authToken?: string | null, cookie?: string | null, hostGroupReadPermissions?: any) { super("hostgroup.get", authToken, cookie, hostGroupReadPermissions,); diff --git a/src/datasources/zabbix-hosts.ts b/src/datasources/zabbix-hosts.ts index b1c60af..d981556 100644 --- a/src/datasources/zabbix-hosts.ts +++ b/src/datasources/zabbix-hosts.ts @@ -62,26 +62,6 @@ export class ZabbixQueryHostsMetaRequest extends ZabbixQueryHostsGenericRequest< } } -export class ZabbixQueryHostsWithDeviceTypeMetaRequest extends ZabbixQueryHostsGenericRequest { - public static PATH = "host.get.meta_with_device_type" - - constructor(authToken?: string | null, cookie?: string | null) { - super(ZabbixQueryHostsWithDeviceTypeMetaRequest.PATH, authToken, cookie); - } - - createZabbixParams(args?: ParsedArgs): ZabbixParams { - return { - ...super.createZabbixParams(args), - tags: [ - { - "tag": "deviceType", - "operator": 4 - } - ], - inheritedTags: true - }; - } -} export class ZabbixQueryHostsGenericRequestWithItems extends ZabbixQueryHostsGenericRequest { constructor(path: string, authToken?: string | null, cookie?: string) { @@ -164,30 +144,6 @@ export class ZabbixQueryHostsRequestWithItemsAndInventory extends ZabbixQueryHos } } -export class ZabbixQueryHostWithInventoryRequest extends ZabbixRequest { - constructor(authToken?: string | null, cookie?: string) { - super("host.get.with_inventory", authToken, cookie); - } - - createZabbixParams(args?: ParsedArgs): ZabbixParams { - return { - ...super.createZabbixParams(args), - selectInventory: [ - "location", "location_lat", "location_lon" - ], - output: [ - "hostid", - "host", - "name", - "hostgroup", - "description", - "parentTemplates" - ], - }; - } -} - - const isZabbixCreateHostInputParams = (value: ZabbixParams): value is ZabbixCreateHostInputParams => "host" in value && !!value.host; export interface ZabbixCreateHostInputParams extends ZabbixParams { @@ -256,45 +212,3 @@ export class ZabbixCreateHostRequest extends ZabbixRequest { return args?.zabbix_params || {}; } } - -export class ZabbixQueryHostRequest extends ZabbixQueryHostsGenericRequest { - constructor(authToken?: string | null, cookie?: string | null) { - super("host.get", authToken, cookie); - } -} - - -export class ZabbixCreateOrFindHostRequest extends ZabbixCreateHostRequest { - - constructor(protected groupid: number, protected templateid: number, authToken?: string | null, cookie?: string,) { - super(authToken, cookie); - } - - createZabbixParams(args?: ParsedArgs): ZabbixParams { - return super.createZabbixParams(args); - } - - async prepare(zabbixAPI: ZabbixAPI, args?: ParsedArgs) { - // Lookup host of appropriate type (by template) and groupName - // or create one if not found. If multiple hosts are found the first - // will be taken - - let queryHostArgs = new ParsedArgs({ - groupids: this.groupid, - templateids: this.templateid, - }); - let hosts: { - hostid: number - }[] = await new ZabbixQueryHostRequest(this.authToken, this.cookie) - .executeRequestThrowError(zabbixAPI, queryHostArgs) - // logger.debug("Query hosts args=", JSON.stringify(queryHostArgs), "lead to result=", JSON.stringify(hosts)); - if (hosts && hosts.length > 0) { - // If we found a host and return it as prep result the execution of the create host request will be skipped - this.prepResult = { - hostids: [hosts[0].hostid] - } - } - return this.prepResult; - - } -} diff --git a/src/datasources/zabbix-items.ts b/src/datasources/zabbix-items.ts index 3a148f4..981f74e 100644 --- a/src/datasources/zabbix-items.ts +++ b/src/datasources/zabbix-items.ts @@ -1,19 +1,6 @@ -import {ParsedArgs, ZabbixParams, ZabbixRequest, ZabbixResult, ZabbixValueType} from "./zabbix-request.js"; +import {ParsedArgs, ZabbixRequest} from "./zabbix-request.js"; import {ZabbixItem} from "../schema/generated/graphql"; -export class ZabbixQueryItemsMetaRequest extends ZabbixRequest { - createZabbixParams(args?: ParsedArgs) { - return { - "templated": false, - output: [ - "itemid", - "key_", - "hostid" - ], ...args?.zabbix_params - }; - } -} - export class ZabbixQueryItemsRequest extends ZabbixRequest { constructor(authToken?: string | null, cookie?: string) { @@ -47,128 +34,3 @@ export class ZabbixQueryItemsRequest extends ZabbixRequest { } } - -export class ZabbixQueryItemsByIdRequest extends ZabbixRequest { - constructor(authToken?: string | null, cookie?: string) { - super("item.get.itembyid", authToken, cookie); - } - createZabbixParams(args?: ParsedArgs): ZabbixParams { - let filter: { key_: string | null } | null = null - if (args?.zabbix_params?.hasOwnProperty("id")) { - // @ts-ignore - args.zabbix_params["filter"] = { - // @ts-ignore - ...args?.zabbix_params.filter, "key_": args?.zabbix_params.id - } - // @ts-ignore - delete args.zabbix_params["id"] - } - return { - filter: filter, - "selectTags": ["tag", "value"], - "inheritedTags": true, - "output": [ - "lastvalue", - "lastclock", - "value_type", - "hostid", - "itemid", - "name", - "status", - "key_" - ], ...args?.zabbix_params, - } - }; -} - - -export interface ZabbixStoreValueInItemParams extends ZabbixParams { - hostid?: number - itemid?: number - key: string - name: string - tags: { - tag: string, - value?: string - }[] - value: Object -} - -const isStoreValueInItem = (value: ZabbixParams): value is ZabbixStoreValueInItemParams => - "hostid" in value && !!value.hostid && "name" in value && "key" in value && "value" in value; - -const isUpdateValueInItemParams = (value: ZabbixParams): value is ZabbixUpdateValueInItemParams => - "itemid" in value && !!value.itemid && isStoreValueInItem(value); - -export interface ZabbixUpdateValueInItemParams extends ZabbixStoreValueInItemParams { - itemid: number -} - -export enum ZabbixItemType { - ZABBIX_TRAPPER = 2, - ZABBIX_SCRIPT = 21 -} - -export class ZabbixCreateOrUpdateStorageItemRequest extends ZabbixRequest { - static MAX_ZABBIX_ITEM_STORAGE_PERIOD = "9125d"; // Maximum possible value is 25 years, which corresponds to 9125 days - - createZabbixParams(args?: ParsedArgs): ZabbixParams { - if (args && isStoreValueInItem(args?.zabbix_params)) { - // Attention!! Zabbix status - // can not be used as expected: - // 1. Status 0 means enabled, all other values mean disabled - // 2. If the status of the item is disabled the value will not be - // evaluated - this means we can't use the item status to reflect - // the activation status of the controlProgram, as we also want - // to read the values of disabled controlPrograms.. - let createOrUpdateItemParams = { - key_: args.zabbix_params.key, - name: args.zabbix_params.name, - tags: args.zabbix_params.tags, - "type": ZabbixItemType.ZABBIX_TRAPPER.valueOf(), - "history": ZabbixCreateOrUpdateStorageItemRequest.MAX_ZABBIX_ITEM_STORAGE_PERIOD, - "value_type": ZabbixValueType.TEXT.valueOf() - } - - if (isUpdateValueInItemParams(args.zabbix_params)) { - return { - itemid: args.zabbix_params.itemid, - ...createOrUpdateItemParams - }; - } - return { - hostid: args.zabbix_params.hostid, - ...createOrUpdateItemParams - } - - } - - return args?.zabbix_params || {}; - - } - -} - -export interface ZabbixDeleteItemResponse extends ZabbixResult { - itemids: { - itemid: string | string[] - } -} - -export class ZabbixDeleteItemRequest extends ZabbixRequest { - constructor(authToken?: string | null, cookie?: string) { - super("item.delete", authToken, cookie); - } -} - -export interface ZabbixCreateOrUpdateItemResponse extends ZabbixResult { - "itemids": string[] -} - -export class ZabbixCreateOrUpdateItemRequest extends ZabbixRequest { - constructor(path: string, authToken?: string | null, cookie?: string) { - super(path, authToken, cookie); - } -} - - diff --git a/src/datasources/zabbix-permissions.ts b/src/datasources/zabbix-permissions.ts index 7799ade..5af0a3d 100644 --- a/src/datasources/zabbix-permissions.ts +++ b/src/datasources/zabbix-permissions.ts @@ -1,17 +1,82 @@ -import {ParsedArgs, ZabbixRequest} from "./zabbix-request.js"; +import {ParsedArgs, ZabbixErrorResult, ZabbixRequest, ZabbixResult} from "./zabbix-request.js"; +import {ZabbixAPI} from "./zabbix-api"; +import {InputMaybe, Permission, QueryHasPermissionsArgs, UserPermission} from "../schema/generated/graphql"; +import {ApiErrorCode, PermissionNumber} from "../model/model_enum_values"; +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) { + 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", @@ -26,41 +91,127 @@ class ZabbixQueryTemplateGroupPermissionsRequest extends ZabbixRequest< } } +export class ZabbixPermissionsHelper { + private static permissionObjectNameCache: Map = new Map() + public static ZABBIX_PERMISSION_TEMPLATE_GROUP_NAME_PREFIX = process.env.ZABBIX_PERMISSION_TEMPLATE_GROUP_NAME_PREFIX || "Permissions" -interface ZabbixUserGroupResponse { - usrgrpid: string, - name: string, - gui_access: string, - users_status: string, - templategroup_rights: - { - id: string, - permission: string - }[] -} - -class ZabbixQueryUserGroupPermissionsRequest extends ZabbixRequest { - constructor(authToken?: string | null, cookie?: string) { - super("usergroup.get.permissions", authToken, cookie); + 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]) + } + }); } - createZabbixParams(args?: ParsedArgs) { - return { - ...super.createZabbixParams(args), - "params": { - "output": [ - "groupid", - "name" - ], - "searchWildcardsEnabled": true, - "search": { - "name": [ - "Permissions/*" - ] + 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); } - }, - "id": 1 - }; + } + }) + + // 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; + } } diff --git a/src/datasources/zabbix-request.ts b/src/datasources/zabbix-request.ts index adc5aad..80f4d75 100644 --- a/src/datasources/zabbix-request.ts +++ b/src/datasources/zabbix-request.ts @@ -1,6 +1,6 @@ -import {ApiError, InputMaybe, QueryHasPermissionsArgs, UserPermission} from "../schema/generated/graphql.js"; +import {ApiError} from "../schema/generated/graphql.js"; import {ZabbixAPI} from "./zabbix-api.js"; -import {ApiErrorCode, Permission, PermissionNumber} from "../model/model_enum_values.js"; +import {ApiErrorCode} from "../model/model_enum_values.js"; import {logger} from "../logging/logger.js"; import {GraphQLError} from "graphql"; @@ -31,10 +31,6 @@ export interface ZabbixWithTagsParams extends ZabbixParams { tags?: { tag: string; operator: number; value: any; }[] } -export enum ZabbixValueType { - TEXT = 4, -} - export class ParsedArgs { public name_pattern?: string public distinct_by_name?: boolean; @@ -142,8 +138,7 @@ export class ZabbixRequest { - // If prepare returns something else than undefined the execution will be skipped and the + + 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; } @@ -342,189 +322,7 @@ export class ZabbixCreateOrUpdateRequest< } } -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 = process.env.ZABBIX_PERMISSION_TEMPLATE_GROUP_NAME_PREFIX || "Permissions" - - 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) { - let result = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string - return result; - } - - 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; - } -}