diff --git a/src/v/pandaproxy/api/api-doc/schema_registry.json b/src/v/pandaproxy/api/api-doc/schema_registry.json index 7a65f88b28a4c..d2bfb4b4149a6 100644 --- a/src/v/pandaproxy/api/api-doc/schema_registry.json +++ b/src/v/pandaproxy/api/api-doc/schema_registry.json @@ -1433,6 +1433,1540 @@ } } }, + "/contexts/{context}/subjects/{subject}": { + "post": { + "summary": "Check if a schema is already registered for the subject (context-prefixed).", + "operationId": "ctx_post_subject", + "consumes": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "parameters": [ + { + "name": "context", + "description": "The context to scope the operation to (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "subject", + "description": "The subject name.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "normalize", + "in": "query", + "required": false, + "type": "boolean" + }, + { + "name": "deleted", + "in": "query", + "required": false, + "type": "boolean" + }, + { + "name": "format", + "in": "query", + "required": false, + "type": "string", + "description": "Redpanda version 25.2 or later. For Avro and Protobuf schemas only. Supported values: an empty string `''` returns the schema in its current format (default), and `serialized` (Protobuf only) returns the schema in its Base64-encoded wire binary format. Unsupported values return a 501 error." + }, + { + "name": "schema_def", + "in": "body", + "schema": { + "$ref": "#/definitions/schema_def_request" + } + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/stored_schema_response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "409": { + "description": "Incompatible schema", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "422": { + "description": "Invalid schema", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "501": { + "description": "Not Implemented: The specified format parameter value is not supported", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + }, + "delete": { + "summary": "Delete all schemas for the subject (context-prefixed).", + "operationId": "ctx_delete_subject", + "parameters": [ + { + "name": "context", + "description": "The context to scope the operation to (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "subject", + "description": "The subject name.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "permanent", + "in": "query", + "required": false, + "type": "boolean" + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + } + }, + "404": { + "description": "Not Found: Subject not found", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + } + }, + "/contexts/{context}/subjects/{subject}/versions": { + "get": { + "summary": "Retrieve a list of versions for a subject (context-prefixed).", + "operationId": "ctx_get_subject_versions", + "parameters": [ + { + "name": "context", + "description": "The context to scope the operation to (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "subject", + "description": "The subject name.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "deleted", + "in": "query", + "required": false, + "type": "boolean" + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + } + }, + "404": { + "description": "Not Found: Subject not found", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + }, + "post": { + "summary": "Create a new schema for the subject (context-prefixed).", + "operationId": "ctx_post_subject_versions", + "consumes": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "parameters": [ + { + "name": "context", + "description": "The context to scope the operation to (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "subject", + "description": "The subject name.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "normalize", + "in": "query", + "required": false, + "type": "boolean" + }, + { + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/stored_schema_request" + } + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/post_subject_versions_response" + } + }, + "409": { + "description": "Incompatible schema", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "422": { + "description": "Invalid schema", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + } + }, + "/contexts/{context}/subjects/{subject}/versions/{version}": { + "get": { + "summary": "Retrieve a schema for the subject and version (context-prefixed).", + "operationId": "ctx_get_subject_versions_version", + "parameters": [ + { + "name": "context", + "description": "The context to scope the operation to (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "subject", + "description": "The subject name.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "version", + "in": "path", + "description": "The schema version to retrieve. Use an integer for a specific version or 'latest' for the most recent version.", + "required": true, + "type": "string" + }, + { + "name": "deleted", + "in": "query", + "required": false, + "type": "boolean" + }, + { + "name": "format", + "in": "query", + "required": false, + "type": "string", + "description": "Redpanda version 25.2 or later. For Avro and Protobuf schemas only. Supported values: an empty string `''` returns the schema in its current format (default), and `serialized` (Protobuf only) returns the schema in its Base64-encoded wire binary format. Unsupported values return a 501 error." + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/get_subject_versions_version_response" + } + }, + "404": { + "description": "Not Found: Schema not found", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "422": { + "description": "Invalid version", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "501": { + "description": "Not Implemented: The specified format parameter value is not supported", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + }, + "delete": { + "summary": "Delete a schema for the subject and version (context-prefixed).", + "operationId": "ctx_delete_subject_version", + "parameters": [ + { + "name": "context", + "description": "The context to scope the operation to (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "subject", + "description": "The subject name.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "version", + "in": "path", + "description": "The schema version to delete. Use an integer for a specific version or 'latest' for the most recent version.", + "required": true, + "type": "string" + }, + { + "name": "permanent", + "in": "query", + "required": false, + "type": "boolean" + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "integer" + } + }, + "404": { + "description": "Not Found: Schema not found", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "422": { + "description": "Invalid version", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + } + }, + "/contexts/{context}/subjects/{subject}/versions/{version}/schema": { + "get": { + "summary": "Retrieve the raw schema for the subject (context-prefixed).", + "operationId": "ctx_get_subject_versions_version_schema", + "description": "Returns the specified version of the schema in its original format, without backslashes.", + "parameters": [ + { + "name": "context", + "description": "The context to scope the operation to (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "subject", + "description": "The subject name.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "version", + "in": "path", + "description": "The schema version to retrieve. Use an integer for a specific version or 'latest' for the most recent version.", + "required": true, + "type": "string" + }, + { + "name": "deleted", + "in": "query", + "required": false, + "type": "boolean" + }, + { + "name": "format", + "in": "query", + "required": false, + "type": "string", + "description": "Redpanda version 25.2 or later. For Avro and Protobuf schemas only. Supported values: an empty string `''` returns the schema in its current format (default), and `serialized` (Protobuf only) returns the schema in its Base64-encoded wire binary format. Unsupported values return a 501 error." + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found: Schema not found", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "422": { + "description": "Invalid version", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "501": { + "description": "Not Implemented: The specified format parameter value is not supported", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + } + }, + "/contexts/{context}/subjects/{subject}/versions/{version}/referencedby": { + "get": { + "summary": "Retrieve a list of schema IDs that reference the subject and version (context-prefixed).", + "operationId": "ctx_get_subject_versions_version_referenced_by", + "parameters": [ + { + "name": "context", + "description": "The context to scope the operation to (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "subject", + "description": "The subject name.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "version", + "in": "path", + "description": "The schema version to check. Use an integer for a specific version or 'latest' for the most recent version.", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + } + }, + "404": { + "description": "Not Found: Schema not found", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "422": { + "description": "Invalid version", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + } + }, + "/contexts/{context}/compatibility/subjects/{subject}/versions/{version}": { + "post": { + "summary": "Test compatibility of a schema for the subject and version (context-prefixed).", + "operationId": "ctx_compatibility_subject_version", + "consumes": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "parameters": [ + { + "name": "context", + "description": "The context to scope the operation to (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "subject", + "description": "The subject name.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "version", + "in": "path", + "description": "The schema version to check compatibility against. Use an integer for a specific version or 'latest' for the most recent version.", + "required": true, + "type": "string" + }, + { + "name": "schema_def", + "in": "body", + "schema": { + "$ref": "#/definitions/schema_def_request" + } + }, + { + "name": "verbose", + "in": "query", + "description": "If true, includes more verbose information about the compatibility check, for example the reason the check failed.", + "required": false, + "type": "boolean" + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/is_compatibile" + } + }, + "409": { + "description": "Incompatible schema", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "422": { + "description": "Invalid schema", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + } + }, + "/contexts/{context}/config/{subject}": { + "get": { + "summary": "Get the compatibility level for a subject (context-prefixed).", + "operationId": "ctx_get_config_subject", + "parameters": [ + { + "name": "context", + "description": "The context to scope the operation to (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "subject", + "description": "The subject to get the config for.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "defaultToGlobal", + "in": "query", + "required": false, + "type": "boolean" + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/get_compatibility" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + }, + "put": { + "summary": "Set the compatibility level for a subject (context-prefixed).", + "operationId": "ctx_put_config_subject", + "consumes": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "parameters": [ + { + "name": "context", + "description": "The context to scope the operation to (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "subject", + "description": "The subject to set the config for.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "config", + "in": "body", + "schema": { + "$ref": "#/definitions/put_compatibility" + } + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/put_compatibility" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + }, + "delete": { + "summary": "Delete the compatibility level for a subject (context-prefixed).", + "operationId": "ctx_delete_config_subject", + "parameters": [ + { + "name": "context", + "description": "The context to scope the operation to (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "subject", + "description": "The subject to delete the config for.", + "in": "path", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/get_compatibility" + } + }, + "404": { + "description": "Not Found: Subject not found", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + } + }, + "/contexts/{context}/mode/{subject}": { + "get": { + "summary": "Get the mode for a subject (context-prefixed).", + "operationId": "ctx_get_mode_subject", + "parameters": [ + { + "name": "context", + "description": "The context to scope the operation to (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "subject", + "description": "The subject to get the mode for.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "defaultToGlobal", + "description": "If true, return the global mode if the subject doesn't have a mode set.", + "in": "query", + "required": false, + "type": "boolean" + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/mode" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + }, + "put": { + "summary": "Set the mode for a subject (context-prefixed).", + "operationId": "ctx_put_mode_subject", + "consumes": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "parameters": [ + { + "name": "context", + "description": "The context to scope the operation to (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "subject", + "description": "The subject to set the mode for.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "mode", + "in": "body", + "schema": { + "$ref": "#/definitions/mode" + } + }, + { + "name": "force", + "description": "If true, override the emptiness check when setting a subject's mode to IMPORT", + "in": "query", + "required": false, + "type": "boolean" + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/mode" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + }, + "delete": { + "summary": "Delete the mode for a subject (context-prefixed).", + "operationId": "ctx_delete_mode_subject", + "parameters": [ + { + "name": "context", + "description": "The context to scope the operation to (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "subject", + "description": "The subject to delete the mode for.", + "in": "path", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/mode" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + } + }, + "/contexts/{context}/subjects": { + "get": { + "summary": "Retrieve a list of subjects in a context (context-prefixed).", + "operationId": "ctx_get_subjects", + "parameters": [ + { + "name": "context", + "description": "The context to scope the operation to (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "deleted", + "in": "query", + "required": false, + "type": "boolean" + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + } + }, + "/contexts/{context}/schemas/types": { + "get": { + "summary": "Get the supported schema types (context-prefixed).", + "operationId": "ctx_get_schemas_types", + "parameters": [ + { + "name": "context", + "description": "The context to scope the operation to (e.g., .staging). Accepted for Confluent compatibility but ignored — schema types are global.", + "in": "path", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + } + }, + "/contexts/{context}/config": { + "get": { + "summary": "Get the compatibility level for a context.", + "operationId": "ctx_get_config", + "parameters": [ + { + "name": "context", + "description": "The context to get the config for (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "defaultToGlobal", + "in": "query", + "required": false, + "type": "boolean" + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/get_compatibility" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + }, + "put": { + "summary": "Set the compatibility level for a context.", + "operationId": "ctx_put_config", + "consumes": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "parameters": [ + { + "name": "context", + "description": "The context to set the config for (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "config", + "in": "body", + "schema": { + "$ref": "#/definitions/put_compatibility" + } + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/put_compatibility" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + }, + "delete": { + "summary": "Delete the compatibility level for a context.", + "operationId": "ctx_delete_config", + "parameters": [ + { + "name": "context", + "description": "The context to delete the config for (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/get_compatibility" + } + }, + "404": { + "description": "Not Found: Subject not found", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + } + }, + "/contexts/{context}/mode": { + "get": { + "summary": "Get the mode for a context.", + "operationId": "ctx_get_mode", + "parameters": [ + { + "name": "context", + "description": "The context to get the mode for (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "defaultToGlobal", + "description": "If true, return the global mode if the context doesn't have a mode set.", + "in": "query", + "required": false, + "type": "boolean" + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/mode" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + }, + "put": { + "summary": "Set the mode for a context.", + "operationId": "ctx_put_mode", + "consumes": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "parameters": [ + { + "name": "context", + "description": "The context to set the mode for (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "mode", + "in": "body", + "schema": { + "$ref": "#/definitions/mode" + } + }, + { + "name": "force", + "description": "If true, override the emptiness check when setting the context mode to IMPORT", + "in": "query", + "required": false, + "type": "boolean" + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/mode" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + }, + "delete": { + "summary": "Delete the mode for a context.", + "operationId": "ctx_delete_mode", + "parameters": [ + { + "name": "context", + "description": "The context to delete the mode for (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/mode" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + } + }, + "/contexts/{context}/schemas/ids/{id}": { + "get": { + "summary": "Get a schema by ID (context-prefixed).", + "operationId": "ctx_get_schemas_ids_id", + "parameters": [ + { + "name": "context", + "description": "The context to scope the operation to (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "id", + "in": "path", + "required": true, + "type": "integer" + }, + { + "name": "format", + "in": "query", + "required": false, + "type": "string", + "description": "Redpanda version 25.2 or later. For Avro and Protobuf schemas only. Supported values: an empty string `''` returns the schema in its current format (default), and `serialized` (Protobuf only) returns the schema in its Base64-encoded wire binary format. Unsupported values return a 501 error." + }, + { + "name": "referenceFormat", + "in": "query", + "required": false, + "type": "string", + "description": "If set to 'qualified', schema references are returned in context-qualified form. Otherwise, unqualified references are returned." + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/schema_def_response" + } + }, + "404": { + "description": "Not Found: Schema not found", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "501": { + "description": "Not Implemented: The specified format parameter value is not supported", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + } + }, + "/contexts/{context}/schemas/ids/{id}/schema": { + "get": { + "summary": "Get the raw schema by ID (context-prefixed).", + "operationId": "ctx_get_schemas_ids_id_schema", + "description": "Retrieves only the schema identified by the input ID, without additional metadata.", + "parameters": [ + { + "name": "context", + "description": "The context to scope the operation to (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "id", + "in": "path", + "required": true, + "type": "integer" + }, + { + "name": "format", + "in": "query", + "required": false, + "type": "string", + "description": "Desired output format, dependent on schema type." + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "Raw schema string", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found: Schema not found", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + } + }, + "/contexts/{context}/schemas/ids/{id}/versions": { + "get": { + "summary": "Get a list of subject-version for the schema ID (context-prefixed).", + "operationId": "ctx_get_schemas_ids_id_versions", + "parameters": [ + { + "name": "context", + "description": "The context to scope the operation to (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "id", + "in": "path", + "required": true, + "type": "integer" + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "subject": { + "type": "string" + }, + "version": { + "type": "integer" + } + } + } + } + }, + "404": { + "description": "Not Found: Schema not found", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + } + }, + "/contexts/{context}/schemas/ids/{id}/subjects": { + "get": { + "summary": "Retrieve a list of subjects associated with some schema ID (context-prefixed).", + "operationId": "ctx_get_schemas_ids_id_subjects", + "parameters": [ + { + "name": "context", + "description": "The context to scope the operation to (e.g., .staging).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "id", + "in": "path", + "required": true, + "type": "integer" + }, + { + "name": "deleted", + "in": "query", + "required": false, + "type": "boolean" + } + ], + "produces": [ + "application/vnd.schemaregistry.v1+json", + "application/vnd.schemaregistry+json", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found: Schema not found", + "schema": { + "$ref": "#/definitions/error_body" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/error_body" + } + } + } + } + }, "/security/acls": { "get": { "summary": "List ACLs", diff --git a/src/v/pandaproxy/schema_registry/BUILD b/src/v/pandaproxy/schema_registry/BUILD index 4fa9dd2855146..ac44445fd7765 100644 --- a/src/v/pandaproxy/schema_registry/BUILD +++ b/src/v/pandaproxy/schema_registry/BUILD @@ -82,6 +82,7 @@ redpanda_cc_library( "avro.h", "compatibility.h", "configuration.h", + "context_router.h", "error.h", "errors.h", "exceptions.h", @@ -251,6 +252,7 @@ redpanda_cc_library( ":types", "//src/v/config:startup_config", "//src/v/kafka/client", + "//src/v/utils:variant", ], ) diff --git a/src/v/pandaproxy/schema_registry/auth.h b/src/v/pandaproxy/schema_registry/auth.h index 461f43a24f27f..8dfda7aba9b9e 100644 --- a/src/v/pandaproxy/schema_registry/auth.h +++ b/src/v/pandaproxy/schema_registry/auth.h @@ -15,6 +15,7 @@ #include "pandaproxy/schema_registry/types.h" #include "pandaproxy/server.h" #include "security/acl.h" +#include "utils/variant.h" #include @@ -42,10 +43,19 @@ class auth { // AuthZ is required to be performed in the handler as the resource is // unknown using deferred = named_type; + // AuthZ will be performed against the context-qualified subject extracted + // from both the {context} and {subject} path parameters + using context_prefix_subject + = named_type; using op = security::acl_operation; + /// Authorization-time resource type. using resource = std::variant; + /// Route-registration-time resource type — includes + /// `context_prefix_subject`, which is resolved to `context_subject` before + /// authorization. + using route_resource = extend_variant_t; using regular_function_handler = ss::noncopyable_function< ss::future(server::request_t, server::reply_t)>; @@ -55,14 +65,14 @@ class auth { using function_handler = std::variant; - auth(level lvl, std::optional op, resource res) + auth(level lvl, std::optional op, route_resource res) : _lvl{lvl} , _op{op} , _res{std::move(res)} {} level get_level() const { return _lvl; } std::optional get_op() const { return _op; } - const resource& get_resource() const { return _res; } + const route_resource& get_resource() const { return _res; } bool is_deferred() const { return std::holds_alternative(get_resource()); } @@ -77,7 +87,7 @@ class auth { private: level _lvl; std::optional _op; - resource _res; + route_resource _res; }; } // namespace pandaproxy::schema_registry diff --git a/src/v/pandaproxy/schema_registry/authorization.cc b/src/v/pandaproxy/schema_registry/authorization.cc index 4e1a439bfd322..baeacb64774d3 100644 --- a/src/v/pandaproxy/schema_registry/authorization.cc +++ b/src/v/pandaproxy/schema_registry/authorization.cc @@ -13,6 +13,7 @@ #include "container/chunked_hash_map.h" #include "pandaproxy/api/api-doc/schema_registry.json.hh" #include "pandaproxy/parsing/httpd.h" +#include "pandaproxy/schema_registry/context_router.h" #include "pandaproxy/schema_registry/service.h" #include "pandaproxy/schema_registry/sharded_store.h" #include "pandaproxy/schema_registry/types.h" @@ -54,15 +55,22 @@ namespace { auth::resource extract_resource_from_request(const server::request_t& rq, const auth& auth) { - auto resource = auth.get_resource(); - ss::visit( - resource, - [&rq](context_subject& ctx_sub) { - ctx_sub = context_subject::from_string( + return ss::visit( + auth.get_resource(), + [&rq](const context_subject&) -> auth::resource { + return context_subject::from_string( parse::request_param(*rq.req, "subject")); }, - [](const auto&) {}); - return resource; + [&rq](const auth::context_prefix_subject&) -> auth::resource { + auto ctx = normalize_context( + parse::request_param(*rq.req, "context")); + auto sub = parse::request_param(*rq.req, "subject"); + if (!starts_with_context(sub)) { + sub = fmt::format(":{}:{}", ctx, sub); + } + return context_subject::from_string(sub); + }, + [](const auto& res) -> auth::resource { return res; }); } void throw_unauthorized() { diff --git a/src/v/pandaproxy/schema_registry/context_router.h b/src/v/pandaproxy/schema_registry/context_router.h new file mode 100644 index 0000000000000..1111d3a12d457 --- /dev/null +++ b/src/v/pandaproxy/schema_registry/context_router.h @@ -0,0 +1,98 @@ +// Copyright 2026 Redpanda Data, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +#pragma once + +#include "base/seastarx.h" +#include "pandaproxy/schema_registry/errors.h" + +#include +#include + +#include + +#include + +namespace pandaproxy::schema_registry { + +/// \brief Normalize a context name from a URL path parameter. +inline ss::sstring normalize_context(std::string_view ctx) { + if (ctx.starts_with(':')) { + ctx.remove_prefix(1); + } + + if (ctx.ends_with(':')) { + ctx.remove_suffix(1); + } + + if (ctx.find(':') != std::string_view::npos) { + throw as_exception(context_invalid(ctx)); + } + + if (!ctx.starts_with('.')) { + return {fmt::format(".{}", ctx)}; + } + return ss::sstring(ctx); +} + +/// \brief Check if a string already has a context prefix. +inline bool starts_with_context(std::string_view s) { + return s.starts_with(":.") || s.starts_with(":*:"); +} + +/// \brief Scope the "subject" path parameter by prepending the context. +/// +/// ctx is expected to be in the form ".name" (with leading dot). The +/// resulting subject is ":.ctx:subject". +inline void +scope_subject_param(ss::http::request& req, const ss::sstring& ctx) { + auto sub = req.get_path_param("subject"); + if (!starts_with_context(sub)) { + auto nctx = normalize_context(ctx); + req.param.set( + ss::sstring("subject"), + ss::sstring(fmt::format("/:{0}:{1}", nctx, sub))); + } +} + +/// \brief Inject or prepend context into the "subject" query parameter. +inline void +scope_subject_query(ss::http::request& req, const ss::sstring& ctx) { + auto nctx = normalize_context(ctx); + auto existing = req.get_query_param("subject"); + if (existing.empty()) { + req.set_query_param("subject", fmt::format(":{0}:", nctx)); + } else if (!starts_with_context(existing)) { + req.set_query_param("subject", fmt::format(":{0}:{1}", nctx, existing)); + } +} + +/// \brief Inject or prepend context into the "subjectPrefix" query parameter. +inline void +scope_subject_prefix_query(ss::http::request& req, const ss::sstring& ctx) { + auto nctx = normalize_context(ctx); + auto existing = req.get_query_param("subjectPrefix"); + if (existing.empty()) { + req.set_query_param("subjectPrefix", fmt::format(":{0}:", nctx)); + } else if (!starts_with_context(existing)) { + req.set_query_param( + "subjectPrefix", fmt::format(":{0}:{1}", nctx, existing)); + } +} + +/// \brief Inject the context as a context-only qualified subject path +/// parameter. +inline void +inject_context_as_subject(ss::http::request& req, const ss::sstring& ctx) { + auto nctx = normalize_context(ctx); + req.param.set( + ss::sstring("subject"), ss::sstring(fmt::format("/:{0}:", nctx))); +} + +} // namespace pandaproxy::schema_registry diff --git a/src/v/pandaproxy/schema_registry/error.cc b/src/v/pandaproxy/schema_registry/error.cc index f0562f109a91d..3cbc508113224 100644 --- a/src/v/pandaproxy/schema_registry/error.cc +++ b/src/v/pandaproxy/schema_registry/error.cc @@ -96,6 +96,8 @@ struct error_category final : std::error_category { return "The specified context is not empty"; case error_code::subject_invalid: return "The specified subject is not valid"; + case error_code::context_invalid: + return "The specified context name is not valid"; } return "(unrecognized error)"; } @@ -164,6 +166,8 @@ struct error_category final : std::error_category { return reply_error_code::context_not_empty; // 42211 case error_code::subject_invalid: return reply_error_code::subject_invalid; // 42208 + case error_code::context_invalid: + return reply_error_code::bad_request; // 400 } return {}; } diff --git a/src/v/pandaproxy/schema_registry/error.h b/src/v/pandaproxy/schema_registry/error.h index f7dd7fc936e5c..5c11cb2de460f 100644 --- a/src/v/pandaproxy/schema_registry/error.h +++ b/src/v/pandaproxy/schema_registry/error.h @@ -47,6 +47,7 @@ enum class error_code { writes_disabled, context_not_empty, subject_invalid, + context_invalid, }; std::error_code make_error_code(error_code); diff --git a/src/v/pandaproxy/schema_registry/errors.h b/src/v/pandaproxy/schema_registry/errors.h index d1f7aafa6bd98..d88bac31d0985 100644 --- a/src/v/pandaproxy/schema_registry/errors.h +++ b/src/v/pandaproxy/schema_registry/errors.h @@ -261,6 +261,12 @@ inline error_info context_not_empty(const context& ctx) { fmt::format("The specified context '{}' is not empty.", ctx())}; } +inline error_info context_invalid(std::string_view ctx) { + return error_info{ + error_code::context_invalid, + fmt::format("The specified context '{}' is not valid.", ctx)}; +} + inline bool failed_subject_schema_lookup(std::error_code ec) { return ec == error_code::subject_not_found || ec == error_code::subject_version_not_found; diff --git a/src/v/pandaproxy/schema_registry/handlers.cc b/src/v/pandaproxy/schema_registry/handlers.cc index 3fc25ea8e12ac..cccfb800b22c1 100644 --- a/src/v/pandaproxy/schema_registry/handlers.cc +++ b/src/v/pandaproxy/schema_registry/handlers.cc @@ -20,6 +20,7 @@ #include "pandaproxy/logger.h" #include "pandaproxy/parsing/httpd.h" #include "pandaproxy/schema_registry/authorization.h" +#include "pandaproxy/schema_registry/context_router.h" #include "pandaproxy/schema_registry/error.h" #include "pandaproxy/schema_registry/errors.h" #include "pandaproxy/schema_registry/exceptions.h" @@ -1514,7 +1515,7 @@ delete_context(server::request_t rq, server::reply_t rp) { parse_accept_header(rq, rp); auto ctx_str = parse::request_param(*rq.req, "context"); - auto ctx = context{ctx_str}; + auto ctx = context{normalize_context(ctx_str)}; if (ctx == default_context) { throw as_exception( diff --git a/src/v/pandaproxy/schema_registry/service.cc b/src/v/pandaproxy/schema_registry/service.cc index ea60c7fb44b0a..992a07bd45199 100644 --- a/src/v/pandaproxy/schema_registry/service.cc +++ b/src/v/pandaproxy/schema_registry/service.cc @@ -27,8 +27,10 @@ #include "model/namespace.h" #include "pandaproxy/api/api-doc/schema_registry.json.hh" #include "pandaproxy/logger.h" +#include "pandaproxy/parsing/httpd.h" #include "pandaproxy/schema_registry/auth.h" #include "pandaproxy/schema_registry/configuration.h" +#include "pandaproxy/schema_registry/context_router.h" #include "pandaproxy/schema_registry/handlers.h" #include "pandaproxy/schema_registry/storage.h" #include "pandaproxy/schema_registry/types.h" @@ -128,7 +130,7 @@ server::routes_t get_schema_registry_routes(ss::gate& gate, one_shot& es) { const ss::httpd::path_description& path, auth::level lvl, std::optional op, - auth::resource res, + auth::route_resource res, auth::function_handler h) { return server::route_t{ path, @@ -366,6 +368,228 @@ server::routes_t get_schema_registry_routes(ss::gate& gate, one_shot& es) { security::default_cluster_name, delete_security_acls)); + // Context-prefixed route helpers: extract the {context} path parameter, + // apply the given URL-rewriting function, then delegate to the handler. + auto ctx_route = [](auto scope_fn, auto handler) { + return [=]( + server::request_t rq, + server::reply_t rp) -> ss::future { + auto ctx = parse::request_param(*rq.req, "context"); + scope_fn(*rq.req, ctx); + return handler(std::move(rq), std::move(rp)); + }; + }; + + auto ctx_deferred_route = [](auto scope_fn, auto handler) { + return [=]( + server::request_t rq, + server::reply_t rp, + std::optional auth_result) + -> ss::future { + auto ctx = parse::request_param(*rq.req, "context"); + scope_fn(*rq.req, ctx); + return handler( + std::move(rq), std::move(rp), std::move(auth_result)); + }; + }; + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_post_subject, + auth::level::user, + acl_operation::read, + auth::context_prefix_subject{}, + ctx_route(scope_subject_param, post_subject))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_delete_subject, + auth::level::user, + acl_operation::remove, + auth::context_prefix_subject{}, + ctx_route(scope_subject_param, delete_subject))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_get_subject_versions, + auth::level::user, + acl_operation::describe, + auth::context_prefix_subject{}, + ctx_route(scope_subject_param, get_subject_versions))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_post_subject_versions, + auth::level::user, + acl_operation::write, + auth::context_prefix_subject{}, + ctx_route(scope_subject_param, post_subject_versions))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_get_subject_versions_version, + auth::level::user, + acl_operation::read, + auth::context_prefix_subject{}, + ctx_route(scope_subject_param, get_subject_versions_version))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_get_subject_versions_version_schema, + auth::level::user, + acl_operation::read, + auth::context_prefix_subject{}, + ctx_route(scope_subject_param, get_subject_versions_version_schema))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json:: + ctx_get_subject_versions_version_referenced_by, + auth::level::user, + acl_operation::describe, + registry_resource{}, + ctx_route( + scope_subject_param, get_subject_versions_version_referenced_by))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_delete_subject_version, + auth::level::user, + acl_operation::remove, + auth::context_prefix_subject{}, + ctx_route(scope_subject_param, delete_subject_version))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_compatibility_subject_version, + auth::level::user, + acl_operation::read, + auth::context_prefix_subject{}, + ctx_route(scope_subject_param, compatibility_subject_version))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_get_config_subject, + auth::level::user, + std::nullopt, + auth::deferred{}, + ctx_deferred_route(scope_subject_param, get_config_subject))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_put_config_subject, + auth::level::user, + std::nullopt, + auth::deferred{}, + ctx_deferred_route(scope_subject_param, put_config_subject))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_delete_config_subject, + auth::level::user, + std::nullopt, + auth::deferred{}, + ctx_deferred_route(scope_subject_param, delete_config_subject))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_get_mode_subject, + auth::level::user, + std::nullopt, + auth::deferred{}, + ctx_deferred_route(scope_subject_param, get_mode_subject))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_put_mode_subject, + auth::level::superuser, + std::nullopt, + auth::deferred{}, + ctx_deferred_route(scope_subject_param, put_mode_subject))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_delete_mode_subject, + auth::level::superuser, + std::nullopt, + auth::deferred{}, + ctx_deferred_route(scope_subject_param, delete_mode_subject))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_get_schemas_ids_id, + auth::level::user, + std::nullopt, + auth::deferred{}, + ctx_deferred_route(scope_subject_query, get_schemas_ids_id))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_get_schemas_ids_id_schema, + auth::level::user, + std::nullopt, + auth::deferred{}, + ctx_deferred_route(scope_subject_query, get_schemas_ids_id_schema))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_get_schemas_ids_id_versions, + auth::level::user, + acl_operation::describe, + registry_resource{}, + ctx_route(scope_subject_query, get_schemas_ids_id_versions))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_get_schemas_ids_id_subjects, + auth::level::user, + acl_operation::describe, + registry_resource{}, + ctx_route(scope_subject_query, get_schemas_ids_id_subjects))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_get_subjects, + auth::level::user, + std::nullopt, + auth::deferred{}, + ctx_deferred_route(scope_subject_prefix_query, get_subjects))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_get_config, + auth::level::user, + std::nullopt, + auth::deferred{}, + ctx_deferred_route(inject_context_as_subject, get_config_subject))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_put_config, + auth::level::user, + std::nullopt, + auth::deferred{}, + ctx_deferred_route(inject_context_as_subject, put_config_subject))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_get_mode, + auth::level::user, + std::nullopt, + auth::deferred{}, + ctx_deferred_route(inject_context_as_subject, get_mode_subject))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_put_mode, + auth::level::superuser, + std::nullopt, + auth::deferred{}, + ctx_deferred_route(inject_context_as_subject, put_mode_subject))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_get_schemas_types, + auth::level::publik, + acl_operation::read, + auth::none{}, + // Schema types are global — the handler ignores the context. Validate + // it anyway for consistency with other /contexts/{context}/... routes. + ctx_route( + [](ss::http::request&, const ss::sstring& ctx) { + normalize_context(ctx); + }, + get_schemas_types))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_delete_config, + auth::level::user, + std::nullopt, + auth::deferred{}, + ctx_deferred_route(inject_context_as_subject, delete_config_subject))); + + routes.routes.emplace_back(wrap( + ss::httpd::schema_registry_json::ctx_delete_mode, + auth::level::superuser, + std::nullopt, + auth::deferred{}, + ctx_deferred_route(inject_context_as_subject, delete_mode_subject))); + return routes; } diff --git a/src/v/pandaproxy/schema_registry/test/BUILD b/src/v/pandaproxy/schema_registry/test/BUILD index 77aee20a089b2..0a1228e952237 100644 --- a/src/v/pandaproxy/schema_registry/test/BUILD +++ b/src/v/pandaproxy/schema_registry/test/BUILD @@ -348,6 +348,21 @@ redpanda_cc_gtest( ], ) +redpanda_cc_gtest( + name = "context_router_test", + timeout = "short", + srcs = [ + "context_router.cc", + ], + deps = [ + "//src/v/pandaproxy/schema_registry:core", + "//src/v/test_utils:gtest", + "@fmt", + "@googletest//:gtest", + "@seastar", + ], +) + redpanda_cc_btest( name = "test_json_schema", timeout = "short", diff --git a/src/v/pandaproxy/schema_registry/test/context_router.cc b/src/v/pandaproxy/schema_registry/test/context_router.cc new file mode 100644 index 0000000000000..d64c36d922b2d --- /dev/null +++ b/src/v/pandaproxy/schema_registry/test/context_router.cc @@ -0,0 +1,160 @@ +// Copyright 2026 Redpanda Data, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +#include "pandaproxy/schema_registry/context_router.h" + +#include "pandaproxy/schema_registry/exceptions.h" + +#include + +#include + +namespace pandaproxy::schema_registry { + +TEST(ContextRouterTest, StartsWithContextDefault) { + EXPECT_FALSE(starts_with_context("")); + EXPECT_FALSE(starts_with_context("my-topic")); + EXPECT_FALSE(starts_with_context("plain-subject")); +} + +TEST(ContextRouterTest, StartsWithContextQualified) { + EXPECT_TRUE(starts_with_context(":.staging:my-topic")); + EXPECT_TRUE(starts_with_context(":.prod:")); + EXPECT_TRUE(starts_with_context(":.:my-topic")); +} + +TEST(ContextRouterTest, StartsWithContextWildcard) { + EXPECT_TRUE(starts_with_context(":*:")); + EXPECT_TRUE(starts_with_context(":*:my-topic")); +} + +TEST(ContextRouterTest, StartsWithContextEdgeCases) { + EXPECT_FALSE(starts_with_context(":")); + EXPECT_FALSE(starts_with_context(":foo")); + EXPECT_FALSE(starts_with_context(":*")); +} + +TEST(ContextRouterTest, NormalizeContextWithDot) { + EXPECT_EQ(normalize_context(".staging"), ".staging"); + EXPECT_EQ(normalize_context(".prod"), ".prod"); + EXPECT_EQ(normalize_context("."), "."); +} + +TEST(ContextRouterTest, NormalizeContextWithoutDot) { + EXPECT_EQ(normalize_context("staging"), ".staging"); + EXPECT_EQ(normalize_context("prod"), ".prod"); + EXPECT_EQ(normalize_context(""), "."); +} + +TEST(ContextRouterTest, NormalizeContextStripColons) { + EXPECT_EQ(normalize_context(":.staging"), ".staging"); + EXPECT_EQ(normalize_context(".staging:"), ".staging"); + EXPECT_EQ(normalize_context(":.staging:"), ".staging"); + EXPECT_EQ(normalize_context(":staging:"), ".staging"); +} + +TEST(ContextRouterTest, NormalizeContextRejectsEmbeddedColons) { + EXPECT_THROW(normalize_context(".:."), exception); + EXPECT_THROW(normalize_context("a:b"), exception); + EXPECT_THROW(normalize_context(":.a:b:"), exception); +} + +TEST(ContextRouterTest, ScopeSubjectParam) { + ss::http::request req; + req.param.set("subject", "/my-topic"); + scope_subject_param(req, ".staging"); + EXPECT_EQ(req.get_path_param("subject"), ":.staging:my-topic"); +} + +TEST(ContextRouterTest, ScopeSubjectParamAlreadyQualified) { + ss::http::request req; + req.param.set("subject", "/:.prod:my-topic"); + scope_subject_param(req, ".staging"); + EXPECT_EQ(req.get_path_param("subject"), ":.prod:my-topic"); +} + +TEST(ContextRouterTest, ScopeSubjectParamWildcard) { + ss::http::request req; + req.param.set("subject", "/:*:my-topic"); + scope_subject_param(req, ".staging"); + EXPECT_EQ(req.get_path_param("subject"), ":*:my-topic"); +} + +TEST(ContextRouterTest, ScopeSubjectParamNoDot) { + ss::http::request req; + req.param.set("subject", "/my-topic"); + scope_subject_param(req, "staging"); + EXPECT_EQ(req.get_path_param("subject"), ":.staging:my-topic"); +} + +TEST(ContextRouterTest, ScopeSubjectQueryAbsent) { + ss::http::request req; + scope_subject_query(req, ".staging"); + EXPECT_EQ(req.get_query_param("subject"), ":.staging:"); +} + +TEST(ContextRouterTest, ScopeSubjectQueryPlain) { + ss::http::request req; + req.set_query_param("subject", "my-topic"); + scope_subject_query(req, ".staging"); + EXPECT_EQ(req.get_query_param("subject"), ":.staging:my-topic"); +} + +TEST(ContextRouterTest, ScopeSubjectQueryAlreadyQualified) { + ss::http::request req; + req.set_query_param("subject", ":.prod:my-topic"); + scope_subject_query(req, ".staging"); + EXPECT_EQ(req.get_query_param("subject"), ":.prod:my-topic"); +} + +TEST(ContextRouterTest, ScopeSubjectQueryNoDot) { + ss::http::request req; + scope_subject_query(req, "staging"); + EXPECT_EQ(req.get_query_param("subject"), ":.staging:"); +} + +TEST(ContextRouterTest, ScopeSubjectPrefixQueryAbsent) { + ss::http::request req; + scope_subject_prefix_query(req, ".staging"); + EXPECT_EQ(req.get_query_param("subjectPrefix"), ":.staging:"); +} + +TEST(ContextRouterTest, ScopeSubjectPrefixQueryPlain) { + ss::http::request req; + req.set_query_param("subjectPrefix", "my-"); + scope_subject_prefix_query(req, ".staging"); + EXPECT_EQ(req.get_query_param("subjectPrefix"), ":.staging:my-"); +} + +TEST(ContextRouterTest, ScopeSubjectPrefixQueryAlreadyQualified) { + ss::http::request req; + req.set_query_param("subjectPrefix", ":.prod:"); + scope_subject_prefix_query(req, ".staging"); + EXPECT_EQ(req.get_query_param("subjectPrefix"), ":.prod:"); +} + +TEST(ContextRouterTest, ScopeSubjectPrefixQueryNoDot) { + ss::http::request req; + scope_subject_prefix_query(req, "staging"); + EXPECT_EQ(req.get_query_param("subjectPrefix"), ":.staging:"); +} + +TEST(ContextRouterTest, InjectContextAsSubject) { + ss::http::request req; + inject_context_as_subject(req, ".staging"); + EXPECT_EQ(req.get_path_param("subject"), ":.staging:"); +} + +TEST(ContextRouterTest, InjectContextAsSubjectNoDot) { + ss::http::request req; + inject_context_as_subject(req, "staging"); + EXPECT_EQ(req.get_path_param("subject"), ":.staging:"); +} + +} // namespace pandaproxy::schema_registry diff --git a/src/v/utils/tests/BUILD b/src/v/utils/tests/BUILD index 6c08f0d347c3b..4b7439ba31bcc 100644 --- a/src/v/utils/tests/BUILD +++ b/src/v/utils/tests/BUILD @@ -628,3 +628,16 @@ redpanda_cc_gtest( "@googletest//:gtest", ], ) + +redpanda_cc_gtest( + name = "variant_test", + timeout = "short", + srcs = [ + "variant_test.cc", + ], + deps = [ + "//src/v/test_utils:gtest", + "//src/v/utils:variant", + "@googletest//:gtest", + ], +) diff --git a/src/v/utils/tests/variant_test.cc b/src/v/utils/tests/variant_test.cc new file mode 100644 index 0000000000000..d5a0f123e6327 --- /dev/null +++ b/src/v/utils/tests/variant_test.cc @@ -0,0 +1,30 @@ +// Copyright 2026 Redpanda Data, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +#include "utils/variant.h" + +#include + +#include +#include +#include + +static_assert(std::is_same_v< + extend_variant_t, bool>, + std::variant>); + +static_assert(std::is_same_v< + extend_variant_t, double, std::string>, + std::variant>); + +static_assert(std::is_same_v< + extend_variant_t>, + std::variant>); + +TEST(VariantTest, CompileOnly) {} diff --git a/src/v/utils/variant.h b/src/v/utils/variant.h index bc5e1dc3176a4..e177fa46e9519 100644 --- a/src/v/utils/variant.h +++ b/src/v/utils/variant.h @@ -35,3 +35,19 @@ using variant_of_identities = decltype(util::detail::variant_of_tags_impl( template using tuple_of_identities = decltype(util::detail::tuple_of_tags_impl( std::type_identity{})); + +namespace util::detail { + +template +struct extend_variant; + +template +struct extend_variant, Extra...> { + using type = std::variant; +}; + +} // namespace util::detail + +template +using extend_variant_t = + typename util::detail::extend_variant::type; diff --git a/tests/go/go-kafka-serde/go.mod b/tests/go/go-kafka-serde/go.mod index ed7bc7558bee2..3b04746484212 100644 --- a/tests/go/go-kafka-serde/go.mod +++ b/tests/go/go-kafka-serde/go.mod @@ -3,20 +3,20 @@ module com.redpanda/go-kafka-serde go 1.19 require ( - github.com/confluentinc/confluent-kafka-go/v2 v2.0.2 + github.com/confluentinc/confluent-kafka-go/v2 v2.3.0 github.com/google/uuid v1.3.0 github.com/redpanda-data/kgo-verifier v0.0.0-20230314144634-a2b6c6ddb0aa - github.com/sirupsen/logrus v1.9.0 + github.com/sirupsen/logrus v1.9.3 golang.org/x/exp v0.0.0-20230321023759-10a507213a29 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - google.golang.org/protobuf v1.30.0 + google.golang.org/protobuf v1.33.0 ) require ( github.com/actgardner/gogen-avro/v10 v10.2.1 // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/heetch/avro v0.3.1 // indirect - github.com/jhump/protoreflect v1.12.0 // indirect - golang.org/x/sys v0.1.0 // indirect - google.golang.org/genproto v0.0.0-20220503193339-ba3ae3f07e29 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/heetch/avro v0.4.4 // indirect + github.com/jhump/protoreflect v1.14.1 // indirect + golang.org/x/sys v0.6.0 // indirect + google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633 // indirect ) diff --git a/tests/go/go-kafka-serde/go.sum b/tests/go/go-kafka-serde/go.sum index 3c7e119bade58..a612f144aa82f 100644 --- a/tests/go/go-kafka-serde/go.sum +++ b/tests/go/go-kafka-serde/go.sum @@ -1,47 +1,41 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/actgardner/gogen-avro/v10 v10.1.0/go.mod h1:o+ybmVjEa27AAr35FRqU98DJu1fXES56uXniYFv4yDA= +github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Microsoft/hcsshim v0.9.4 h1:mnUj0ivWy6UzbB1uLFqKR6F+ZyiDc7j4iGgHTpO+5+I= github.com/actgardner/gogen-avro/v10 v10.2.1 h1:z3pOGblRjAJCYpkIJ8CmbMJdksi4rAhaygw0dyXZ930= github.com/actgardner/gogen-avro/v10 v10.2.1/go.mod h1:QUhjeHPchheYmMDni/Nx7VB0RsT/ee8YIgGY/xpEQgQ= -github.com/actgardner/gogen-avro/v9 v9.1.0/go.mod h1:nyTj6wPqDJoxM3qdnjcLv+EnMDSDFqE0qDpva2QRmKc= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/confluentinc/confluent-kafka-go/v2 v2.0.2 h1:YmUjjRp1mSTqTxtHQYMQKBLa2hfgIZz9PSqoSRDkwf4= -github.com/confluentinc/confluent-kafka-go/v2 v2.0.2/go.mod h1:qWGwym8EpAsIP5lZsTKhYTnYSGqkbxEfRB4A489Jo64= +github.com/confluentinc/confluent-kafka-go/v2 v2.3.0 h1:icCHutJouWlQREayFwCc7lxDAhws08td+W3/gdqgZts= +github.com/confluentinc/confluent-kafka-go/v2 v2.3.0/go.mod h1:/VTy8iEpe6mD9pkCH5BhijlUl8ulUXymKv1Qig5Rgb8= +github.com/containerd/cgroups v1.0.4 h1:jN/mbWBEaz+T1pi5OFtnkQ+8qnmEbAr1Oo1FRm5B0dA= +github.com/containerd/containerd v1.6.8 h1:h4dOFDwzHmqFEP754PgfgTeVXFnLiRc6kiqC7tplDJs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= +github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -49,13 +43,10 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -66,25 +57,17 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20211008130755-947d60d73cc0/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hamba/avro v1.5.6/go.mod h1:3vNT0RLXXpFm2Tb/5KC71ZRJlOroggq1Rcitb6k4Fr8= -github.com/heetch/avro v0.3.1 h1:i6DyUBDIwzt6Fs78dYBIXYd5XrYUs/ir4+39WbHQhJE= -github.com/heetch/avro v0.3.1/go.mod h1:4xn38Oz/+hiEUTpbVfGVLfvOg0yKLlRP7Q9+gJJILgA= -github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= -github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= -github.com/invopop/jsonschema v0.4.0/go.mod h1:O9uiLokuu0+MGFlyiaqtWxwqJm41/+8Nj0lD7A36YH0= +github.com/heetch/avro v0.4.4 h1:5PmgDy1cX/MegMy6btJ4bUFHgT5GLfSYfc5U7+JUQzg= +github.com/heetch/avro v0.4.4/go.mod h1:c0whqijPh/C+RwnXzAHFit01tdtf7gMeEHYSbICxJjU= github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ= github.com/jhump/protoreflect v1.11.0/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E= -github.com/jhump/protoreflect v1.12.0 h1:1NQ4FpWMgn3by/n1X0fbeKEUxP1wBt7+Oitpv01HR10= -github.com/jhump/protoreflect v1.12.0/go.mod h1:JytZfP5d0r8pVNLZvai7U/MCuTWITgrI4tTg7puQFKI= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jhump/protoreflect v1.14.1 h1:N88q7JkxTHWFEqReuTsYH1dPIwXxA0ITNQp7avLY10s= +github.com/jhump/protoreflect v1.14.1/go.mod h1:JytZfP5d0r8pVNLZvai7U/MCuTWITgrI4tTg7puQFKI= github.com/juju/qthttptest v0.1.1/go.mod h1:aTlAv8TYaflIiTDIQYzxnl1QdPjAg8Q8qJMErpKy6A4= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -95,38 +78,36 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/linkedin/goavro v2.1.0+incompatible/go.mod h1:bBCwI2eGYpUI/4820s67MElg9tdeLbINjLjiM2xZFYM= -github.com/linkedin/goavro/v2 v2.10.0/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA= -github.com/linkedin/goavro/v2 v2.10.1/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA= github.com/linkedin/goavro/v2 v2.11.1/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/nrwiersma/avro-benchmarks v0.0.0-20210913175520-21aec48c8f76/go.mod h1:iKyFMidsk/sVYONJRE372sJuX/QTRPacU7imPqqsu7g= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/moby/sys/mount v0.3.3 h1:fX1SVkXFJ47XWDoeFW4Sq7PdQJnV2QIDZAqjNqgEjUs= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= +github.com/opencontainers/runc v1.1.3 h1:vIXrkId+0/J2Ymu2m7VjGvbSlAId9XNRPhn2p4b+d8w= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/redpanda-data/kgo-verifier v0.0.0-20230314144634-a2b6c6ddb0aa h1:fZv0iaZdp+h9vV5hZ6Q8+FcdtAbvIzEol7qtlEqCuy8= github.com/redpanda-data/kgo-verifier v0.0.0-20230314144634-a2b6c6ddb0aa/go.mod h1:y+uJpvjaT3ClodoBpqSTe/zHTcF1nUutyeMEQQyjTNc= github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a/go.mod h1:4r5QyqhjIWCcK8DO4KMclc5Iknq5qVBAlbYYzAbUScQ= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/testcontainers/testcontainers-go v0.14.0 h1:h0D5GaYG9mhOWr2qHdEKDXpkce/VlvaYOCzTRi6UBi8= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -139,7 +120,6 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -147,15 +127,10 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= @@ -164,21 +139,12 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -189,24 +155,19 @@ golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roY golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20220503193339-ba3ae3f07e29 h1:DJUvgAPiJWeMBiT+RzBVcJGQN7bAEWS5UEoMshES9xs= -google.golang.org/genproto v0.0.0-20220503193339-ba3ae3f07e29/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633 h1:0BOZf6qNozI3pkN3fJLwNubheHJYHhMh91GRFOWWK08= +google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -218,11 +179,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/avro.v0 v0.0.0-20171217001914-a730b5802183/go.mod h1:FvqrFXt+jCsyQibeRv4xxEJBL5iG2DDW5aeJwzDiq4A= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -232,11 +190,10 @@ gopkg.in/httprequest.v1 v1.2.1/go.mod h1:x2Otw96yda5+8+6ZeWwHIJTFkEHWP/qP8pJOzqE gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/retry.v1 v1.0.3/go.mod h1:FJkXmWiMaAo7xB+xhvDF59zhfjDWyzmyAxiT4dB688g= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/tests/rptest/tests/schema_registry_test.py b/tests/rptest/tests/schema_registry_test.py index 3a50c3cedcd43..612f9d94db63f 100644 --- a/tests/rptest/tests/schema_registry_test.py +++ b/tests/rptest/tests/schema_registry_test.py @@ -1393,8 +1393,9 @@ def _get_serde_client( compression_type: Optional[TopicSpec.CompressionTypes] = None, context_name_strategy: Optional[str] = None, context_name: Optional[str] = None, + schema_registry_url: Optional[str] = None, ): - schema_reg = self.redpanda.schema_reg().split(",", 1)[0] + schema_reg = schema_registry_url or self.redpanda.schema_reg().split(",", 1)[0] sec_cfg = self.redpanda.kafka_client_security().to_dict() return SerdeClient( @@ -5666,6 +5667,34 @@ def test_context_list_delete(self): result = self.sr_client.delete_context(".nonexistent") self.assert_equal(result.status_code, 404) + # Test context alias normalization for delete: + # All alias forms for the same context should resolve and delete it. + alias_ctx = ".alias-ctx" + alias_subject = f":{alias_ctx}:alias-sub" + for delete_alias in ["alias-ctx", ":.alias-ctx:", ".alias-ctx"]: + result = self.sr_client.post_subjects_subject_versions( + subject=alias_subject, + data=json.dumps({"schema": schema1_def}), + ) + self.assert_equal(result.status_code, requests.codes.ok) + result = self.sr_client.delete_subject(subject=alias_subject) + self.assert_equal(result.status_code, requests.codes.ok) + result = self.sr_client.delete_subject( + subject=alias_subject, permanent=True + ) + self.assert_equal(result.status_code, requests.codes.ok) + + result = self.sr_client.delete_context(delete_alias) + self.assert_equal( + result.status_code, + 204, + f"delete_context({delete_alias!r}) should succeed", + ) + + # Verify default context rejection works with alias form ":.:" + result = self.sr_client.delete_context(":.:") + self.assert_equal(result.status_code, 422) + @cluster(num_nodes=1) def test_get_schema_by_id_with_subject(self): """Test GET /schemas/ids/{id} with subject query parameter for context lookup.""" @@ -6853,6 +6882,503 @@ def test_context_name_strategy(self): f"Expected subject {expected_subject} not found in {subjects}" ) + @cluster(num_nodes=3) + def test_context_prefix_subject_operations(self): + """ + Verify all context-prefixed /contexts/{context}/... routes that + contain a {subject} path parameter. Each route should scope the + subject with the context and delegate to the existing handler. + """ + subject = "ctx-prefix-test" + ctx = ".staging" + schema_data = json.dumps({"schema": schema1_def}) + compat_schema_data = json.dumps({"schema": schema2_def}) + + # Register a schema via context-prefixed POST versions + result = self.sr_client.request( + "POST", + f"contexts/{ctx}/subjects/{subject}/versions", + headers=HTTP_POST_HEADERS, + data=schema_data, + ) + assert result.status_code == requests.codes.ok, ( + f"POST versions failed: {result.text}" + ) + schema_id = result.json()["id"] + assert schema_id == 1 + + # GET versions via context prefix + result = self.sr_client.request( + "GET", + f"contexts/{ctx}/subjects/{subject}/versions", + headers=HTTP_GET_HEADERS, + ) + assert result.status_code == requests.codes.ok + assert result.json() == [1] + + # GET specific version via context prefix + result = self.sr_client.request( + "GET", + f"contexts/{ctx}/subjects/{subject}/versions/1", + headers=HTTP_GET_HEADERS, + ) + assert result.status_code == requests.codes.ok + assert result.json()["version"] == 1 + + # GET version schema via context prefix + result = self.sr_client.request( + "GET", + f"contexts/{ctx}/subjects/{subject}/versions/1/schema", + headers=HTTP_GET_HEADERS, + ) + assert result.status_code == requests.codes.ok + + # GET referencedby via context prefix (empty, no references) + result = self.sr_client.request( + "GET", + f"contexts/{ctx}/subjects/{subject}/versions/1/referencedby", + headers=HTTP_GET_HEADERS, + ) + assert result.status_code == requests.codes.ok + assert result.json() == [] + + # POST lookup (post_subject) via context prefix + result = self.sr_client.request( + "POST", + f"contexts/{ctx}/subjects/{subject}", + headers=HTTP_POST_HEADERS, + data=schema_data, + ) + assert result.status_code == requests.codes.ok + assert result.json()["id"] == schema_id + + # Compatibility check via context prefix + result = self.sr_client.request( + "POST", + f"contexts/{ctx}/compatibility/subjects/{subject}/versions/1", + headers=HTTP_POST_HEADERS, + data=compat_schema_data, + ) + assert result.status_code == requests.codes.ok + assert result.json()["is_compatible"] is True + + # PUT config/{subject} via context prefix + result = self.sr_client.request( + "PUT", + f"contexts/{ctx}/config/{subject}", + headers=HTTP_POST_HEADERS, + data=json.dumps({"compatibility": "FULL"}), + ) + assert result.status_code == requests.codes.ok + + # GET config/{subject} via context prefix + result = self.sr_client.request( + "GET", + f"contexts/{ctx}/config/{subject}", + headers=HTTP_GET_HEADERS, + ) + assert result.status_code == requests.codes.ok + assert result.json()["compatibilityLevel"] == "FULL" + + # DELETE config/{subject} via context prefix + result = self.sr_client.request( + "DELETE", + f"contexts/{ctx}/config/{subject}", + headers=HTTP_DELETE_HEADERS, + ) + assert result.status_code == requests.codes.ok + + # PUT mode/{subject} via context prefix + result = self.sr_client.request( + "PUT", + f"contexts/{ctx}/mode/{subject}", + headers=HTTP_POST_HEADERS, + data=json.dumps({"mode": "READONLY"}), + ) + assert result.status_code == requests.codes.ok + + # GET mode/{subject} via context prefix + result = self.sr_client.request( + "GET", + f"contexts/{ctx}/mode/{subject}", + headers=HTTP_GET_HEADERS, + ) + assert result.status_code == requests.codes.ok + assert result.json()["mode"] == "READONLY" + + # DELETE mode/{subject} via context prefix + result = self.sr_client.request( + "DELETE", + f"contexts/{ctx}/mode/{subject}", + headers=HTTP_DELETE_HEADERS, + ) + assert result.status_code == requests.codes.ok + + # Verify isolation: default context should NOT see the subject + result = self.sr_client.get_subjects() + assert result.status_code == requests.codes.ok + assert subject not in result.json(), ( + f"Subject {subject} should not be visible in default context" + ) + + # DELETE version via context prefix + result = self.sr_client.request( + "DELETE", + f"contexts/{ctx}/subjects/{subject}/versions/1", + headers=HTTP_DELETE_HEADERS, + ) + assert result.status_code == requests.codes.ok + + # DELETE subject via context prefix + result = self.sr_client.request( + "DELETE", + f"contexts/{ctx}/subjects/{subject}?permanent=true", + headers=HTTP_DELETE_HEADERS, + ) + assert result.status_code == requests.codes.ok + + # Invalid context name (embedded colon) returns 400 + result = self.sr_client.request( + "GET", + "contexts/a:b/subjects", + headers=HTTP_GET_HEADERS, + ) + assert result.status_code == requests.codes.bad_request, ( + f"Expected 400 for invalid context name, got {result.status_code}" + ) + + @cluster(num_nodes=3) + def test_context_prefix_schema_by_id(self): + """ + Verify the context-prefixed /contexts/{context}/schemas/ids/{id} and + sub-resource routes. The wrapper injects the context as a subject + query parameter, scoping schema lookups to the specified context. + """ + subject = "ctx-schema-id-test" + ctx = ".staging" + schema_data = json.dumps({"schema": schema1_def}) + + # Register a schema in the .staging context via the direct API so we + # have an ID to look up. + result = self.sr_client.request( + "POST", + f"contexts/{ctx}/subjects/{subject}/versions", + headers=HTTP_POST_HEADERS, + data=schema_data, + ) + assert result.status_code == requests.codes.ok, ( + f"POST versions failed: {result.text}" + ) + schema_id = result.json()["id"] + + # GET /contexts/{ctx}/schemas/ids/{id} + result = self.sr_client.request( + "GET", + f"contexts/{ctx}/schemas/ids/{schema_id}", + headers=HTTP_GET_HEADERS, + ) + assert result.status_code == requests.codes.ok, ( + f"GET schemas/ids/{schema_id} failed: {result.text}" + ) + assert "schema" in result.json() + + # GET /contexts/{ctx}/schemas/ids/{id}/schema + result = self.sr_client.request( + "GET", + f"contexts/{ctx}/schemas/ids/{schema_id}/schema", + headers=HTTP_GET_HEADERS, + ) + assert result.status_code == requests.codes.ok, ( + f"GET schemas/ids/{schema_id}/schema failed: {result.text}" + ) + + # GET /contexts/{ctx}/schemas/ids/{id}/versions + result = self.sr_client.request( + "GET", + f"contexts/{ctx}/schemas/ids/{schema_id}/versions", + headers=HTTP_GET_HEADERS, + ) + assert result.status_code == requests.codes.ok, ( + f"GET schemas/ids/{schema_id}/versions failed: {result.text}" + ) + versions = result.json() + assert len(versions) >= 1 + qualified_subject = f":.{ctx.lstrip('.')}:{subject}" + subjects_in_versions = [v["subject"] for v in versions] + assert qualified_subject in subjects_in_versions, ( + f"Expected {qualified_subject} in versions {subjects_in_versions}" + ) + + # GET /contexts/{ctx}/schemas/ids/{id}/subjects + result = self.sr_client.request( + "GET", + f"contexts/{ctx}/schemas/ids/{schema_id}/subjects", + headers=HTTP_GET_HEADERS, + ) + assert result.status_code == requests.codes.ok, ( + f"GET schemas/ids/{schema_id}/subjects failed: {result.text}" + ) + subjects = result.json() + assert qualified_subject in subjects, ( + f"Expected {qualified_subject} in subjects {subjects}" + ) + + @cluster(num_nodes=3) + def test_context_prefix_subject_listing(self): + """ + Verify the context-prefixed GET /contexts/{context}/subjects route. + The wrapper injects the context into the subjectPrefix query parameter, + scoping the subject listing to the specified context. + """ + ctx = ".listing" + schema_data = json.dumps({"schema": schema1_def}) + + # Register two subjects in the .listing context + for subj in ("topic-a", "topic-b"): + result = self.sr_client.request( + "POST", + f"contexts/{ctx}/subjects/{subj}/versions", + headers=HTTP_POST_HEADERS, + data=schema_data, + ) + assert result.status_code == requests.codes.ok, ( + f"POST {subj} failed: {result.text}" + ) + + # Register a subject in the default context + result = self.sr_client.request( + "POST", + "subjects/default-only-subject/versions", + headers=HTTP_POST_HEADERS, + data=schema_data, + ) + assert result.status_code == requests.codes.ok, ( + f"POST default subject failed: {result.text}" + ) + + # GET /contexts/{ctx}/subjects — only context subjects should appear + result = self.sr_client.request( + "GET", + f"contexts/{ctx}/subjects", + headers=HTTP_GET_HEADERS, + ) + assert result.status_code == requests.codes.ok, ( + f"GET contexts/{ctx}/subjects failed: {result.text}" + ) + listed = result.json() + assert "default-only-subject" not in listed, ( + f"Default-context subject should not appear: {listed}" + ) + ctx_name = ctx.lstrip(".") + for subj in ("topic-a", "topic-b"): + qualified = f":.{ctx_name}:{subj}" + assert qualified in listed, f"Expected {qualified} in {listed}" + + @cluster(num_nodes=3) + def test_context_prefix_config_and_mode(self): + """ + Verify GET/PUT /contexts/{context}/config and /contexts/{context}/mode. + The wrapper injects the context as a context-only qualified subject + (e.g., ':.cfgmode:') and delegates to the existing config/mode + subject handlers. + """ + ctx = ".cfgmode" + schema_data = json.dumps({"schema": schema1_def}) + + # Materialize the context by registering a schema + result = self.sr_client.request( + "POST", + f"contexts/{ctx}/subjects/cfg-subject/versions", + headers=HTTP_POST_HEADERS, + data=schema_data, + ) + assert result.status_code == requests.codes.ok, ( + f"POST schema failed: {result.text}" + ) + + # PUT /contexts/{ctx}/config + result = self.sr_client.request( + "PUT", + f"contexts/{ctx}/config", + headers=HTTP_POST_HEADERS, + data=json.dumps({"compatibility": "FULL"}), + ) + assert result.status_code == requests.codes.ok, ( + f"PUT config failed: {result.text}" + ) + + # GET /contexts/{ctx}/config + result = self.sr_client.request( + "GET", + f"contexts/{ctx}/config", + headers=HTTP_GET_HEADERS, + ) + assert result.status_code == requests.codes.ok, ( + f"GET config failed: {result.text}" + ) + assert result.json()["compatibilityLevel"] == "FULL", ( + f"Unexpected config response: {result.json()}" + ) + + # PUT /contexts/{ctx}/mode + result = self.sr_client.request( + "PUT", + f"contexts/{ctx}/mode", + headers=HTTP_POST_HEADERS, + data=json.dumps({"mode": "READONLY"}), + ) + assert result.status_code == requests.codes.ok, ( + f"PUT mode failed: {result.text}" + ) + + # GET /contexts/{ctx}/mode + result = self.sr_client.request( + "GET", + f"contexts/{ctx}/mode", + headers=HTTP_GET_HEADERS, + ) + assert result.status_code == requests.codes.ok, ( + f"GET mode failed: {result.text}" + ) + assert result.json()["mode"] == "READONLY", ( + f"Unexpected mode response: {result.json()}" + ) + + @cluster(num_nodes=3) + def test_context_prefix_schema_types(self): + """ + Verify GET /contexts/{context}/schemas/types passes through to the + global schema-types handler. The context is accepted for Confluent + compatibility but ignored. + """ + result = self.sr_client.request( + "GET", + "contexts/.staging/schemas/types", + headers=HTTP_GET_HEADERS, + ) + assert result.status_code == requests.codes.ok, ( + f"GET schemas/types failed: {result.text}" + ) + types = result.json() + assert "AVRO" in types, f"Expected AVRO in schema types: {types}" + + # Invalid context name (embedded colon) returns 400 + result = self.sr_client.request( + "GET", + "contexts/a:b/schemas/types", + headers=HTTP_GET_HEADERS, + ) + assert result.status_code == requests.codes.bad_request, ( + f"Expected 400 for invalid context name, got {result.status_code}" + ) + + @cluster(num_nodes=3) + def test_context_prefix_delete_config_and_mode(self): + """ + Verify DELETE /contexts/{context}/config and /contexts/{context}/mode. + The wrapper injects the context as a context-only qualified subject + and delegates to the existing delete_config_subject and + delete_mode_subject handlers. + """ + ctx = ".delcfg" + schema_data = json.dumps({"schema": schema1_def}) + + # Materialize the context + result = self.sr_client.request( + "POST", + f"contexts/{ctx}/subjects/del-subject/versions", + headers=HTTP_POST_HEADERS, + data=schema_data, + ) + assert result.status_code == requests.codes.ok, ( + f"POST schema failed: {result.text}" + ) + + # Set config, then DELETE it + result = self.sr_client.request( + "PUT", + f"contexts/{ctx}/config", + headers=HTTP_POST_HEADERS, + data=json.dumps({"compatibility": "FULL"}), + ) + assert result.status_code == requests.codes.ok, ( + f"PUT config failed: {result.text}" + ) + result = self.sr_client.request( + "DELETE", + f"contexts/{ctx}/config", + headers=HTTP_DELETE_HEADERS, + ) + assert result.status_code == requests.codes.ok, ( + f"DELETE config failed: {result.text}" + ) + + # Set mode, then DELETE it + result = self.sr_client.request( + "PUT", + f"contexts/{ctx}/mode", + headers=HTTP_POST_HEADERS, + data=json.dumps({"mode": "READONLY"}), + ) + assert result.status_code == requests.codes.ok, ( + f"PUT mode failed: {result.text}" + ) + result = self.sr_client.request( + "DELETE", + f"contexts/{ctx}/mode", + headers=HTTP_DELETE_HEADERS, + ) + assert result.status_code == requests.codes.ok, ( + f"DELETE mode failed: {result.text}" + ) + + @cluster(num_nodes=4) + @parametrize(client_type=SerdeClientType.Python) + @parametrize(client_type=SerdeClientType.Golang) + @parametrize(client_type=SerdeClientType.Java) + def test_context_prefix_serde_client(self, client_type): + """ + Verify a serde client can target a context by setting the schema + registry URL to /contexts/{context}. This is the acceptance test + for CORE-15191. + """ + topic = f"serde-context-prefix-{client_type.name.lower()}" + ctx = ".serde" + self._create_topic(topic=topic) + + # Build context-prefixed SR URL + schema_reg_base = self.redpanda.schema_reg().split(",", 1)[0] + context_sr_url = f"{schema_reg_base}/contexts/{ctx}" + + client = self._get_serde_client( + SchemaType.AVRO, + client_type, + topic, + 5, + schema_registry_url=context_sr_url, + ) + client.start() + client.wait() + + # Verify schemas landed in the context + result = self.sr_client.request( + "GET", + "subjects", + params={"subjectPrefix": f":{ctx}:"}, + ) + assert result.status_code == 200, result.text + subjects = result.json() + expected_subject = f":{ctx}:{topic}-value" + assert expected_subject in subjects, ( + f"Expected {expected_subject} in {subjects}" + ) + + # Verify default context does NOT have this subject + result = self.sr_client.request("GET", f"subjects/{topic}-value/versions") + assert result.status_code == 404, ( + f"Expected 404 for default context, got {result.status_code}" + ) + class SchemaRegistryBasicAuthTest(SchemaRegistryEndpoints): """ @@ -10651,3 +11177,210 @@ def test_nonexistent_schema_id_returns_403_not_404(self): 99999, subject="sub1", auth=self.user_auth ) self.assert_equal(result.status_code, 403) + + @cluster(num_nodes=1) + def test_context_prefix_acl_isolation(self): + """ + A user with ACLs on 'foo' (default context) must NOT be able to + access /contexts/.ctx1/subjects/foo/... — the ACL on the unqualified + subject should not grant access to the context-qualified subject. + """ + # Grant READ on unqualified "sub1" (default context) + self._post_acl(self._create_acl("sub1", "SUBJECT", "LITERAL", "READ")) + + # Access via context prefix should be denied — ACL is on "sub1", + # not ":.ctx1:sub1" + result = self.sr_client.request( + "GET", + "contexts/.ctx1/subjects/sub1/versions", + auth=self.user_auth, + ) + self.assert_equal(result.status_code, 403) + + # Grant READ on the context-qualified subject + self._post_acl(self._create_acl(":.ctx1:sub1", "SUBJECT", "LITERAL", "READ")) + + # Now access via context prefix should succeed + result = self.sr_client.request( + "GET", + "contexts/.ctx1/subjects/sub1/versions", + auth=self.user_auth, + ) + self.assert_equal(result.status_code, 200) + + @cluster(num_nodes=1) + def test_context_prefix_acl_with_prefix_pattern(self): + """ + A prefix ACL on ':.ctx1:' grants access to all subjects in .ctx1 + via context-prefixed URLs, but not to subjects in other contexts. + """ + # Grant prefix ACL covering all subjects in .ctx1 + self._post_acl(self._create_acl(":.ctx1:", "SUBJECT", "PREFIXED", "READ")) + + # Access subjects in .ctx1 via prefix URL — should succeed + result = self.sr_client.request( + "GET", + "contexts/.ctx1/subjects/sub1/versions", + auth=self.user_auth, + ) + self.assert_equal(result.status_code, 200) + + result = self.sr_client.request( + "GET", + "contexts/.ctx1/subjects/sub2/versions", + auth=self.user_auth, + ) + self.assert_equal(result.status_code, 200) + + # Access a subject in a different context — should be denied + result = self.sr_client.request( + "GET", + "contexts/.ctx2/subjects/sub1/versions", + auth=self.user_auth, + ) + self.assert_equal(result.status_code, 403) + + @cluster(num_nodes=1) + def test_context_prefix_all_subject_operations_protected(self): + """ + All context-prefixed subject endpoints must authorize against the + context-qualified subject, not the bare subject name. + """ + # Grant ACL on unqualified "sub1" with ALL operations + self._post_acl(self._create_acl("sub1", "SUBJECT", "LITERAL", "ALL")) + + # Each of these should be denied because the ACL is on "sub1", + # not ":.ctx1:sub1" + schema_data = json.dumps({"schema": schema1_def}) + prefix = "contexts/.ctx1" + + # POST subject (lookup) + result = self.sr_client.request( + "POST", + f"{prefix}/subjects/sub1", + headers=HTTP_POST_HEADERS, + data=schema_data, + auth=self.user_auth, + ) + self.assert_equal( + result.status_code, + 403, + f"POST subjects/sub1 should be 403, got {result.status_code}", + ) + + # GET subject versions + result = self.sr_client.request( + "GET", + f"{prefix}/subjects/sub1/versions", + auth=self.user_auth, + ) + self.assert_equal( + result.status_code, + 403, + f"GET versions should be 403, got {result.status_code}", + ) + + # POST subject versions (register) + result = self.sr_client.request( + "POST", + f"{prefix}/subjects/sub1/versions", + headers=HTTP_POST_HEADERS, + data=schema_data, + auth=self.user_auth, + ) + self.assert_equal( + result.status_code, + 403, + f"POST versions should be 403, got {result.status_code}", + ) + + # GET subject versions version + result = self.sr_client.request( + "GET", + f"{prefix}/subjects/sub1/versions/1", + auth=self.user_auth, + ) + self.assert_equal( + result.status_code, + 403, + f"GET version should be 403, got {result.status_code}", + ) + + # GET subject versions version schema + result = self.sr_client.request( + "GET", + f"{prefix}/subjects/sub1/versions/1/schema", + auth=self.user_auth, + ) + self.assert_equal( + result.status_code, + 403, + f"GET version/schema should be 403, got {result.status_code}", + ) + + # DELETE subject version + result = self.sr_client.request( + "DELETE", + f"{prefix}/subjects/sub1/versions/1", + auth=self.user_auth, + ) + self.assert_equal( + result.status_code, + 403, + f"DELETE version should be 403, got {result.status_code}", + ) + + # DELETE subject + result = self.sr_client.request( + "DELETE", + f"{prefix}/subjects/sub1", + auth=self.user_auth, + ) + self.assert_equal( + result.status_code, + 403, + f"DELETE subject should be 403, got {result.status_code}", + ) + + # Compatibility check + result = self.sr_client.request( + "POST", + f"{prefix}/compatibility/subjects/sub1/versions/1", + headers=HTTP_POST_HEADERS, + data=schema_data, + auth=self.user_auth, + ) + self.assert_equal( + result.status_code, + 403, + f"Compatibility check should be 403, got {result.status_code}", + ) + + # Schema-by-ID routes use deferred auth with scope_subject_query. + # The context prefix injects subject=:.ctx1: which should not match + # the ACL on unqualified "sub1". + sid = self.schema_id_ctx1 + + # GET /contexts/.ctx1/schemas/ids/{id} + result = self.sr_client.request( + "GET", + f"{prefix}/schemas/ids/{sid}", + auth=self.user_auth, + ) + self.assert_equal( + result.status_code, + 403, + f"GET schemas/ids/{sid} should be 403, got {result.status_code}", + ) + + # GET /contexts/.ctx1/schemas/ids/{id}/schema + result = self.sr_client.request( + "GET", + f"{prefix}/schemas/ids/{sid}/schema", + auth=self.user_auth, + ) + self.assert_equal( + result.status_code, + 403, + f"GET schemas/ids/{sid}/schema should be 403, got {result.status_code}", + )