diff --git a/docker-compose.yml b/docker-compose.yml index d70691c..eca8cc5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -95,6 +95,9 @@ services: # Consumer group options KAFKA_CFG_GROUP_COORDINATOR_REBALANCE_PROTOCOLS: "classic,consumer" KAFKA_CFG_GROUP_INITIAL_REBALANCE_DELAY_MS: "0" + # ACL options + KAFKA_AUTHORIZER_CLASS_NAME: "org.apache.kafka.metadata.authorizer.StandardAuthorizer" + KAFKA_SUPER_USERS: "User:ANONYMOUS" broker-cluster-2: image: *image diff --git a/docs/admin.md b/docs/admin.md index aa6e325..cc58760 100644 --- a/docs/admin.md +++ b/docs/admin.md @@ -98,10 +98,10 @@ The return value is an object specifying quotas for the requested user/client co Options: -| Property | Type | Description | -| ---------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -| components | `DescribeClientQuotasRequestComponent[]` | Array of components specifying the entity types and match criteria for which to describe client quotas. | -| strict | `boolean` | Whether to use strict matching for components. Defaults to `false`. | +| Property | Type | Description | +| ---------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| components | `DescribeClientQuotasRequestComponent[]` | Array of components specifying the entity types and match criteria for which to describe client quotas. | +| strict | `boolean` | Whether to use strict matching for components. Defaults to `false`. | ### `alterClientQuotas(options[, callback])` @@ -111,10 +111,48 @@ The return value is a list of entities for which quotas have been changed. Options: -| Property | Type | Description | -| ------------ | --------------------------------- | ----------------------------------------------------------------------------------------- | -| entries | `AlterClientQuotasRequestEntry[]` | Array of entries specifying the entities and quotas to change. | -| validateOnly | `boolean` | Whether to only validate the request without applying changes. Defaults to `false`. | +| Property | Type | Description | +| ------------ | --------------------------------- | ----------------------------------------------------------------------------------- | +| entries | `AlterClientQuotasRequestEntry[]` | Array of entries specifying the entities and quotas to change. | +| validateOnly | `boolean` | Whether to only validate the request without applying changes. Defaults to `false`. | + +### `createAcls(options[, callback])` + +Creates Access Control List (ACL) entries to define permissions for Kafka resources. + +The return value is `void`. + +Options: + +| Property | Type | Description | +| ---------- | ------- | ------------------------------- | +| creations | `Acl[]` | Array of ACL entries to create. | + +### `describeAcls(options[, callback])` + +Describes existing Access Control List (ACL) entries that match the specified filter criteria. + +The return value is an array of resources with their associated ACL entries. + +Options: + +| Property | Type | Description | +| -------- | ----------- | ----------------------------------------- | +| filter | `AclFilter` | Filter criteria for matching ACL entries. | + +The filter contains the same properties as ACL entries, but `resourceName`, `principal`, and `host` can be `null` to match any value. + +### `deleteAcls(options[, callback])` + +Deletes Access Control List (ACL) entries that match the specified filter criteria. + +The return value is an array of deleted ACL entries. + +Options: + +| Property | Type | Description | +| -------- | ------------- | --------------------------------------------------- | +| filters | `AclFilter[]` | Array of filter criteria for ACL entries to delete. | ### `describeLogDirs(options[, callback])` diff --git a/docs/diagnostic.md b/docs/diagnostic.md index 3f16782..0858838 100644 --- a/docs/diagnostic.md +++ b/docs/diagnostic.md @@ -73,6 +73,7 @@ Each tracing channel publishes events with the following common properties: | `plt:kafka:admin:groups` | `Admin` | Traces a `Admin.listGroups`, `Admin.describeGroups` or `Admin.deleteGroups` request. | | `plt:kafka:admin:clientQuotas` | `Admin` | Traces a `Admin.describeClientQuotas` or `Admin.alterClientQuotas` request. | | `plt:kafka:admin:logDirs` | `Admin` | Traces a `Admin.describeLogDirs` request. | +| `plt:kafka:admin:acls` | `Admin` | Traces a `Admin.createAcls`, `Admin.describeAcls` or `Admin.deleteAcls` request. | | `plt:kafka:producer:initIdempotent` | `Producer` | Traces a `Producer.initIdempotentProducer` request. | | `plt:kafka:producer:sends` | `Producer` | Traces a `Producer.send` request. | | `plt:kafka:consumer:group` | `Consumer` | Traces a `Consumer.findGroupCoordinator`, `Consumer.joinGroup` or `Consumer.leaveGroup` requests. | diff --git a/playground/apis/admin/acl.ts b/playground/apis/admin/acl.ts index fbd1a35..cbf0187 100644 --- a/playground/apis/admin/acl.ts +++ b/playground/apis/admin/acl.ts @@ -1,12 +1,7 @@ import { api as createAclsV3 } from '../../../src/apis/admin/create-acls-v3.ts' import { api as deleteAclsV3 } from '../../../src/apis/admin/delete-acls-v3.ts' import { api as describeAclsV3 } from '../../../src/apis/admin/describe-acls-v3.ts' -import { - AclOperations, - AclPermissionTypes, - ResourcePatternTypes, - ResourceTypes -} from '../../../src/apis/enumerations.ts' +import { AclOperations, AclPermissionTypes, PatternTypes, ResourceTypes } from '../../../src/apis/enumerations.ts' import { Connection } from '../../../src/network/connection.ts' import { performAPICallWithRetry } from '../../utils.ts' @@ -18,7 +13,7 @@ await performAPICallWithRetry('CreateAcls', () => { resourceType: ResourceTypes.TOPIC, resourceName: 'temp', - resourcePatternType: ResourcePatternTypes.LITERAL, + patternType: PatternTypes.LITERAL, principal: 'abc:cde', host: '*', operation: AclOperations.READ, @@ -27,37 +22,35 @@ await performAPICallWithRetry('CreateAcls', () => ])) await performAPICallWithRetry('DescribeAcls', () => - describeAclsV3.async( - connection, - ResourceTypes.TOPIC, - 'temp', - ResourcePatternTypes.LITERAL, - null, - null, - AclOperations.READ, - AclPermissionTypes.DENY - )) + describeAclsV3.async(connection, { + resourceType: ResourceTypes.TOPIC, + resourceName: 'temp', + patternType: PatternTypes.LITERAL, + principal: null, + host: null, + operation: AclOperations.READ, + permissionType: AclPermissionTypes.DENY + })) await performAPICallWithRetry('DescribeAcls', () => - describeAclsV3.async( - connection, - ResourceTypes.TOPIC, - 'temp', - ResourcePatternTypes.LITERAL, - null, - null, - AclOperations.READ, - AclPermissionTypes.ALLOW - )) + describeAclsV3.async(connection, { + resourceType: ResourceTypes.TOPIC, + resourceName: 'temp', + patternType: PatternTypes.LITERAL, + principal: null, + host: null, + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW + })) await performAPICallWithRetry('DeleteAcls', () => deleteAclsV3.async(connection, [ { - resourceTypeFilter: ResourceTypes.TOPIC, - resourceNameFilter: 'temp', - patternTypeFilter: ResourcePatternTypes.LITERAL, - principalFilter: null, - hostFilter: null, + resourceType: ResourceTypes.TOPIC, + resourceName: 'temp', + patternType: PatternTypes.LITERAL, + principal: null, + host: null, operation: AclOperations.READ, permissionType: AclPermissionTypes.DENY } diff --git a/src/apis/admin/create-acls-v3.ts b/src/apis/admin/create-acls-v3.ts index de0a3c4..1410b6b 100644 --- a/src/apis/admin/create-acls-v3.ts +++ b/src/apis/admin/create-acls-v3.ts @@ -3,16 +3,7 @@ import { type NullableString } from '../../protocol/definitions.ts' import { type Reader } from '../../protocol/reader.ts' import { Writer } from '../../protocol/writer.ts' import { createAPI, type ResponseErrorWithLocation } from '../definitions.ts' - -export interface CreateAclsRequestCreation { - resourceType: number - resourceName: string - resourcePatternType: number - principal: string - host: string - operation: number - permissionType: number -} +import { type Acl } from '../types.ts' export type CreateAclsRequest = Parameters @@ -37,12 +28,12 @@ CreateAcls Request (Version: 3) => [creations] TAG_BUFFER operation => INT8 permission_type => INT8 */ -export function createRequest (creations: CreateAclsRequestCreation[]): Writer { +export function createRequest (creations: Acl[]): Writer { return Writer.create() .appendArray(creations, (w, c) => { w.appendInt8(c.resourceType) .appendString(c.resourceName) - .appendInt8(c.resourcePatternType) + .appendInt8(c.patternType) .appendString(c.principal) .appendString(c.host) .appendInt8(c.operation) diff --git a/src/apis/admin/delete-acls-v3.ts b/src/apis/admin/delete-acls-v3.ts index 99f2009..78486c2 100644 --- a/src/apis/admin/delete-acls-v3.ts +++ b/src/apis/admin/delete-acls-v3.ts @@ -3,28 +3,14 @@ import { type NullableString } from '../../protocol/definitions.ts' import { type Reader } from '../../protocol/reader.ts' import { Writer } from '../../protocol/writer.ts' import { createAPI, type ResponseErrorWithLocation } from '../definitions.ts' +import { type AclOperation, type AclPermissionType, type PatternType, type ResourceType } from '../enumerations.ts' +import { type Acl, type AclFilter } from '../types.ts' -export interface DeleteAclsRequestFilter { - resourceTypeFilter: number - resourceNameFilter?: NullableString - patternTypeFilter: number - principalFilter?: NullableString - hostFilter?: NullableString - operation: number - permissionType: number -} export type DeleteAclsRequest = Parameters -export interface DeleteAclsResponseMatchingAcl { +export interface DeleteAclsResponseMatchingAcl extends Acl { errorCode: number errorMessage: NullableString - resourceType: number - resourceName: string - patternType: number - principal: string - host: string - operation: number - permissionType: number } export interface DeleteAclsResponseFilterResults { @@ -48,14 +34,14 @@ export interface DeleteAclsResponse { operation => INT8 permission_type => INT8 */ -export function createRequest (filters: DeleteAclsRequestFilter[]): Writer { +export function createRequest (filters: AclFilter[]): Writer { return Writer.create() .appendArray(filters, (w, f) => { - w.appendInt8(f.resourceTypeFilter) - .appendString(f.resourceNameFilter) - .appendInt8(f.patternTypeFilter) - .appendString(f.principalFilter) - .appendString(f.hostFilter) + w.appendInt8(f.resourceType) + .appendString(f.resourceName) + .appendInt8(f.patternType) + .appendString(f.principal) + .appendString(f.host) .appendInt8(f.operation) .appendInt8(f.permissionType) }) @@ -109,13 +95,13 @@ export function parseResponse ( return { errorCode, errorMessage: r.readNullableString(), - resourceType: r.readInt8(), + resourceType: r.readInt8() as ResourceType, resourceName: r.readString(), - patternType: r.readInt8(), + patternType: r.readInt8() as PatternType, principal: r.readString(), host: r.readString(), - operation: r.readInt8(), - permissionType: r.readInt8() + operation: r.readInt8() as AclOperation, + permissionType: r.readInt8() as AclPermissionType } }) } diff --git a/src/apis/admin/describe-acls-v3.ts b/src/apis/admin/describe-acls-v3.ts index a3d99a2..2aa3801 100644 --- a/src/apis/admin/describe-acls-v3.ts +++ b/src/apis/admin/describe-acls-v3.ts @@ -3,21 +3,13 @@ import { type NullableString } from '../../protocol/definitions.ts' import { type Reader } from '../../protocol/reader.ts' import { Writer } from '../../protocol/writer.ts' import { createAPI } from '../definitions.ts' +import { type AclOperation, type AclPermissionType, type PatternType, type ResourceType } from '../enumerations.ts' +import { type AclPermission, type AclTarget, type AclFilter } from '../types.ts' export type DescribeAclsRequest = Parameters -export interface DescribeAclsResponseAcl { - principal: string - host: string - operation: number - permissionType: number -} - -export interface DescribeAclsResponseResource { - resourceType: number - resourceName: string - patternType: number - acls: DescribeAclsResponseAcl[] +export interface DescribeAclsResponseResource extends AclTarget { + acls: AclPermission[] } export interface DescribeAclsResponse { throttleTimeMs: number @@ -36,23 +28,15 @@ export interface DescribeAclsResponse { operation => INT8 permission_type => INT8 */ -export function createRequest ( - resourceTypeFilter: number, - resourceNameFilter: NullableString, - patternTypeFilter: number, - principalFilter: NullableString, - hostFilter: NullableString, - operation: number, - permissionType: number -): Writer { +export function createRequest (filter: AclFilter): Writer { return Writer.create() - .appendInt8(resourceTypeFilter) - .appendString(resourceNameFilter) - .appendInt8(patternTypeFilter) - .appendString(principalFilter) - .appendString(hostFilter) - .appendInt8(operation) - .appendInt8(permissionType) + .appendInt8(filter.resourceType) + .appendString(filter.resourceName) + .appendInt8(filter.patternType) + .appendString(filter.principal) + .appendString(filter.host) + .appendInt8(filter.operation) + .appendInt8(filter.permissionType) .appendTaggedFields() } @@ -83,15 +67,15 @@ export function parseResponse ( errorMessage: reader.readNullableString(), resources: reader.readArray(r => { return { - resourceType: r.readInt8(), + resourceType: r.readInt8() as ResourceType, resourceName: r.readString(), - patternType: r.readInt8(), + patternType: r.readInt8() as PatternType, acls: r.readArray(r => { return { principal: r.readString(), host: r.readString(), - operation: r.readInt8(), - permissionType: r.readInt8() + operation: r.readInt8() as AclOperation, + permissionType: r.readInt8() as AclPermissionType } }) } diff --git a/src/apis/enumerations.ts b/src/apis/enumerations.ts index fb31983..2a579df 100644 --- a/src/apis/enumerations.ts +++ b/src/apis/enumerations.ts @@ -48,12 +48,13 @@ export const ResourceTypes = { GROUP: 3, CLUSTER: 4, TRANSACTIONAL_ID: 5, - DELEGATION_TOKEN: 6 + DELEGATION_TOKEN: 6, + USER: 7 } as const -export type ResourceType = keyof typeof ResourceTypes +export type ResourceType = (typeof ResourceTypes)[keyof typeof ResourceTypes] -export const ResourcePatternTypes = { UNKNOWN: 0, ANY: 1, MATCH: 2, LITERAL: 3, PREFIXED: 4 } as const -export type ResourcePatternType = keyof typeof ResourcePatternTypes +export const PatternTypes = { UNKNOWN: 0, ANY: 1, MATCH: 2, LITERAL: 3, PREFIXED: 4 } as const +export type PatternType = (typeof PatternTypes)[keyof typeof PatternTypes] export const AclOperations = { UNKNOWN: 0, @@ -68,12 +69,15 @@ export const AclOperations = { CLUSTER_ACTION: 9, DESCRIBE_CONFIGS: 10, ALTER_CONFIGS: 11, - IDEMPOTENT_WRITE: 12 + IDEMPOTENT_WRITE: 12, + CREATE_TOKENS: 13, + DESCRIBE_TOKENS: 14, + TWO_PHASE_COMMIT: 15 } as const -export type AclOperation = keyof typeof AclOperations +export type AclOperation = (typeof AclOperations)[keyof typeof AclOperations] export const AclPermissionTypes = { UNKNOWN: 0, ANY: 1, DENY: 2, ALLOW: 3 } as const -export type AclPermissionType = keyof typeof AclPermissionTypes +export type AclPermissionType = (typeof AclPermissionTypes)[keyof typeof AclPermissionTypes] // ./admin/*-configs.ts export const ConfigSources = { diff --git a/src/apis/index.ts b/src/apis/index.ts index a2cff14..3700034 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -2,6 +2,7 @@ export * from './callbacks.ts' export * from './definitions.ts' export * from './enumerations.ts' +export * from './types.ts' // Low-level APIs export * from './admin/index.ts' diff --git a/src/apis/types.ts b/src/apis/types.ts new file mode 100644 index 0000000..4b2b1fe --- /dev/null +++ b/src/apis/types.ts @@ -0,0 +1,20 @@ +import { type AclOperation, type AclPermissionType, type PatternType, type ResourceType } from './enumerations.ts' + +type AclFilterOptionals = 'resourceName' | 'principal' | 'host' + +export interface AclTarget { + resourceType: ResourceType + resourceName: string + patternType: PatternType +} + +export interface AclPermission { + principal: string + host: string + operation: AclOperation + permissionType: AclPermissionType +} + +export type Acl = AclTarget & AclPermission + +export type AclFilter = { [K in AclFilterOptionals]: Acl[K] | null | undefined } & Omit diff --git a/src/clients/admin/admin.ts b/src/clients/admin/admin.ts index 007cd17..5a15757 100644 --- a/src/clients/admin/admin.ts +++ b/src/clients/admin/admin.ts @@ -1,3 +1,10 @@ +import { type CreateAclsRequest, type CreateAclsResponse } from '../../apis/admin/create-acls-v3.ts' +import { type DeleteAclsRequest, type DeleteAclsResponse } from '../../apis/admin/delete-acls-v3.ts' +import { + type DescribeAclsResponseResource, + type DescribeAclsRequest, + type DescribeAclsResponse +} from '../../apis/admin/describe-acls-v3.ts' import { type AlterClientQuotasRequest, type AlterClientQuotasResponse, @@ -38,6 +45,7 @@ import { FindCoordinatorKeyTypes, type ConsumerGroupState } from '../../apis/enu import { type FindCoordinatorRequest, type FindCoordinatorResponse } from '../../apis/metadata/find-coordinator-v6.ts' import { type MetadataRequest, type MetadataResponse } from '../../apis/metadata/metadata-v12.ts' import { + adminAclsChannel, adminClientQuotasChannel, adminGroupsChannel, adminLogDirsChannel, @@ -70,7 +78,10 @@ import { describeGroupsOptionsValidator, describeLogDirsOptionsValidator, listGroupsOptionsValidator, - listTopicsOptionsValidator + listTopicsOptionsValidator, + deleteAclsOptionsValidator, + describeAclsOptionsValidator, + createAclsOptionsValidator } from './options.ts' import { type AdminOptions, @@ -86,8 +97,12 @@ import { type Group, type GroupBase, type ListGroupsOptions, - type ListTopicsOptions + type ListTopicsOptions, + type DescribeAclsOptions, + type CreateAclsOptions, + type DeleteAclsOptions } from './types.ts' +import { type Acl } from '../../apis/types.ts' export class Admin extends Base { constructor (options: AdminOptions) { @@ -391,6 +406,96 @@ export class Admin extends Base { return callback[kCallbackPromise] } + createAcls (options: CreateAclsOptions, callback: CallbackWithPromise): void + createAcls (options: CreateAclsOptions): Promise + createAcls (options: CreateAclsOptions, callback?: CallbackWithPromise): void | Promise { + if (!callback) { + callback = createPromisifiedCallback() + } + + if (this[kCheckNotClosed](callback)) { + return callback[kCallbackPromise] + } + + const validationError = this[kValidateOptions](options, createAclsOptionsValidator, '/options', false) + if (validationError) { + callback(validationError, undefined) + return callback[kCallbackPromise] + } + + adminAclsChannel.traceCallback( + this.#createAcls, + 1, + createDiagnosticContext({ client: this, operation: 'createAcls', options }), + this, + options, + callback + ) + + return callback[kCallbackPromise] + } + + describeAcls (options: DescribeAclsOptions, callback: CallbackWithPromise): void + describeAcls (options: DescribeAclsOptions): Promise + describeAcls ( + options: DescribeAclsOptions, + callback?: CallbackWithPromise + ): void | Promise { + if (!callback) { + callback = createPromisifiedCallback() + } + + if (this[kCheckNotClosed](callback)) { + return callback[kCallbackPromise] + } + + const validationError = this[kValidateOptions](options, describeAclsOptionsValidator, '/options', false) + if (validationError) { + callback(validationError, undefined as unknown as DescribeAclsResponseResource[]) + return callback[kCallbackPromise] + } + + adminAclsChannel.traceCallback( + this.#describeAcls, + 1, + createDiagnosticContext({ client: this, operation: 'describeAcls', options }), + this, + options, + callback + ) + + return callback[kCallbackPromise] + } + + deleteAcls (options: DeleteAclsOptions, callback: CallbackWithPromise): void + deleteAcls (options: DeleteAclsOptions): Promise + deleteAcls (options: DeleteAclsOptions, callback?: CallbackWithPromise): void | Promise { + if (!callback) { + callback = createPromisifiedCallback() + } + + if (this[kCheckNotClosed](callback)) { + return callback[kCallbackPromise] + } + + const validationError = this[kValidateOptions](options, deleteAclsOptionsValidator, '/options', false) + if (validationError) { + callback(validationError, undefined as unknown as Acl[]) + return callback[kCallbackPromise] + } + + adminAclsChannel.traceCallback( + this.#deleteAcls, + 1, + createDiagnosticContext({ client: this, operation: 'deleteAcls', options }), + this, + options, + callback + ) + + return callback[kCallbackPromise] + } + #listTopics (options: ListTopicsOptions, callback: CallbackWithPromise): void { const includeInternals = options.includeInternals ?? false @@ -981,4 +1086,117 @@ export class Admin extends Base { ) }) } + + #createAcls (options: CreateAclsOptions, callback: CallbackWithPromise): void { + this[kPerformWithRetry]( + 'createAcls', + retryCallback => { + this[kGetBootstrapConnection]((error, connection) => { + if (error) { + retryCallback(error, undefined) + return + } + + this[kGetApi]('CreateAcls', (error, api) => { + if (error) { + retryCallback(error, undefined) + return + } + + api(connection, options.creations, retryCallback as unknown as Callback) + }) + }) + }, + error => { + if (error) { + callback(new MultipleErrors('Creating ACLs failed.', [error]), undefined) + return + } + + callback(null, undefined) + }, + 0 + ) + } + + #describeAcls (options: DescribeAclsOptions, callback: CallbackWithPromise): void { + this[kPerformWithRetry]( + 'describeAcls', + retryCallback => { + this[kGetBootstrapConnection]((error, connection) => { + if (error) { + retryCallback(error, undefined as unknown as DescribeAclsResponse) + return + } + + this[kGetApi]('DescribeAcls', (error, api) => { + if (error) { + retryCallback(error, undefined as unknown as DescribeAclsResponse) + return + } + + api(connection, options.filter, retryCallback as unknown as Callback) + }) + }) + }, + (error, response) => { + if (error) { + callback( + new MultipleErrors('Describing ACLs failed.', [error]), + undefined as unknown as DescribeAclsResponseResource[] + ) + return + } + + callback(null, response.resources) + }, + 0 + ) + } + + #deleteAcls (options: DeleteAclsOptions, callback: CallbackWithPromise): void { + this[kPerformWithRetry]( + 'deleteAcls', + retryCallback => { + this[kGetBootstrapConnection]((error, connection) => { + if (error) { + retryCallback(error, undefined as unknown as DeleteAclsResponse) + return + } + + this[kGetApi]('DeleteAcls', (error, api) => { + if (error) { + retryCallback(error, undefined as unknown as DeleteAclsResponse) + return + } + + api(connection, options.filters, retryCallback as unknown as Callback) + }) + }) + }, + (error, response) => { + if (error) { + callback(new MultipleErrors('Deleting ACLs failed.', [error]), undefined as unknown as Acl[]) + return + } + + callback( + null, + response.filterResults.flatMap(results => + results.matchingAcls.map(acl => { + return { + resourceType: acl.resourceType, + resourceName: acl.resourceName, + patternType: acl.patternType, + principal: acl.principal, + host: acl.host, + operation: acl.operation, + permissionType: acl.permissionType + } + })) + ) + }, + 0 + ) + } } diff --git a/src/clients/admin/options.ts b/src/clients/admin/options.ts index 38758a8..30d29dd 100644 --- a/src/clients/admin/options.ts +++ b/src/clients/admin/options.ts @@ -1,4 +1,11 @@ -import { ClientQuotaMatchTypes, ConsumerGroupStates } from '../../apis/enumerations.ts' +import { + AclOperations, + AclPermissionTypes, + ClientQuotaMatchTypes, + ConsumerGroupStates, + PatternTypes, + ResourceTypes +} from '../../apis/enumerations.ts' import { ajv, listErrorMessage } from '../../utils.ts' import { idProperty } from '../base/options.ts' @@ -199,6 +206,77 @@ export const describeLogDirsOptionsSchema = { additionalProperties: false } +const aclSchema = { + type: 'object', + properties: { + resourceType: { type: 'number', enum: Object.values(ResourceTypes) }, + resourceName: { type: 'string', minLength: 1 }, + patternType: { type: 'number', enum: Object.values(PatternTypes) }, + principal: { type: 'string', minLength: 1 }, + host: { type: 'string', minLength: 1 }, + operation: { type: 'number', enum: Object.values(AclOperations) }, + permissionType: { type: 'number', enum: Object.values(AclPermissionTypes) } + }, + required: ['resourceType', 'resourceName', 'patternType', 'principal', 'host', 'operation', 'permissionType'], + additionalProperties: false +} + +const aclFilterSchema = { + type: 'object', + properties: { + resourceType: { type: 'number', enum: Object.values(ResourceTypes) }, + resourceName: { + anyOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] + }, + patternType: { type: 'number', enum: Object.values(PatternTypes) }, + principal: { + anyOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] + }, + host: { + anyOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] + }, + operation: { type: 'number', enum: Object.values(AclOperations) }, + permissionType: { type: 'number', enum: Object.values(AclPermissionTypes) } + }, + required: ['resourceType', 'patternType', 'operation', 'permissionType'], + additionalProperties: false +} + +export const createAclsOptionsSchema = { + type: 'object', + properties: { + creations: { + type: 'array', + items: aclSchema, + minItems: 1 + } + }, + required: ['creations'], + additionalProperties: false +} + +export const describeAclsOptionsSchema = { + type: 'object', + properties: { + filter: aclFilterSchema + }, + required: ['filter'], + additionalProperties: false +} + +export const deleteAclsOptionsSchema = { + type: 'object', + properties: { + filters: { + type: 'array', + items: aclFilterSchema, + minItems: 1 + } + }, + required: ['filters'], + additionalProperties: false +} + export const createTopicsOptionsValidator = ajv.compile(createTopicOptionsSchema) export const listTopicsOptionsValidator = ajv.compile(listTopicOptionsSchema) export const deleteTopicsOptionsValidator = ajv.compile(deleteTopicOptionsSchema) @@ -208,3 +286,6 @@ export const deleteGroupsOptionsValidator = ajv.compile(deleteGroupsOptionsSchem export const describeClientQuotasOptionsValidator = ajv.compile(describeClientQuotasOptionsSchema) export const alterClientQuotasOptionsValidator = ajv.compile(alterClientQuotasOptionsSchema) export const describeLogDirsOptionsValidator = ajv.compile(describeLogDirsOptionsSchema) +export const createAclsOptionsValidator = ajv.compile(createAclsOptionsSchema) +export const describeAclsOptionsValidator = ajv.compile(describeAclsOptionsSchema) +export const deleteAclsOptionsValidator = ajv.compile(deleteAclsOptionsSchema) diff --git a/src/clients/admin/types.ts b/src/clients/admin/types.ts index 160d81a..2f6f048 100644 --- a/src/clients/admin/types.ts +++ b/src/clients/admin/types.ts @@ -10,6 +10,7 @@ import { type ConsumerGroupState } from '../../apis/enumerations.ts' import { type NullableString } from '../../protocol/definitions.ts' import { type BaseOptions } from '../base/types.ts' import { type ExtendedGroupProtocolSubscription, type GroupAssignment } from '../consumer/types.ts' +import { type Acl, type AclFilter } from '../../apis/types.ts' export interface BrokerAssignment { partition: number @@ -98,3 +99,15 @@ export interface BrokerLogDirDescription { throttleTimeMs: DescribeLogDirsResponse['throttleTimeMs'] results: Omit[] } + +export interface CreateAclsOptions { + creations: Acl[] +} + +export interface DescribeAclsOptions { + filter: AclFilter +} + +export interface DeleteAclsOptions { + filters: AclFilter[] +} diff --git a/src/diagnostic.ts b/src/diagnostic.ts index 11f2b0d..4546a6d 100644 --- a/src/diagnostic.ts +++ b/src/diagnostic.ts @@ -77,6 +77,7 @@ export const adminTopicsChannel = createTracingChannel('a export const adminGroupsChannel = createTracingChannel('admin:groups') export const adminClientQuotasChannel = createTracingChannel('admin:clientQuotas') export const adminLogDirsChannel = createTracingChannel('admin:logDirs') +export const adminAclsChannel = createTracingChannel('admin:acls') // Producer channels export const producerInitIdempotentChannel = createTracingChannel('producer:initIdempotent') diff --git a/test/apis/admin/create-acls-v3.test.ts b/test/apis/admin/create-acls-v3.test.ts index 2c978d6..f148c5f 100644 --- a/test/apis/admin/create-acls-v3.test.ts +++ b/test/apis/admin/create-acls-v3.test.ts @@ -1,65 +1,21 @@ import { deepStrictEqual, ok, throws } from 'node:assert' import test from 'node:test' -import { createAclsV3, Reader, ResponseError, Writer } from '../../../src/index.ts' +import { AclOperations, AclPermissionTypes, createAclsV3, PatternTypes, Reader, ResourceTypes, ResponseError, Writer } from '../../../src/index.ts' const { createRequest, parseResponse } = createAclsV3 -// Constants for ACL resource types, pattern types, operations, and permission types -const RESOURCE_TYPE = { - UNKNOWN: 0, - ANY: 1, - TOPIC: 2, - GROUP: 3, - CLUSTER: 4, - TRANSACTIONAL_ID: 5, - DELEGATION_TOKEN: 6 -} - -const PATTERN_TYPE = { - UNKNOWN: 0, - ANY: 1, - MATCH: 2, - LITERAL: 3, - PREFIXED: 4 -} - -const OPERATION = { - UNKNOWN: 0, - ANY: 1, - ALL: 2, - READ: 3, - WRITE: 4, - CREATE: 5, - DELETE: 6, - ALTER: 7, - DESCRIBE: 8, - CLUSTER_ACTION: 9, - DESCRIBE_CONFIGS: 10, - ALTER_CONFIGS: 11, - IDEMPOTENT_WRITE: 12 -} - -const PERMISSION_TYPE = { - UNKNOWN: 0, - ANY: 1, - DENY: 2, - ALLOW: 3 -} - test('createRequest serializes a single ACL creation correctly', () => { - const creations = [ + const writer = createRequest([ { - resourceType: RESOURCE_TYPE.TOPIC, + resourceType: ResourceTypes.TOPIC, resourceName: 'test-topic', - resourcePatternType: PATTERN_TYPE.LITERAL, + patternType: PatternTypes.LITERAL, principal: 'User:test-user', host: '*', - operation: OPERATION.READ, - permissionType: PERMISSION_TYPE.ALLOW + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW } - ] - - const writer = createRequest(creations) + ]) // Verify it returns a Writer ok(writer instanceof Writer, 'Should return a Writer instance') @@ -93,13 +49,13 @@ test('createRequest serializes a single ACL creation correctly', () => { creationsArray, [ { - resourceType: RESOURCE_TYPE.TOPIC, + resourceType: ResourceTypes.TOPIC, resourceName: 'test-topic', - resourcePatternType: PATTERN_TYPE.LITERAL, + resourcePatternType: PatternTypes.LITERAL, principal: 'User:test-user', host: '*', - operation: OPERATION.READ, - permissionType: PERMISSION_TYPE.ALLOW + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW } ], 'Serialized data should match expected values' @@ -107,37 +63,35 @@ test('createRequest serializes a single ACL creation correctly', () => { }) test('createRequest serializes multiple ACL creations correctly', () => { - const creations = [ + const writer = createRequest([ { - resourceType: RESOURCE_TYPE.TOPIC, + resourceType: ResourceTypes.TOPIC, resourceName: 'test-topic', - resourcePatternType: PATTERN_TYPE.LITERAL, + patternType: PatternTypes.LITERAL, principal: 'User:test-user', host: '*', - operation: OPERATION.READ, - permissionType: PERMISSION_TYPE.ALLOW + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW }, { - resourceType: RESOURCE_TYPE.TOPIC, + resourceType: ResourceTypes.TOPIC, resourceName: 'test-topic', - resourcePatternType: PATTERN_TYPE.LITERAL, + patternType: PatternTypes.LITERAL, principal: 'User:test-user', host: '*', - operation: OPERATION.WRITE, - permissionType: PERMISSION_TYPE.ALLOW + operation: AclOperations.WRITE, + permissionType: AclPermissionTypes.ALLOW }, { - resourceType: RESOURCE_TYPE.GROUP, + resourceType: ResourceTypes.GROUP, resourceName: 'test-group', - resourcePatternType: PATTERN_TYPE.LITERAL, + patternType: PatternTypes.LITERAL, principal: 'User:test-user', host: '*', - operation: OPERATION.READ, - permissionType: PERMISSION_TYPE.ALLOW + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW } - ] - - const writer = createRequest(creations) + ]) const reader = Reader.from(writer) // Read creations array @@ -165,31 +119,29 @@ test('createRequest serializes multiple ACL creations correctly', () => { deepStrictEqual(creationsArray.length, 3, 'Should correctly serialize multiple ACL creations') // Verify specific fields from different ACL creations - deepStrictEqual(creationsArray[0].operation, OPERATION.READ, 'First ACL should have READ operation') + deepStrictEqual(creationsArray[0].operation, AclOperations.READ, 'First ACL should have READ operation') - deepStrictEqual(creationsArray[1].operation, OPERATION.WRITE, 'Second ACL should have WRITE operation') + deepStrictEqual(creationsArray[1].operation, AclOperations.WRITE, 'Second ACL should have WRITE operation') - deepStrictEqual(creationsArray[2].resourceType, RESOURCE_TYPE.GROUP, 'Third ACL should have GROUP resource type') + deepStrictEqual(creationsArray[2].resourceType, ResourceTypes.GROUP, 'Third ACL should have GROUP resource type') }) test('createRequest serializes pattern type correctly', () => { - const patternTypes = [PATTERN_TYPE.LITERAL, PATTERN_TYPE.PREFIXED] + const patternTypes = [PatternTypes.LITERAL, PatternTypes.PREFIXED] // Test each pattern type for (const patternType of patternTypes) { - const creations = [ + const writer = createRequest([ { - resourceType: RESOURCE_TYPE.TOPIC, + resourceType: ResourceTypes.TOPIC, resourceName: 'test-topic', - resourcePatternType: patternType, + patternType, principal: 'User:test-user', host: '*', - operation: OPERATION.READ, - permissionType: PERMISSION_TYPE.ALLOW + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW } - ] - - const writer = createRequest(creations) + ]) const reader = Reader.from(writer) // Read creation and verify pattern type diff --git a/test/apis/admin/delete-acls-v3.test.ts b/test/apis/admin/delete-acls-v3.test.ts index c7acc76..8e4b2e6 100644 --- a/test/apis/admin/delete-acls-v3.test.ts +++ b/test/apis/admin/delete-acls-v3.test.ts @@ -1,65 +1,31 @@ import { deepStrictEqual, ok, throws } from 'node:assert' import test from 'node:test' -import { deleteAclsV3, Reader, ResponseError, Writer } from '../../../src/index.ts' +import { + AclOperations, + AclPermissionTypes, + deleteAclsV3, + PatternTypes, + Reader, + ResourceTypes, + ResponseError, + Writer +} from '../../../src/index.ts' +import { type DeleteAclsResponseFilterResults } from '../../../src/apis/admin/delete-acls-v3.ts' const { createRequest, parseResponse } = deleteAclsV3 -// Constants for ACL resource types, pattern types, operations, and permission types -const RESOURCE_TYPE = { - UNKNOWN: 0, - ANY: 1, - TOPIC: 2, - GROUP: 3, - CLUSTER: 4, - TRANSACTIONAL_ID: 5, - DELEGATION_TOKEN: 6 -} - -const PATTERN_TYPE = { - UNKNOWN: 0, - ANY: 1, - MATCH: 2, - LITERAL: 3, - PREFIXED: 4 -} - -const OPERATION = { - UNKNOWN: 0, - ANY: 1, - ALL: 2, - READ: 3, - WRITE: 4, - CREATE: 5, - DELETE: 6, - ALTER: 7, - DESCRIBE: 8, - CLUSTER_ACTION: 9, - DESCRIBE_CONFIGS: 10, - ALTER_CONFIGS: 11, - IDEMPOTENT_WRITE: 12 -} - -const PERMISSION_TYPE = { - UNKNOWN: 0, - ANY: 1, - DENY: 2, - ALLOW: 3 -} - test('createRequest serializes a single filter correctly', () => { - const filters = [ + const writer = createRequest([ { - resourceTypeFilter: RESOURCE_TYPE.TOPIC, - resourceNameFilter: 'test-topic', - patternTypeFilter: PATTERN_TYPE.LITERAL, - principalFilter: 'User:test-user', - hostFilter: '*', - operation: OPERATION.READ, - permissionType: PERMISSION_TYPE.ALLOW + resourceType: ResourceTypes.TOPIC, + resourceName: 'test-topic', + patternType: PatternTypes.LITERAL, + principal: 'User:test-user', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW } - ] - - const writer = createRequest(filters) + ]) // Verify it returns a Writer ok(writer instanceof Writer, 'Should return a Writer instance') @@ -69,20 +35,20 @@ test('createRequest serializes a single filter correctly', () => { // Read filters array const filtersArray = reader.readArray(() => { - const resourceTypeFilter = reader.readInt8() - const resourceNameFilter = reader.readNullableString() - const patternTypeFilter = reader.readInt8() - const principalFilter = reader.readNullableString() - const hostFilter = reader.readNullableString() + const resourceType = reader.readInt8() + const resourceName = reader.readNullableString() + const resourcePatternType = reader.readInt8() + const principal = reader.readNullableString() + const host = reader.readNullableString() const operation = reader.readInt8() const permissionType = reader.readInt8() return { - resourceTypeFilter, - resourceNameFilter, - patternTypeFilter, - principalFilter, - hostFilter, + resourceType, + resourceName, + resourcePatternType, + principal, + host, operation, permissionType } @@ -93,13 +59,13 @@ test('createRequest serializes a single filter correctly', () => { filtersArray, [ { - resourceTypeFilter: RESOURCE_TYPE.TOPIC, - resourceNameFilter: 'test-topic', - patternTypeFilter: PATTERN_TYPE.LITERAL, - principalFilter: 'User:test-user', - hostFilter: '*', - operation: OPERATION.READ, - permissionType: PERMISSION_TYPE.ALLOW + resourceType: ResourceTypes.TOPIC, + resourceName: 'test-topic', + resourcePatternType: PatternTypes.LITERAL, + principal: 'User:test-user', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW } ], 'Serialized data should match expected values' @@ -107,46 +73,44 @@ test('createRequest serializes a single filter correctly', () => { }) test('createRequest serializes multiple filters correctly', () => { - const filters = [ + const writer = createRequest([ { - resourceTypeFilter: RESOURCE_TYPE.TOPIC, - resourceNameFilter: 'test-topic', - patternTypeFilter: PATTERN_TYPE.LITERAL, - principalFilter: 'User:test-user', - hostFilter: '*', - operation: OPERATION.READ, - permissionType: PERMISSION_TYPE.ALLOW + resourceType: ResourceTypes.TOPIC, + resourceName: 'test-topic', + patternType: PatternTypes.LITERAL, + principal: 'User:test-user', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW }, { - resourceTypeFilter: RESOURCE_TYPE.GROUP, - resourceNameFilter: 'test-group', - patternTypeFilter: PATTERN_TYPE.LITERAL, - principalFilter: 'User:test-user', - hostFilter: '*', - operation: OPERATION.READ, - permissionType: PERMISSION_TYPE.ALLOW + resourceType: ResourceTypes.GROUP, + resourceName: 'test-group', + patternType: PatternTypes.LITERAL, + principal: 'User:test-user', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW } - ] - - const writer = createRequest(filters) + ]) const reader = Reader.from(writer) // Read filters array const filtersArray = reader.readArray(() => { - const resourceTypeFilter = reader.readInt8() - const resourceNameFilter = reader.readNullableString() - const patternTypeFilter = reader.readInt8() - const principalFilter = reader.readNullableString() - const hostFilter = reader.readNullableString() + const resourceType = reader.readInt8() + const resourceName = reader.readNullableString() + const resourcePatternType = reader.readInt8() + const principal = reader.readNullableString() + const host = reader.readNullableString() const operation = reader.readInt8() const permissionType = reader.readInt8() return { - resourceTypeFilter, - resourceNameFilter, - patternTypeFilter, - principalFilter, - hostFilter, + resourceType, + resourceName, + resourcePatternType, + principal, + host, operation, permissionType } @@ -155,51 +119,41 @@ test('createRequest serializes multiple filters correctly', () => { // Verify multiple filters deepStrictEqual(filtersArray.length, 2, 'Should correctly serialize multiple filters') - deepStrictEqual( - filtersArray[0].resourceTypeFilter, - RESOURCE_TYPE.TOPIC, - 'First filter should have TOPIC resource type' - ) + deepStrictEqual(filtersArray[0].resourceType, ResourceTypes.TOPIC, 'First filter should have TOPIC resource type') - deepStrictEqual( - filtersArray[1].resourceTypeFilter, - RESOURCE_TYPE.GROUP, - 'Second filter should have GROUP resource type' - ) + deepStrictEqual(filtersArray[1].resourceType, ResourceTypes.GROUP, 'Second filter should have GROUP resource type') }) test('createRequest serializes filters with null values correctly', () => { - const filters = [ + const writer = createRequest([ { - resourceTypeFilter: RESOURCE_TYPE.ANY, - resourceNameFilter: null, - patternTypeFilter: PATTERN_TYPE.ANY, - principalFilter: null, - hostFilter: null, - operation: OPERATION.ANY, - permissionType: PERMISSION_TYPE.ANY + resourceType: ResourceTypes.ANY, + resourceName: null, + patternType: PatternTypes.ANY, + principal: null, + host: null, + operation: AclOperations.ANY, + permissionType: AclPermissionTypes.ANY } - ] - - const writer = createRequest(filters) + ]) const reader = Reader.from(writer) // Read filters array const filtersArray = reader.readArray(() => { - const resourceTypeFilter = reader.readInt8() - const resourceNameFilter = reader.readNullableString() - const patternTypeFilter = reader.readInt8() - const principalFilter = reader.readNullableString() - const hostFilter = reader.readNullableString() + const resourceType = reader.readInt8() + const resourceName = reader.readNullableString() + const resourcePatternType = reader.readInt8() + const principal = reader.readNullableString() + const host = reader.readNullableString() const operation = reader.readInt8() const permissionType = reader.readInt8() return { - resourceTypeFilter, - resourceNameFilter, - patternTypeFilter, - principalFilter, - hostFilter, + resourceType, + resourceName, + resourcePatternType, + principal, + host, operation, permissionType } @@ -208,14 +162,14 @@ test('createRequest serializes filters with null values correctly', () => { // Verify null values deepStrictEqual( { - resourceNameFilter: filtersArray[0].resourceNameFilter, - principalFilter: filtersArray[0].principalFilter, - hostFilter: filtersArray[0].hostFilter + resourceName: filtersArray[0].resourceName, + principal: filtersArray[0].principal, + host: filtersArray[0].host }, { - resourceNameFilter: null, - principalFilter: null, - hostFilter: null + resourceName: null, + principal: null, + host: null }, 'Null filter values should be serialized correctly' ) @@ -234,13 +188,13 @@ test('parseResponse correctly processes a successful response with matching ACLs { errorCode: 0, errorMessage: null, - resourceType: RESOURCE_TYPE.TOPIC, + resourceType: ResourceTypes.TOPIC, resourceName: 'test-topic', - patternType: PATTERN_TYPE.LITERAL, + patternType: PatternTypes.LITERAL, principal: 'User:test-user', host: '*', - operation: OPERATION.READ, - permissionType: PERMISSION_TYPE.ALLOW + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW } ] } @@ -270,7 +224,7 @@ test('parseResponse correctly processes a successful response with matching ACLs deepStrictEqual( response.filterResults[0].matchingAcls[0].resourceType, - RESOURCE_TYPE.TOPIC, + ResourceTypes.TOPIC, 'Matching ACL should have TOPIC resource type' ) @@ -294,13 +248,13 @@ test('parseResponse correctly processes multiple filter results', () => { { errorCode: 0, errorMessage: null, - resourceType: RESOURCE_TYPE.TOPIC, + resourceType: ResourceTypes.TOPIC, resourceName: 'test-topic', - patternType: PATTERN_TYPE.LITERAL, + patternType: PatternTypes.LITERAL, principal: 'User:test-user', host: '*', - operation: OPERATION.READ, - permissionType: PERMISSION_TYPE.ALLOW + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW } ] }, @@ -311,17 +265,17 @@ test('parseResponse correctly processes multiple filter results', () => { { errorCode: 0, errorMessage: null, - resourceType: RESOURCE_TYPE.GROUP, + resourceType: ResourceTypes.GROUP, resourceName: 'test-group', - patternType: PATTERN_TYPE.LITERAL, + patternType: PatternTypes.LITERAL, principal: 'User:test-user', host: '*', - operation: OPERATION.READ, - permissionType: PERMISSION_TYPE.ALLOW + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW } ] } - ], + ] as DeleteAclsResponseFilterResults[], (w, filterResult) => { w.appendInt16(filterResult.errorCode) .appendString(filterResult.errorMessage) @@ -347,13 +301,13 @@ test('parseResponse correctly processes multiple filter results', () => { deepStrictEqual( response.filterResults[0].matchingAcls[0].resourceType, - RESOURCE_TYPE.TOPIC, + ResourceTypes.TOPIC, 'First filter result should match TOPIC resource' ) deepStrictEqual( response.filterResults[1].matchingAcls[0].resourceType, - RESOURCE_TYPE.GROUP, + ResourceTypes.GROUP, 'Second filter result should match GROUP resource' ) }) @@ -371,24 +325,24 @@ test('parseResponse correctly processes multiple matching ACLs in a filter', () { errorCode: 0, errorMessage: null, - resourceType: RESOURCE_TYPE.TOPIC, + resourceType: ResourceTypes.TOPIC, resourceName: 'test-topic', - patternType: PATTERN_TYPE.LITERAL, + patternType: PatternTypes.LITERAL, principal: 'User:test-user', host: '*', - operation: OPERATION.READ, - permissionType: PERMISSION_TYPE.ALLOW + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW }, { errorCode: 0, errorMessage: null, - resourceType: RESOURCE_TYPE.TOPIC, + resourceType: ResourceTypes.TOPIC, resourceName: 'test-topic', - patternType: PATTERN_TYPE.LITERAL, + patternType: PatternTypes.LITERAL, principal: 'User:test-user', host: '*', - operation: OPERATION.WRITE, - permissionType: PERMISSION_TYPE.ALLOW + operation: AclOperations.WRITE, + permissionType: AclPermissionTypes.ALLOW } ] } @@ -418,13 +372,13 @@ test('parseResponse correctly processes multiple matching ACLs in a filter', () deepStrictEqual( response.filterResults[0].matchingAcls[0].operation, - OPERATION.READ, + AclOperations.READ, 'First matching ACL should have READ operation' ) deepStrictEqual( response.filterResults[0].matchingAcls[1].operation, - OPERATION.WRITE, + AclOperations.WRITE, 'Second matching ACL should have WRITE operation' ) }) @@ -484,13 +438,13 @@ test('parseResponse handles matching ACL level errors correctly', () => { { errorCode: 38, // SECURITY_DISABLED errorMessage: 'Security is disabled', - resourceType: RESOURCE_TYPE.TOPIC, + resourceType: ResourceTypes.TOPIC, resourceName: 'test-topic', - patternType: PATTERN_TYPE.LITERAL, + patternType: PatternTypes.LITERAL, principal: 'User:test-user', host: '*', - operation: OPERATION.READ, - permissionType: PERMISSION_TYPE.ALLOW + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW } ] } diff --git a/test/apis/admin/describe-acls-v3.test.ts b/test/apis/admin/describe-acls-v3.test.ts index ee61037..a82c776 100644 --- a/test/apis/admin/describe-acls-v3.test.ts +++ b/test/apis/admin/describe-acls-v3.test.ts @@ -1,69 +1,36 @@ import { deepStrictEqual, ok, throws } from 'node:assert' import test from 'node:test' -import { describeAclsV3, Reader, ResponseError, Writer } from '../../../src/index.ts' +import { + AclOperations, + AclPermissionTypes, + describeAclsV3, + PatternTypes, + Reader, + ResourceTypes, + ResponseError, + Writer +} from '../../../src/index.ts' const { createRequest, parseResponse } = describeAclsV3 -// Constants for ACL resource types, pattern types, operations, and permission types -const RESOURCE_TYPE = { - UNKNOWN: 0, - ANY: 1, - TOPIC: 2, - GROUP: 3, - CLUSTER: 4, - TRANSACTIONAL_ID: 5, - DELEGATION_TOKEN: 6 -} - -const PATTERN_TYPE = { - UNKNOWN: 0, - ANY: 1, - MATCH: 2, - LITERAL: 3, - PREFIXED: 4 -} - -const OPERATION = { - UNKNOWN: 0, - ANY: 1, - ALL: 2, - READ: 3, - WRITE: 4, - CREATE: 5, - DELETE: 6, - ALTER: 7, - DESCRIBE: 8, - CLUSTER_ACTION: 9, - DESCRIBE_CONFIGS: 10, - ALTER_CONFIGS: 11, - IDEMPOTENT_WRITE: 12 -} - -const PERMISSION_TYPE = { - UNKNOWN: 0, - ANY: 1, - DENY: 2, - ALLOW: 3 -} - test('createRequest serializes all filters correctly', () => { - const resourceTypeFilter = RESOURCE_TYPE.TOPIC - const resourceNameFilter = 'test-topic' - const patternTypeFilter = PATTERN_TYPE.LITERAL - const principalFilter = 'User:test-user' - const hostFilter = '*' - const operation = OPERATION.READ - const permissionType = PERMISSION_TYPE.ALLOW - - const writer = createRequest( - resourceTypeFilter, - resourceNameFilter, - patternTypeFilter, - principalFilter, - hostFilter, + const resourceType = ResourceTypes.TOPIC + const resourceName = 'test-topic' + const patternType = PatternTypes.LITERAL + const principal = 'User:test-user' + const host = '*' + const operation = AclOperations.READ + const permissionType = AclPermissionTypes.ALLOW + + const writer = createRequest({ + resourceType, + resourceName, + patternType, + principal, + host, operation, permissionType - ) + }) // Verify it returns a Writer ok(writer instanceof Writer, 'Should return a Writer instance') @@ -72,79 +39,79 @@ test('createRequest serializes all filters correctly', () => { const reader = Reader.from(writer) // Read all filter parameters - const serializedResourceTypeFilter = reader.readInt8() - const serializedResourceNameFilter = reader.readNullableString() - const serializedPatternTypeFilter = reader.readInt8() - const serializedPrincipalFilter = reader.readNullableString() - const serializedHostFilter = reader.readNullableString() + const serializedResourceType = reader.readInt8() + const serializedResourceName = reader.readNullableString() + const serializedResourcePatternType = reader.readInt8() + const serializedPrincipal = reader.readNullableString() + const serializedHost = reader.readNullableString() const serializedOperation = reader.readInt8() const serializedPermissionType = reader.readInt8() // Verify serialized data deepStrictEqual( { - resourceTypeFilter: serializedResourceTypeFilter, - resourceNameFilter: serializedResourceNameFilter, - patternTypeFilter: serializedPatternTypeFilter, - principalFilter: serializedPrincipalFilter, - hostFilter: serializedHostFilter, + resourceType: serializedResourceType, + resourceName: serializedResourceName, + resourcePatternType: serializedResourcePatternType, + principal: serializedPrincipal, + host: serializedHost, operation: serializedOperation, permissionType: serializedPermissionType }, { - resourceTypeFilter: RESOURCE_TYPE.TOPIC, - resourceNameFilter: 'test-topic', - patternTypeFilter: PATTERN_TYPE.LITERAL, - principalFilter: 'User:test-user', - hostFilter: '*', - operation: OPERATION.READ, - permissionType: PERMISSION_TYPE.ALLOW + resourceType: ResourceTypes.TOPIC, + resourceName: 'test-topic', + resourcePatternType: PatternTypes.LITERAL, + principal: 'User:test-user', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW }, 'Serialized data should match expected values' ) }) test('createRequest serializes null filters correctly', () => { - const resourceTypeFilter = RESOURCE_TYPE.ANY - const resourceNameFilter = null - const patternTypeFilter = PATTERN_TYPE.ANY - const principalFilter = null - const hostFilter = null - const operation = OPERATION.ANY - const permissionType = PERMISSION_TYPE.ANY - - const writer = createRequest( - resourceTypeFilter, - resourceNameFilter, - patternTypeFilter, - principalFilter, - hostFilter, + const resourceType = ResourceTypes.ANY + const resourceName = null + const patternType = PatternTypes.ANY + const principal = null + const host = null + const operation = AclOperations.ANY + const permissionType = AclPermissionTypes.ANY + + const writer = createRequest({ + resourceType, + resourceName, + patternType, + principal, + host, operation, permissionType - ) + }) const reader = Reader.from(writer) // Read all filter parameters - const serializedResourceTypeFilter = reader.readInt8() - const serializedResourceNameFilter = reader.readNullableString() - const serializedPatternTypeFilter = reader.readInt8() - const serializedPrincipalFilter = reader.readNullableString() - const serializedHostFilter = reader.readNullableString() + const serializedResourceType = reader.readInt8() + const serializedResourceName = reader.readNullableString() + const serializedResourcePatternType = reader.readInt8() + const serializedPrincipal = reader.readNullableString() + const serializedHost = reader.readNullableString() const serializedOperation = reader.readInt8() const serializedPermissionType = reader.readInt8() // Verify null values are serialized correctly deepStrictEqual( { - resourceNameFilter: serializedResourceNameFilter, - principalFilter: serializedPrincipalFilter, - hostFilter: serializedHostFilter + resourceName: serializedResourceName, + principal: serializedPrincipal, + host: serializedHost }, { - resourceNameFilter: null, - principalFilter: null, - hostFilter: null + resourceName: null, + principal: null, + host: null }, 'Null filter values should be serialized correctly' ) @@ -152,16 +119,16 @@ test('createRequest serializes null filters correctly', () => { // Verify ANY values are serialized correctly deepStrictEqual( { - resourceTypeFilter: serializedResourceTypeFilter, - patternTypeFilter: serializedPatternTypeFilter, + resourceType: serializedResourceType, + resourcePatternType: serializedResourcePatternType, operation: serializedOperation, permissionType: serializedPermissionType }, { - resourceTypeFilter: RESOURCE_TYPE.ANY, - patternTypeFilter: PATTERN_TYPE.ANY, - operation: OPERATION.ANY, - permissionType: PERMISSION_TYPE.ANY + resourceType: ResourceTypes.ANY, + resourcePatternType: PatternTypes.ANY, + operation: AclOperations.ANY, + permissionType: AclPermissionTypes.ANY }, 'ANY filter values should be serialized correctly' ) @@ -200,15 +167,15 @@ test('parseResponse correctly processes a successful response with resources', ( .appendArray( [ { - resourceType: RESOURCE_TYPE.TOPIC, + resourceType: ResourceTypes.TOPIC, resourceName: 'test-topic', - patternType: PATTERN_TYPE.LITERAL, + patternType: PatternTypes.LITERAL, acls: [ { principal: 'User:test-user', host: '*', - operation: OPERATION.READ, - permissionType: PERMISSION_TYPE.ALLOW + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW } ] } @@ -238,15 +205,15 @@ test('parseResponse correctly processes a successful response with resources', ( errorMessage: null, resources: [ { - resourceType: RESOURCE_TYPE.TOPIC, + resourceType: ResourceTypes.TOPIC, resourceName: 'test-topic', - patternType: PATTERN_TYPE.LITERAL, + patternType: PatternTypes.LITERAL, acls: [ { principal: 'User:test-user', host: '*', - operation: OPERATION.READ, - permissionType: PERMISSION_TYPE.ALLOW + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW } ] } @@ -265,28 +232,28 @@ test('parseResponse correctly processes multiple resources', () => { .appendArray( [ { - resourceType: RESOURCE_TYPE.TOPIC, + resourceType: ResourceTypes.TOPIC, resourceName: 'test-topic', - patternType: PATTERN_TYPE.LITERAL, + patternType: PatternTypes.LITERAL, acls: [ { principal: 'User:test-user', host: '*', - operation: OPERATION.READ, - permissionType: PERMISSION_TYPE.ALLOW + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW } ] }, { - resourceType: RESOURCE_TYPE.GROUP, + resourceType: ResourceTypes.GROUP, resourceName: 'test-group', - patternType: PATTERN_TYPE.LITERAL, + patternType: PatternTypes.LITERAL, acls: [ { principal: 'User:test-user', host: '*', - operation: OPERATION.READ, - permissionType: PERMISSION_TYPE.ALLOW + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW } ] } @@ -310,9 +277,9 @@ test('parseResponse correctly processes multiple resources', () => { // Verify multiple resources deepStrictEqual(response.resources.length, 2, 'Response should contain 2 resources') - deepStrictEqual(response.resources[0].resourceType, RESOURCE_TYPE.TOPIC, 'First resource should be a TOPIC') + deepStrictEqual(response.resources[0].resourceType, ResourceTypes.TOPIC, 'First resource should be a TOPIC') - deepStrictEqual(response.resources[1].resourceType, RESOURCE_TYPE.GROUP, 'Second resource should be a GROUP') + deepStrictEqual(response.resources[1].resourceType, ResourceTypes.GROUP, 'Second resource should be a GROUP') }) test('parseResponse correctly processes multiple ACLs per resource', () => { @@ -324,21 +291,21 @@ test('parseResponse correctly processes multiple ACLs per resource', () => { .appendArray( [ { - resourceType: RESOURCE_TYPE.TOPIC, + resourceType: ResourceTypes.TOPIC, resourceName: 'test-topic', - patternType: PATTERN_TYPE.LITERAL, + patternType: PatternTypes.LITERAL, acls: [ { principal: 'User:test-user', host: '*', - operation: OPERATION.READ, - permissionType: PERMISSION_TYPE.ALLOW + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW }, { principal: 'User:test-user', host: '*', - operation: OPERATION.WRITE, - permissionType: PERMISSION_TYPE.ALLOW + operation: AclOperations.WRITE, + permissionType: AclPermissionTypes.ALLOW } ] } @@ -362,9 +329,13 @@ test('parseResponse correctly processes multiple ACLs per resource', () => { // Verify multiple ACLs deepStrictEqual(response.resources[0].acls.length, 2, 'Resource should have 2 ACLs') - deepStrictEqual(response.resources[0].acls[0].operation, OPERATION.READ, 'First ACL should have READ operation') + deepStrictEqual(response.resources[0].acls[0].operation, AclOperations.READ, 'First ACL should have READ operation') - deepStrictEqual(response.resources[0].acls[1].operation, OPERATION.WRITE, 'Second ACL should have WRITE operation') + deepStrictEqual( + response.resources[0].acls[1].operation, + AclOperations.WRITE, + 'Second ACL should have WRITE operation' + ) }) test('parseResponse handles error response correctly', () => { diff --git a/test/clients/admin/admin.test.ts b/test/clients/admin/admin.test.ts index 6528779..fd1063c 100644 --- a/test/clients/admin/admin.test.ts +++ b/test/clients/admin/admin.test.ts @@ -5,7 +5,10 @@ import { scheduler } from 'node:timers/promises' import { ClientQuotaEntityTypes, ClientQuotaKeys } from '../../../src/apis/enumerations.ts' import { kConnections } from '../../../src/clients/base/base.ts' import { + AclOperations, + AclPermissionTypes, Admin, + adminAclsChannel, adminClientQuotasChannel, adminGroupsChannel, adminLogDirsChannel, @@ -16,7 +19,10 @@ import { ClientQuotaMatchTypes, type ClusterPartitionMetadata, Consumer, + createAclsV3, type CreatedTopic, + deleteAclsV3, + describeAclsV3, type DescribeClientQuotasOptions, describeClientQuotasV0, describeGroupsV5, @@ -26,6 +32,8 @@ import { instancesChannel, listGroupsV5, MultipleErrors, + PatternTypes, + ResourceTypes, sleep, UnsupportedApiError } from '../../../src/index.ts' @@ -2409,3 +2417,669 @@ test('describeLogDirs should handle unavailable API errors', async t => { strictEqual(error.errors[0].message.includes('Unsupported API DescribeLogDirs.'), true) } }) + +const aclTestCases = [ + { + resourceType: ResourceTypes.TOPIC, + resourceName: 'test-topic', + patternType: PatternTypes.LITERAL, + principal: 'User:test-user', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW + }, + { + resourceType: ResourceTypes.GROUP, + resourceName: 'test-group', + patternType: PatternTypes.LITERAL, + principal: 'User:test-user2', + host: '192.168.1.1', + operation: AclOperations.WRITE, + permissionType: AclPermissionTypes.DENY + }, + { + resourceType: ResourceTypes.CLUSTER, + resourceName: 'kafka-cluster', + patternType: PatternTypes.LITERAL, + principal: 'User:admin', + host: '*', + operation: AclOperations.ALTER, + permissionType: AclPermissionTypes.ALLOW + } +] + +for (const testCase of aclTestCases) { + test(`ACLs should be created, described, and deleted for ${testCase.principal} on ${testCase.resourceName}`, async t => { + const admin = createAdmin(t) + + const createOptions = { + creations: [testCase] + } + + await admin.createAcls(createOptions) + + const describeOptions = { + filter: { + resourceType: testCase.resourceType, + resourceName: testCase.resourceName, + patternType: testCase.patternType, + principal: testCase.principal, + host: testCase.host, + operation: testCase.operation, + permissionType: testCase.permissionType + } + } + + await scheduler.wait(1000) + + const describeResult = await admin.describeAcls(describeOptions) + + deepStrictEqual(describeResult, [ + { + resourceType: testCase.resourceType, + resourceName: testCase.resourceName, + patternType: testCase.patternType, + acls: [ + { + principal: testCase.principal, + host: testCase.host, + operation: testCase.operation, + permissionType: testCase.permissionType + } + ] + } + ]) + + const deleteOptions = { + filters: [testCase] + } + + const deleteResult = await admin.deleteAcls(deleteOptions) + + deepStrictEqual(deleteResult, [testCase]) + + await scheduler.wait(1000) + + const verifyResult = await admin.describeAcls(describeOptions) + deepStrictEqual(verifyResult, []) + }) +} + +test('createAcls, describeAcls, and deleteAcls should support diagnostic channels', async t => { + const admin = createAdmin(t) + + const testAcl = { + resourceType: ResourceTypes.TOPIC, + resourceName: 'test-topic-diagnostic', + patternType: PatternTypes.LITERAL, + principal: 'User:test-diagnostic', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW + } + + const createOptions = { creations: [testAcl] } + + const verifyCreateTracingChannel = createTracingChannelVerifier( + adminAclsChannel, + 'client', + { + start (context: ClientDiagnosticEvent) { + deepStrictEqual(context, { + client: admin, + operation: 'createAcls', + options: createOptions, + operationId: mockedOperationId + }) + }, + error (context: ClientDiagnosticEvent) { + ok(typeof context === 'undefined') + } + }, + (_label: string, data: ClientDiagnosticEvent) => data.operation === 'createAcls' + ) + + await admin.createAcls(createOptions) + verifyCreateTracingChannel() + + const describeOptions = { + filter: { + resourceType: testAcl.resourceType, + resourceName: testAcl.resourceName, + patternType: testAcl.patternType, + principal: testAcl.principal, + host: testAcl.host, + operation: testAcl.operation, + permissionType: testAcl.permissionType + } + } + + const verifyDescribeTracingChannel = createTracingChannelVerifier( + adminAclsChannel, + 'client', + { + start (context: ClientDiagnosticEvent) { + deepStrictEqual(context, { + client: admin, + operation: 'describeAcls', + options: describeOptions, + operationId: mockedOperationId + }) + }, + asyncStart (context: ClientDiagnosticEvent) { + const result = context.result as any + ok(Array.isArray(result)) + }, + error (context: ClientDiagnosticEvent) { + ok(typeof context === 'undefined') + } + }, + (_label: string, data: ClientDiagnosticEvent) => data.operation === 'describeAcls' + ) + + await admin.describeAcls(describeOptions) + verifyDescribeTracingChannel() + + const deleteOptions = { filters: [testAcl] } + + const verifyDeleteTracingChannel = createTracingChannelVerifier( + adminAclsChannel, + 'client', + { + start (context: ClientDiagnosticEvent) { + deepStrictEqual(context, { + client: admin, + operation: 'deleteAcls', + options: deleteOptions, + operationId: mockedOperationId + }) + }, + asyncStart (context: ClientDiagnosticEvent) { + const result = context.result as any + ok(Array.isArray(result)) + }, + error (context: ClientDiagnosticEvent) { + ok(typeof context === 'undefined') + } + }, + (_label: string, data: ClientDiagnosticEvent) => data.operation === 'deleteAcls' + ) + + await admin.deleteAcls(deleteOptions) + verifyDeleteTracingChannel() +}) + +test('createAcls should validate options in strict mode', async t => { + const admin = createAdmin(t, { strict: true }) + + // Test with missing required field (creations) + try { + // @ts-expect-error - Intentionally passing invalid options + await admin.createAcls({}) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error.message.includes('creations'), true) + } + + // Test with invalid type for creations + try { + // @ts-expect-error - Intentionally passing invalid options + await admin.createAcls({ creations: 'not-an-array' }) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error.message.includes('creations'), true) + } + + // Test with empty creations array + try { + await admin.createAcls({ creations: [] }) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error.message.includes('creations'), true) + } + + // Test with invalid ACL object (missing resourceType) + try { + await admin.createAcls({ + creations: [ + { + resourceName: 'test-topic', + patternType: PatternTypes.LITERAL, + principal: 'User:test', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW + } + ] as any + }) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error.message.includes('resourceType'), true) + } + + // Test with invalid additional property + try { + await admin.createAcls({ + creations: [ + { + resourceType: ResourceTypes.TOPIC, + resourceName: 'test-topic', + patternType: PatternTypes.LITERAL, + principal: 'User:test', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW, + invalidProperty: true + } + ], + invalidOption: true + } as any) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error.message.includes('must NOT have additional properties'), true) + } +}) + +test('describeAcls should validate options in strict mode', async t => { + const admin = createAdmin(t, { strict: true }) + + // Test with missing required field (filter) + try { + // @ts-expect-error - Intentionally passing invalid options + await admin.describeAcls({}) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error.message.includes('filter'), true) + } + + // Test with invalid filter object (missing resourceType) + try { + await admin.describeAcls({ + filter: { + resourceName: 'test-topic', + patternType: PatternTypes.LITERAL, + principal: 'User:test', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW + } as any + }) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error.message.includes('resourceType'), true) + } + + // Test with invalid additional property + try { + await admin.describeAcls({ + filter: { + resourceType: ResourceTypes.TOPIC, + resourceName: 'test-topic', + patternType: PatternTypes.LITERAL, + principal: 'User:test', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW + }, + invalidProperty: true + } as any) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error.message.includes('must NOT have additional properties'), true) + } +}) + +test('deleteAcls should validate options in strict mode', async t => { + const admin = createAdmin(t, { strict: true }) + + // Test with missing required field (filters) + try { + // @ts-expect-error - Intentionally passing invalid options + await admin.deleteAcls({}) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error.message.includes('filters'), true) + } + + // Test with invalid type for filters + try { + // @ts-expect-error - Intentionally passing invalid options + await admin.deleteAcls({ filters: 'not-an-array' }) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error.message.includes('filters'), true) + } + + // Test with empty filters array + try { + await admin.deleteAcls({ filters: [] }) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error.message.includes('filters'), true) + } + + // Test with invalid additional property + try { + await admin.deleteAcls({ + filters: [ + { + resourceType: ResourceTypes.TOPIC, + resourceName: 'test-topic', + patternType: PatternTypes.LITERAL, + principal: 'User:test', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW + } + ], + invalidProperty: true + } as any) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error.message.includes('must NOT have additional properties'), true) + } +}) + +test('createAcls should handle errors from Connection.getFirstAvailable', async t => { + const admin = createAdmin(t) + + mockConnectionPoolGetFirstAvailable(admin[kConnections], 1) + + try { + await admin.createAcls({ + creations: [ + { + resourceType: ResourceTypes.TOPIC, + resourceName: 'test-topic', + patternType: PatternTypes.LITERAL, + principal: 'User:test', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW + } + ] + }) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error instanceof MultipleErrors, true) + strictEqual(error.message.includes('Creating ACLs failed.'), true) + } +}) + +test('describeAcls should handle errors from Connection.getFirstAvailable', async t => { + const admin = createAdmin(t) + + mockConnectionPoolGetFirstAvailable(admin[kConnections], 1) + + try { + await admin.describeAcls({ + filter: { + resourceType: ResourceTypes.TOPIC, + resourceName: 'test-topic', + patternType: PatternTypes.LITERAL, + principal: 'User:test', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW + } + }) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error instanceof MultipleErrors, true) + strictEqual(error.message.includes('Describing ACLs failed.'), true) + } +}) + +test('deleteAcls should handle errors from Connection.getFirstAvailable', async t => { + const admin = createAdmin(t) + + mockConnectionPoolGetFirstAvailable(admin[kConnections], 1) + + try { + await admin.deleteAcls({ + filters: [ + { + resourceType: ResourceTypes.TOPIC, + resourceName: 'test-topic', + patternType: PatternTypes.LITERAL, + principal: 'User:test', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW + } + ] + }) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error instanceof MultipleErrors, true) + strictEqual(error.message.includes('Deleting ACLs failed.'), true) + } +}) + +test('createAcls should handle errors from Connection.get', async t => { + const admin = createAdmin(t) + + mockConnectionPoolGet(admin[kConnections], 1) + + try { + await admin.createAcls({ + creations: [ + { + resourceType: ResourceTypes.TOPIC, + resourceName: 'test-topic', + patternType: PatternTypes.LITERAL, + principal: 'User:test', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW + } + ] + }) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error instanceof MultipleErrors, true) + strictEqual(error.message.includes('Creating ACLs failed.'), true) + } +}) + +test('describeAcls should handle errors from Connection.get', async t => { + const admin = createAdmin(t) + + mockConnectionPoolGet(admin[kConnections], 1) + + try { + await admin.describeAcls({ + filter: { + resourceType: ResourceTypes.TOPIC, + resourceName: 'test-topic', + patternType: PatternTypes.LITERAL, + principal: 'User:test', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW + } + }) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error instanceof MultipleErrors, true) + strictEqual(error.message.includes('Describing ACLs failed.'), true) + } +}) + +test('deleteAcls should handle errors from Connection.get', async t => { + const admin = createAdmin(t) + + mockConnectionPoolGet(admin[kConnections], 1) + + try { + await admin.deleteAcls({ + filters: [ + { + resourceType: ResourceTypes.TOPIC, + resourceName: 'test-topic', + patternType: PatternTypes.LITERAL, + principal: 'User:test', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW + } + ] + }) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error instanceof MultipleErrors, true) + strictEqual(error.message.includes('Deleting ACLs failed.'), true) + } +}) + +test('createAcls should handle errors from the API', async t => { + const admin = createAdmin(t) + + mockAPI(admin[kConnections], createAclsV3.api.key) + + try { + await admin.createAcls({ + creations: [ + { + resourceType: ResourceTypes.TOPIC, + resourceName: 'test-topic', + patternType: PatternTypes.LITERAL, + principal: 'User:test', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW + } + ] + }) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error instanceof MultipleErrors, true) + strictEqual(error.message.includes('Creating ACLs failed.'), true) + } +}) + +test('describeAcls should handle errors from the API', async t => { + const admin = createAdmin(t) + + mockAPI(admin[kConnections], describeAclsV3.api.key) + + try { + await admin.describeAcls({ + filter: { + resourceType: ResourceTypes.TOPIC, + resourceName: 'test-topic', + patternType: PatternTypes.LITERAL, + principal: 'User:test', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW + } + }) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error instanceof MultipleErrors, true) + strictEqual(error.message.includes('Describing ACLs failed.'), true) + } +}) + +test('deleteAcls should handle errors from the API', async t => { + const admin = createAdmin(t) + + mockAPI(admin[kConnections], deleteAclsV3.api.key) + + try { + await admin.deleteAcls({ + filters: [ + { + resourceType: ResourceTypes.TOPIC, + resourceName: 'test-topic', + patternType: PatternTypes.LITERAL, + principal: 'User:test', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW + } + ] + }) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error instanceof MultipleErrors, true) + strictEqual(error.message.includes('Deleting ACLs failed.'), true) + } +}) + +test('createAcls should handle unavailable API errors (CreateAcls)', async t => { + const admin = createAdmin(t) + + mockUnavailableAPI(admin, 'CreateAcls') + + try { + await admin.createAcls({ + creations: [ + { + resourceType: ResourceTypes.TOPIC, + resourceName: 'test-topic', + patternType: PatternTypes.LITERAL, + principal: 'User:test', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW + } + ] + }) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error instanceof MultipleErrors, true) + strictEqual(error.errors[0].message.includes('Unsupported API CreateAcls.'), true) + } +}) + +test('describeAcls should handle unavailable API errors (DescribeAcls)', async t => { + const admin = createAdmin(t) + + mockUnavailableAPI(admin, 'DescribeAcls') + + try { + await admin.describeAcls({ + filter: { + resourceType: ResourceTypes.TOPIC, + resourceName: 'test-topic', + patternType: PatternTypes.LITERAL, + principal: 'User:test', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW + } + }) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error instanceof MultipleErrors, true) + strictEqual(error.errors[0].message.includes('Unsupported API DescribeAcls.'), true) + } +}) + +test('deleteAcls should handle unavailable API errors (DeleteAcls)', async t => { + const admin = createAdmin(t) + + mockUnavailableAPI(admin, 'DeleteAcls') + + try { + await admin.deleteAcls({ + filters: [ + { + resourceType: ResourceTypes.TOPIC, + resourceName: 'test-topic', + patternType: PatternTypes.LITERAL, + principal: 'User:test', + host: '*', + operation: AclOperations.READ, + permissionType: AclPermissionTypes.ALLOW + } + ] + }) + throw new Error('Expected error not thrown') + } catch (error) { + strictEqual(error instanceof MultipleErrors, true) + strictEqual(error.errors[0].message.includes('Unsupported API DeleteAcls.'), true) + } +})