zabbix-graphql-api/src/datasources/zabbix-usergroups.ts
Andreas Hilbig fb5e9cbe81 feat: improve Zabbix multi-version compatibility and introduce local development environment
This update enhances compatibility across multiple Zabbix versions and introduces tools for easier local development and testing.

Key improvements and verified version support:
- Verified Zabbix version support: 6.2, 6.4, 7.0, and 7.4.
- Version-specific feature handling:
  - `history.push` is enabled only for Zabbix 7.0+; older versions skip it with a clear error or notice.
  - Conditional JSON-RPC authentication: the `auth` field is automatically added to the request body for versions older than 6.4.
  - Implemented static Zabbix version caching in the datasource to minimize redundant API calls.
- Query optimization refinements:
  - Added mapping for implied fields (e.g., `state` -> `items`, `deviceType` -> `tags`).
  - Automatically prune unnecessary Zabbix parameters (like `selectItems` or `selectTags`) when not requested.
- Local development environment:
  - Added a new `zabbix-local` Docker Compose profile that includes PostgreSQL, Zabbix Server, and Zabbix Web.
  - Supports testing different versions by passing the `ZABBIX_VERSION` environment variable (e.g., 6.2, 6.4, 7.0, 7.4).
  - Provided a sample environment file at `samples/zabbix-local.env`.
- Documentation and Roadmap:
  - Updated README with a comprehensive version compatibility matrix and local environment instructions.
  - Created a new guide: `docs/howtos/local_development.md`.
  - Updated maintenance guides and added "Local Development Environment" as an achieved milestone in the roadmap.
- Test suite enhancements:
  - Improved Smoketest and RegressionTest executors with more reliable resource cleanup and error reporting.
  - Made tests version-aware to prevent failures on older Zabbix instances.

BREAKING CHANGE: Dropped Zabbix 6.0 specific workarounds; the minimum supported version is now 6.2.
2026-02-04 14:01:53 +01:00

351 lines
13 KiB
TypeScript

