refactor!: Cleanup zabbix api access and remove unused classes

This commit is contained in:
Andreas Hilbig 2026-01-07 18:11:47 +01:00
parent a89c3eeea7
commit da86c726db
9 changed files with 246 additions and 624 deletions

View file

@ -1,24 +1,26 @@
import { import {
DeviceCommunicationType, DeviceCommunicationType,
DeviceStatus, DeviceStatus,
Host,
MutationCreateHostArgs, MutationCreateHostArgs,
MutationImportHostsArgs,
MutationImportHostGroupsArgs, MutationImportHostGroupsArgs,
MutationImportHostsArgs,
MutationImportUserRightsArgs, MutationImportUserRightsArgs,
Permission, Permission,
QueryAllHostsArgs,
QueryAllHostGroupsArgs, QueryAllHostGroupsArgs,
QueryAllHostsArgs,
QueryExportHostValueHistoryArgs,
QueryExportUserRightsArgs, QueryExportUserRightsArgs,
QueryHasPermissionsArgs, QueryHasPermissionsArgs,
QueryUserPermissionsArgs, QueryUserPermissionsArgs,
Resolvers, Resolvers,
StorageItemType, Host, QueryExportHostValueHistoryArgs, Device, StorageItemType,
} from "../schema/generated/graphql.js"; } from "../schema/generated/graphql.js";
import {HostImporter} from "../execution/host_importer"; import {HostImporter} from "../execution/host_importer";
import {HostValueExporter} from "../execution/host_exporter"; import {HostValueExporter} from "../execution/host_exporter";
import {logger} from "../logging/logger.js"; 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 {ZabbixCreateHostRequest, ZabbixQueryHostsRequestWithItemsAndInventory,} from "../datasources/zabbix-hosts.js";
import {ZabbixQueryHostgroupsParams, ZabbixQueryHostgroupsRequest} from "../datasources/zabbix-hostgroups.js"; import {ZabbixQueryHostgroupsParams, ZabbixQueryHostgroupsRequest} from "../datasources/zabbix-hostgroups.js";
import { import {
@ -35,6 +37,7 @@ import {
import {ZABBIX_EDGE_DEVICE_BASE_GROUP, zabbixAPI} from "../datasources/zabbix-api"; import {ZABBIX_EDGE_DEVICE_BASE_GROUP, zabbixAPI} from "../datasources/zabbix-api";
import {GraphQLInterfaceType, GraphQLList} from "graphql/type"; import {GraphQLInterfaceType, GraphQLList} from "graphql/type";
import {isDevice} from "./resolver_helpers"; import {isDevice} from "./resolver_helpers";
import {ZabbixPermissionsHelper} from "../datasources/zabbix-permissions";
export function createResolvers(): Resolvers { export function createResolvers(): Resolvers {
@ -66,7 +69,7 @@ export function createResolvers(): Resolvers {
zabbixAPI, new ParsedArgs(args)) zabbixAPI, new ParsedArgs(args))
}, },
logout: async (_parent, _args, {zabbixAuthToken, cookie}: any) => { logout: async (_parent, _args, {zabbixAuthToken, cookie}: any) => {
return await new ZabbixRequest<any>("user.logout", undefined, cookie).executeRequestThrowError(zabbixAPI); return await new ZabbixRequest<any>("user.logout", zabbixAuthToken, cookie).executeRequestThrowError(zabbixAPI);
}, },
allHosts: async (_parent: any, args: QueryAllHostsArgs, { allHosts: async (_parent: any, args: QueryAllHostsArgs, {

View file

@ -25,7 +25,7 @@ const createZabbixHierarchicalDeviceTagsResolver =
} }
export async function schema_loader(): Promise<GraphQLSchema> { export async function schema_loader(): Promise<GraphQLSchema> {
const resolvers = createResolvers(); 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) { if (process.env.ADDITIONAL_SCHEMAS) {
for (const schema of process.env.ADDITIONAL_SCHEMAS.split(",")){ for (const schema of process.env.ADDITIONAL_SCHEMAS.split(",")){
typeDefs += readFileSync(schema, {encoding: 'utf-8'}); typeDefs += readFileSync(schema, {encoding: 'utf-8'});

View file

@ -1,5 +1,4 @@
import { import {
CacheOptions,
DataSourceConfig, DataSourceConfig,
DataSourceFetchResult, DataSourceFetchResult,
DataSourceRequest, DataSourceRequest,
@ -26,65 +25,57 @@ export class ZabbixAPI
override async fetch<Object>(path: string, incomingRequest: DataSourceRequest = {}): Promise<DataSourceFetchResult<Object>> { override async fetch<Object>(path: string, incomingRequest: DataSourceRequest = {}): Promise<DataSourceFetchResult<Object>> {
logger.debug(`Zabbix request path=${path}, body=${JSON.stringify(incomingRequest.body).substring(0, ZabbixAPI.MAX_LOG_REQUEST_BODY_LIMIT_LENGTH)} (...)`) 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<DataSourceFetchResult<Object>> = super.fetch("api_jsonrpc.php", incomingRequest);
try { try {
const response_promise: Promise<DataSourceFetchResult<Object>> = super.fetch("api_jsonrpc.php", incomingRequest); const response = await response_promise;
try { const body = response.parsedBody;
const response = await response_promise; return await new Promise!<DataSourceFetchResult<Object>>((resolve) => {
const body = response.parsedBody; if (body && body.hasOwnProperty("result")) {
return await new Promise!<DataSourceFetchResult<Object>>((resolve, reject) => { // @ts-ignore
if (body && body.hasOwnProperty("result")) { let result: any = body["result"];
// @ts-ignore response.parsedBody = result;
let result: any = body["result"]; if (result) {
response.parsedBody = result; logger.debug(`Found and returned result - length = ${result.length}`);
if (result) { if (!Array.isArray(result) || !result.length) {
logger.debug(`Found and returned result - length = ${result.length}`); logger.debug(`Result: ${JSON.stringify(result)}`);
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"];
} else { } 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);
} }
}); resolve(response);
} catch (reason) { } else {
let msg = `Unable to retrieve response for request body=${JSON.stringify(incomingRequest.body)}: ${JSON.stringify(reason)}`; let error_result: any;
logger.error(msg); if (body && body.hasOwnProperty("error")) {
return response_promise // @ts-ignore
} error_result = body["error"];
} catch (e) { } else {
let msg = `Unable to retrieve response for request body=${JSON.stringify(incomingRequest.body)}: ${JSON.stringify(e)}` error_result = body;
logger.error(msg) }
// @ts-ignore logger.error(`No result for Zabbix request body=${JSON.stringify(incomingRequest.body)}: ${JSON.stringify(error_result)}`);
return response_promise_original 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<TResult = any>(path: string, request?: PostRequest<CacheOptions>): Promise<TResult> { public post<TResult = any>(path: string, request?: PostRequest): Promise<TResult> {
return super.post(path, request); return super.post(path, request);
} }

View file

@ -1,11 +1,5 @@
import {ZabbixAPI} from "./zabbix-api.js"; import {SortOrder, StorageItemType} from "../schema/generated/graphql.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 {ParsedArgs, ZabbixParams, ZabbixRequest, ZabbixResult} from "./zabbix-request.js"; import {ParsedArgs, ZabbixParams, ZabbixRequest, ZabbixResult} from "./zabbix-request.js";
import {sleep} from "../common_utils";
export interface ZabbixValue { export interface ZabbixValue {
key?: string, key?: string,
@ -56,73 +50,3 @@ export class ZabbixQueryHistoryRequest extends ZabbixRequest<ZabbixExportValue[]
} }
} }
} }
export interface ZabbixHistoryPushResult {
response: string,
data: { itemid: string, error?: string[] | ApiError }[],
error?: ApiError | string[]
}
export class ZabbixHistoryPushRequest extends ZabbixRequest<ZabbixHistoryPushResult> {
constructor(authToken?: string | null, cookie?: string) {
super("history.push", authToken, cookie);
}
}
export class ZabbixStoreObjectInItemHistoryRequest extends ZabbixRequest<ZabbixHistoryPushResult> {
// 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<any> {
// 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"))
}
}
}

View file

@ -7,6 +7,7 @@ import {
zabbixSuperAuthToken zabbixSuperAuthToken
} from "./zabbix-api"; } from "./zabbix-api";
import {logger} from "../logging/logger"; import {logger} from "../logging/logger";
import {ZabbixRequestWithPermissions} from "./zabbix-permissions";
export interface CreateHostGroupResult { export interface CreateHostGroupResult {
groupids: string[] groupids: string[]
@ -20,34 +21,12 @@ const hostGroupReadWritePermissions = {
}] }]
} }
const hostGroupReadPermissions = { export class ZabbixCreateHostGroupRequest extends ZabbixRequestWithPermissions<CreateHostGroupResult> {
permissions: [
{
objectName: "Hostgroup/ConstructionSite",
permission: Permission.Read
}]
}
export class ZabbixCreateHostGroupRequest extends ZabbixRequest<CreateHostGroupResult> {
constructor(_authToken?: string | null, cookie?: string) { constructor(_authToken?: string | null, cookie?: string) {
super("hostgroup.create", zabbixSuperAuthToken, cookie, hostGroupReadWritePermissions); 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 { export class ZabbixQueryHostgroupsParams extends ParsedArgs {
search_name: string | undefined search_name: string | undefined
@ -66,7 +45,7 @@ export type ZabbixQueryHostgroupsResult = {
uuid: string uuid: string
} }
export class ZabbixQueryHostgroupsRequest extends ZabbixRequest<ZabbixQueryHostgroupsResult[], export class ZabbixQueryHostgroupsRequest extends ZabbixRequestWithPermissions<ZabbixQueryHostgroupsResult[],
ZabbixQueryHostgroupsParams> { ZabbixQueryHostgroupsParams> {
constructor(authToken?: string | null, cookie?: string | null, hostGroupReadPermissions?: any) { constructor(authToken?: string | null, cookie?: string | null, hostGroupReadPermissions?: any) {
super("hostgroup.get", authToken, cookie, hostGroupReadPermissions,); super("hostgroup.get", authToken, cookie, hostGroupReadPermissions,);

View file

@ -62,26 +62,6 @@ export class ZabbixQueryHostsMetaRequest extends ZabbixQueryHostsGenericRequest<
} }
} }
export class ZabbixQueryHostsWithDeviceTypeMetaRequest extends ZabbixQueryHostsGenericRequest<Host[]> {
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<T extends ZabbixResult> extends ZabbixQueryHostsGenericRequest<T> { export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult> extends ZabbixQueryHostsGenericRequest<T> {
constructor(path: string, authToken?: string | null, cookie?: string) { constructor(path: string, authToken?: string | null, cookie?: string) {
@ -164,30 +144,6 @@ export class ZabbixQueryHostsRequestWithItemsAndInventory extends ZabbixQueryHos
} }
} }
export class ZabbixQueryHostWithInventoryRequest extends ZabbixRequest<any> {
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; const isZabbixCreateHostInputParams = (value: ZabbixParams): value is ZabbixCreateHostInputParams => "host" in value && !!value.host;
export interface ZabbixCreateHostInputParams extends ZabbixParams { export interface ZabbixCreateHostInputParams extends ZabbixParams {
@ -256,45 +212,3 @@ export class ZabbixCreateHostRequest extends ZabbixRequest<CreateHostResponse> {
return args?.zabbix_params || {}; return args?.zabbix_params || {};
} }
} }
export class ZabbixQueryHostRequest extends ZabbixQueryHostsGenericRequest<any> {
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;
}
}

View file

@ -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"; import {ZabbixItem} from "../schema/generated/graphql";
export class ZabbixQueryItemsMetaRequest extends ZabbixRequest<any> {
createZabbixParams(args?: ParsedArgs) {
return {
"templated": false,
output: [
"itemid",
"key_",
"hostid"
], ...args?.zabbix_params
};
}
}
export class ZabbixQueryItemsRequest extends ZabbixRequest<ZabbixItem[]> { export class ZabbixQueryItemsRequest extends ZabbixRequest<ZabbixItem[]> {
constructor(authToken?: string | null, cookie?: string) { constructor(authToken?: string | null, cookie?: string) {
@ -47,128 +34,3 @@ export class ZabbixQueryItemsRequest extends ZabbixRequest<ZabbixItem[]> {
} }
} }
export class ZabbixQueryItemsByIdRequest extends ZabbixRequest<ZabbixItem[]> {
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<any> {
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<ZabbixDeleteItemResponse> {
constructor(authToken?: string | null, cookie?: string) {
super("item.delete", authToken, cookie);
}
}
export interface ZabbixCreateOrUpdateItemResponse extends ZabbixResult {
"itemids": string[]
}
export class ZabbixCreateOrUpdateItemRequest extends ZabbixRequest<ZabbixCreateOrUpdateItemResponse> {
constructor(path: string, authToken?: string | null, cookie?: string) {
super(path, authToken, cookie);
}
}

View file

@ -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<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs> extends ZabbixRequest<T, A> {
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<T | ZabbixErrorResult | undefined> {
// 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< class ZabbixQueryTemplateGroupPermissionsRequest extends ZabbixRequest<
{ {
groupid: string, groupid: string,
name: string name: string
}[]> { }[]> {
constructor(authToken?: string | null, cookie?: string) { constructor(authToken?: string | null, cookie?: string | null) {
super("templategroup.get.permissions", authToken, cookie); super("templategroup.get.permissions", authToken, cookie);
} }
createZabbixParams(args?: ParsedArgs) { createZabbixParams(args?: ParsedArgs) {
return { 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<ZabbixUserGroupResponse[]> {
constructor(authToken?: string | null, cookie?: string | null) {
super("usergroup.get.permissions", authToken, cookie);
}
createZabbixParams(args?: ParsedArgs) {
return {
...super.createZabbixParams(args),
"output": [ "output": [
"usrgrpid", "usrgrpid",
"name", "name",
@ -26,41 +91,127 @@ class ZabbixQueryTemplateGroupPermissionsRequest extends ZabbixRequest<
} }
} }
export class ZabbixPermissionsHelper {
private static permissionObjectNameCache: Map<string, string | null> = new Map()
public static ZABBIX_PERMISSION_TEMPLATE_GROUP_NAME_PREFIX = process.env.ZABBIX_PERMISSION_TEMPLATE_GROUP_NAME_PREFIX || "Permissions"
interface ZabbixUserGroupResponse { public static async getUserPermissions(zabbixAPI: ZabbixAPI, zabbixAuthToken?: string, cookie?: string,
usrgrpid: string, objectNames?: InputMaybe<string[]> | undefined): Promise<UserPermission[]> {
name: string, return Array.from((await this.getUserPermissionNumbers(zabbixAPI, zabbixAuthToken, cookie, objectNames)).entries()).map(value => {
gui_access: string, return {
users_status: string, objectName: value[0],
templategroup_rights: permission: this.mapPermissionToZabbixEnum(value[1])
{ }
id: string, });
permission: string
}[]
}
class ZabbixQueryUserGroupPermissionsRequest extends ZabbixRequest<ZabbixUserGroupResponse[]> {
constructor(authToken?: string | null, cookie?: string) {
super("usergroup.get.permissions", authToken, cookie);
} }
createZabbixParams(args?: ParsedArgs) { public static async getUserPermissionNumbers(zabbixAPI: ZabbixAPI, zabbixAuthToken?: string | null, cookie?: string | null, objectNamesFilter?: InputMaybe<string[]> | undefined): Promise<Map<string, PermissionNumber>> {
return { const userGroupPermissions = await new ZabbixQueryUserGroupPermissionsRequest(zabbixAuthToken, cookie).executeRequestThrowError(zabbixAPI)
...super.createZabbixParams(args),
"params": { // Prepare the list of templateIds that are not loaded yet
"output": [ const templateIdsToLoad = new Set(userGroupPermissions.flatMap(usergroup => usergroup.templategroup_rights.map(templateGroupRight => templateGroupRight.id)));
"groupid",
"name" // Remove all templateIds that are already in the permissionObjectNameCache
], templateIdsToLoad.forEach(id => {
"searchWildcardsEnabled": true, if (this.permissionObjectNameCache.has(id)) {
"search": { templateIdsToLoad.delete(id);
"name": [ }
"Permissions/*" })
]
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<string, PermissionNumber>();
userGroupPermissions.forEach(usergroup => {
permissions = this.mergeTemplateGroupPermissions(usergroup, permissions, objectNamesFilter);
})
return permissions;
}
private static mergeTemplateGroupPermissions(usergroup: ZabbixUserGroupResponse,
currentTemplateGroupPermissions: Map<string, PermissionNumber>,
objectNames: InputMaybe<string[]> | undefined): Map<string, PermissionNumber> {
// 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<string, PermissionNumber> = 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<string[]> | 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<boolean> {
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;
}
} }

View file

@ -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 {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 {logger} from "../logging/logger.js";
import {GraphQLError} from "graphql"; import {GraphQLError} from "graphql";
@ -31,10 +31,6 @@ export interface ZabbixWithTagsParams extends ZabbixParams {
tags?: { tag: string; operator: number; value: any; }[] tags?: { tag: string; operator: number; value: any; }[]
} }
export enum ZabbixValueType {
TEXT = 4,
}
export class ParsedArgs { export class ParsedArgs {
public name_pattern?: string public name_pattern?: string
public distinct_by_name?: boolean; public distinct_by_name?: boolean;
@ -142,8 +138,7 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
protected method: string protected method: string
protected prepResult: T | ZabbixErrorResult | undefined = undefined protected prepResult: T | ZabbixErrorResult | undefined = undefined
constructor(public path: string, public authToken?: string | null, public cookie?: string | null, constructor(public path: string, public authToken?: string | null, public cookie?: string | null) {
protected permissionsNeeded?: QueryHasPermissionsArgs) {
this.method = path.split(".", 2).join("."); this.method = path.split(".", 2).join(".");
this.requestBodyTemplate = new ZabbixRequestBody(this.method); this.requestBodyTemplate = new ZabbixRequestBody(this.method);
} }
@ -188,26 +183,11 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
return headers return headers
} }
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
}
}
async prepare(zabbixAPI: ZabbixAPI, args?: A): Promise<T | ZabbixErrorResult | undefined> {
// If prepare returns something else than undefined the execution will be skipped and the async prepare(zabbixAPI: ZabbixAPI, _args?: A): Promise<T | ZabbixErrorResult | undefined> {
// If prepare returns something else than undefined, the execution will be skipped and the
// result returned // result returned
this.prepResult = await this.assureUserPermissions(zabbixAPI);
return this.prepResult; 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<ZabbixUserGroupResponse[]> {
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<string, string | null> = 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<string[]> | undefined): Promise<UserPermission[]> {
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<string[]> | undefined): Promise<Map<string, PermissionNumber>> {
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<string, PermissionNumber>();
userGroupPermissions.forEach(usergroup => {
permissions = this.mergeTemplateGroupPermissions(usergroup, permissions, objectNamesFilter);
})
return permissions;
}
private static mergeTemplateGroupPermissions(usergroup: ZabbixUserGroupResponse,
currentTemplateGroupPermissions: Map<string, PermissionNumber>,
objectNames: InputMaybe<string[]> | undefined): Map<string, PermissionNumber> {
// 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<string, PermissionNumber> = 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<string[]> | 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<boolean> {
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;
}
}