import {
isZabbixErrorResult,
ParsedArgs,
ZabbixCreateOrUpdateParams,
ZabbixCreateOrUpdateRequest,
ZabbixErrorResult,
ZabbixParams,
ZabbixRequest,
ZabbixResult
} from "./zabbix-request.js";
import {
ApiError,
ImportUserRightResult,
UserGroup,
UserGroupInput,
ZabbixGroupRight,
ZabbixGroupRightInput
} from "../schema/generated/graphql.js";
import {ZabbixAPI} from "./zabbix-api.js";
import {logger} from "../logging/logger.js";
import {ZabbixQueryTemplateGroupRequest, ZabbixQueryTemplateGroupResponse} from "./zabbix-templates.js";
import {GroupHelper, ZabbixQueryHostgroupsRequest, ZabbixQueryHostgroupsResult} from "./zabbix-hostgroups.js";
import {ApiErrorCode} from "../model/model_enum_values.js";
abstract class ZabbixPrepareGetTemplatesAndHostgroupsRequest<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs> extends ZabbixRequest<T, A> {
protected templategroups: ZabbixQueryTemplateGroupResponse[];
protected hostgroups: ZabbixQueryHostgroupsResult[];
constructor(path: string, authToken?: string | null, cookie?: string) {
super(path, authToken, cookie);
}
async prepare(zabbixAPI: ZabbixAPI, args?: A): Promise<ZabbixErrorResult | T | undefined> {
let prepResult = await super.prepare(zabbixAPI, args);
if (prepResult) {
return prepResult;
}
let templategroups = await new ZabbixQueryTemplateGroupRequest(this.authToken, this.cookie)
.executeRequestReturnError(zabbixAPI);
if (isZabbixErrorResult(templategroups)) {
this.prepResult = templategroups;
return templategroups;
}
this.templategroups = templategroups;
let hostgroups =
await new ZabbixQueryHostgroupsRequest(this.authToken, this.cookie)
.executeRequestReturnError(zabbixAPI);
if (isZabbixErrorResult(hostgroups)) {
this.prepResult = hostgroups;
return hostgroups;
}
this.hostgroups = hostgroups;
return undefined
}
}
export class ZabbixExportUserGroupArgs extends ParsedArgs {
public exclude_hostgroups_pattern?: RegExp | undefined = undefined;
constructor(name_pattern?: string | null, exclude_hostgroups_pattern_str?: string | null) {
super(name_pattern? {name_pattern: name_pattern} : undefined);
if (exclude_hostgroups_pattern_str) {
this.exclude_hostgroups_pattern = new RegExp(exclude_hostgroups_pattern_str);
}
}
}
export class ZabbixExportUserGroupsRequest extends ZabbixPrepareGetTemplatesAndHostgroupsRequest<
UserGroup[], ZabbixExportUserGroupArgs> {
constructor(authToken?: string | null, cookie?: string) {
super("usergroup.get.withuuids", authToken, cookie);
}
createZabbixParams(args?: ZabbixExportUserGroupArgs): ZabbixParams {
return {
...super.createZabbixParams(args),
output: "extend",
selectTemplateGroupRights: "extend",
selectHostGroupRights: "extend"
};
}
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: ZabbixExportUserGroupArgs): Promise<ZabbixErrorResult | UserGroup[]> {
let result = await super.executeRequestReturnError(zabbixAPI, args);
if (!isZabbixErrorResult(result)) {
for (let userGroup of result) {
for (let template_permission of userGroup.templategroup_rights || []) {
for (let templategroup of this.templategroups) {
if (templategroup.groupid == template_permission.id.toString()) {
template_permission.uuid = templategroup.uuid;
template_permission.name = templategroup.name;
break;
}
}
}
let filtered_hostgroup_permission: ZabbixGroupRight [] = [];
for (let hostgroup_permission of userGroup.hostgroup_rights || []) {
for (let hostgroup of this.hostgroups) {
if (hostgroup.groupid == hostgroup_permission.id.toString()) {
hostgroup_permission.uuid = hostgroup.uuid;
hostgroup_permission.name = hostgroup.name;
break;
}
}
if (!args?.exclude_hostgroups_pattern || !hostgroup_permission.name || !args.exclude_hostgroups_pattern.test(hostgroup_permission.name)) {
filtered_hostgroup_permission.push(hostgroup_permission);
}
}
userGroup.hostgroup_rights = filtered_hostgroup_permission;
}
}
return result;
}
}
export class ZabbixQueryUserGroupsRequest extends ZabbixRequest<UserGroup[]> {
constructor(authToken?: string | null, cookie?: string | null) {
super("usergroup.get", authToken, cookie);
}
createZabbixParams(args?: ParsedArgs): ZabbixParams {
return {
...super.createZabbixParams(args),
output: "extend",
};
}
}
export class ZabbixImportUserGroupsParams extends ParsedArgs {
constructor(public usergroups: UserGroupInput[], public dryRun = true) {
super();
}
}
export class ZabbixImportUserGroupsRequest
extends ZabbixPrepareGetTemplatesAndHostgroupsRequest<ImportUserRightResult[],
ZabbixImportUserGroupsParams> {
constructor(zabbixAuthToken: any, cookie: any) {
super("usergroup.create.import", zabbixAuthToken, cookie);
}
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: ZabbixImportUserGroupsParams): Promise<ZabbixErrorResult | ImportUserRightResult[]> {
let prepareResult = await this.prepare(zabbixAPI, args);
if (prepareResult) {
return prepareResult;
}
let results: ImportUserRightResult[] = [];
let hostGroupsToPropagete: number[] = []
let createGroupRequest = new ZabbixCreateOrUpdateRequest<
ZabbixCreateUserGroupResponse, ZabbixQueryUserGroupsRequest, ZabbixCreateOrUpdateParams>(
"usergroup", "usrgrpid", ZabbixQueryUserGroupsRequest, this.authToken, this.cookie);
for (let userGroup of args?.usergroups || []) {
let templategroup_rights = this.calc_templategroup_rights(userGroup);
let hostgroup_rights = this.calc_hostgroup_rights(userGroup);
let errors: ApiError[] = [];
let paramsObj: any = {
name: userGroup.name,
gui_access: userGroup.gui_access,
users_status: userGroup.users_status,
hostgroup_rights: hostgroup_rights.hostgroup_rights,
templategroup_rights: templategroup_rights.templategroup_rights,
};
let params = new ZabbixCreateOrUpdateParams(paramsObj, args?.dryRun)
let result = await createGroupRequest.executeRequestReturnError(zabbixAPI, params);
if (isZabbixErrorResult(result)) {
errors.push(result.error);
results.push(
{
name: userGroup.name,
errors: errors,
message: result.error.message || "Error creating user group",
}
)
} else {
hostGroupsToPropagete.push(
...hostgroup_rights.hostgroup_rights.map(
value => value.id));
results.push(
{
name: userGroup.name,
id: result.usrgrpids[0],
message: createGroupRequest.message,
errors: errors,
}
)
}
errors.push(...templategroup_rights.errors);
errors.push(...hostgroup_rights.errors);
}
// If user groups were imported: Propagate group permissions to group children
if (hostGroupsToPropagete.length > 0) {
// Propagate group permissions to group children, filter duplicate groupids first
await new ZabbixPropagateHostGroupsRequest(this.authToken, this.cookie)
.executeRequestThrowError(zabbixAPI,
new ZabbixPropagateHostGroupsParams(hostGroupsToPropagete))
}
return results;
}
calc_hostgroup_rights(usergroup: UserGroupInput): {
errors: ApiError[],
hostgroup_rights: ZabbixGroupRight[]
} {
let result: ZabbixGroupRight [] = [];
let errors: ApiError[] = [];
for (let hostgroup_right of usergroup.hostgroup_rights || []) {
let success = false;
let matchedName = "";
let matchedId: number | undefined = undefined;
// Try matching by UUID first
for (let hostgroup of this.hostgroups) {
if (hostgroup.uuid && hostgroup_right.uuid && hostgroup.uuid === hostgroup_right.uuid) {
matchedId = Number(hostgroup.groupid);
matchedName = hostgroup.name;
success = true;
break;
}
}
if (success) {
result.push(
{
id: matchedId!,
permission: hostgroup_right.permission,
}
)
if (hostgroup_right.name && hostgroup_right.name != matchedName) {
errors.push(
{
code: ApiErrorCode.OK,
message: `WARNING: Hostgroup found and permissions set, but target name=${matchedName} does not match provided name=${hostgroup_right.name}`,
data: hostgroup_right,
}
)
}
} else {
errors.push(
{
code: ApiErrorCode.ZABBIX_HOSTGROUP_NOT_FOUND,
message: `Hostgroup with UUID ${hostgroup_right.uuid} not found`,
data: hostgroup_right,
}
)
}
}
return {
hostgroup_rights: result,
errors: errors,
};
}
calc_templategroup_rights(usergroup: UserGroupInput): {
errors: ApiError[],
templategroup_rights: ZabbixGroupRightInput[]
} {
let result: ZabbixGroupRight [] = [];
let errors: ApiError[] = [];
for (let templategroup_right of usergroup.templategroup_rights || []) {
let success = false;
let matchedName = "";
let matchedId: number | undefined = undefined;
// Try matching by UUID first
for (let templategroup of this.templategroups) {
if (templategroup.uuid && templategroup_right.uuid && templategroup.uuid === templategroup_right.uuid) {
matchedId = Number(templategroup.groupid);
matchedName = templategroup.name;
success = true;
break;
}
}
if (success) {
result.push(
{
id: matchedId!,
permission: templategroup_right.permission,
}
)
if (templategroup_right.name && templategroup_right.name != matchedName) {
errors.push(
{
code: ApiErrorCode.OK,
message: `WARNING: Templategroup found and permissions set, but target name=${matchedName} does not match provided name=${templategroup_right.name}`,
data: templategroup_right,
}
)
}
} else {
errors.push(
{
code: ApiErrorCode.ZABBIX_TEMPLATEGROUP_NOT_FOUND,
message: `Templategroup with UUID ${templategroup_right.uuid} not found`,
data: templategroup_right,
}
)
}
}
return {
templategroup_rights: result,
errors: errors,
};
}
}
export type ZabbixCreateUserGroupResponse = {
usrgrpids: string[];
}
class ZabbixPropagateHostGroupsParams extends ParsedArgs {
constructor(public groups: number[]) {
super();
}
}
export class ZabbixPropagateHostGroupsRequest extends ZabbixRequest<ZabbixCreateUserGroupResponse,
ZabbixPropagateHostGroupsParams> {
constructor(authToken?: string | null, cookie?: string | null) {
super("hostgroup.propagate", authToken, cookie);
}
async prepare(zabbixAPI: ZabbixAPI, args?: ZabbixPropagateHostGroupsParams): Promise<ZabbixCreateUserGroupResponse | ZabbixErrorResult | undefined> {
return super.prepare(zabbixAPI, args);
}
createZabbixParams(args?: ZabbixPropagateHostGroupsParams): ZabbixParams {
return {
groups: [...new Set(args?.groups || [])].map(value => {
return {
groupid: value
}
}) || [],
permissions: true
}
}
}