Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Kerberos credentials #297

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 88 additions & 1 deletion src/generated/resources/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@
}
},
"401" : {
"description" : "Could not authenticate with updated connection configuration",
"description" : "Could not authenticate",
"content" : {
"application/json" : {
"schema" : {
Expand Down Expand Up @@ -263,6 +263,68 @@
"description" : "No Content"
}
}
},
"patch" : {
"tags" : [ "Connections Resource" ],
"parameters" : [ {
"name" : "id",
"in" : "path",
"required" : true,
"schema" : {
"type" : "string"
}
} ],
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/JsonMergePatch"
}
}
}
},
"responses" : {
"200" : {
"description" : "Connection updated with PATCH",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Connection"
}
}
}
},
"404" : {
"description" : "Connection not found",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Failure"
}
}
}
},
"401" : {
"description" : "Could not authenticate",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Failure"
}
}
}
},
"400" : {
"description" : "Invalid input",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Failure"
}
}
}
}
}
}
},
"/gateway/v1/feature-flags/{id}/value" : {
Expand Down Expand Up @@ -937,6 +999,9 @@
"type" : "string",
"example" : "2022-03-10T16:15:50Z"
},
"JsonMergePatch" : {
"type" : "object"
},
"JsonNode" : {
"type" : "object",
"properties" : {
Expand Down Expand Up @@ -1031,6 +1096,8 @@
"$ref" : "#/components/schemas/ApiKeyAndSecret"
}, {
"$ref" : "#/components/schemas/OAuthCredentials"
}, {
"$ref" : "#/components/schemas/KerberosCredentials"
} ],
"nullable" : true
},
Expand Down Expand Up @@ -1072,6 +1139,26 @@
}
}
},
"KerberosCredentials" : {
"description" : "Kerberos authentication credentials",
"required" : [ "principal", "keytab_path" ],
"type" : "object",
"properties" : {
"principal" : {
"description" : "The Kerberos principal to use.",
"type" : "string"
},
"keytab_path" : {
"description" : "The Kerberos keytab file path.",
"type" : "string"
},
"service_name" : {
"description" : "Service name that matches the primary name of the Kafka brokers configured in the Broker JAAS file. Defaults to 'kafka'.",
"default" : "kafka",
"type" : "string"
}
}
},
"KeyStore" : {
"required" : [ "path" ],
"type" : "object",
Expand Down
62 changes: 61 additions & 1 deletion src/generated/resources/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ paths:
schema:
$ref: "#/components/schemas/Failure"
"401":
description: Could not authenticate with updated connection configuration
description: Could not authenticate
content:
application/json:
schema:
Expand All @@ -180,6 +180,45 @@ paths:
responses:
"204":
description: No Content
patch:
rohitsanj marked this conversation as resolved.
Show resolved Hide resolved
tags:
- Connections Resource
parameters:
- name: id
in: path
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/JsonMergePatch"
responses:
"200":
description: Connection updated with PATCH
content:
application/json:
schema:
$ref: "#/components/schemas/Connection"
"404":
description: Connection not found
content:
application/json:
schema:
$ref: "#/components/schemas/Failure"
"401":
description: Could not authenticate
content:
application/json:
schema:
$ref: "#/components/schemas/Failure"
"400":
description: Invalid input
content:
application/json:
schema:
$ref: "#/components/schemas/Failure"
/gateway/v1/feature-flags/{id}/value:
get:
tags:
Expand Down Expand Up @@ -673,6 +712,8 @@ components:
format: date-time
type: string
example: 2022-03-10T16:15:50Z
JsonMergePatch:
type: object
JsonNode:
type: object
properties:
Expand Down Expand Up @@ -751,6 +792,7 @@ components:
- $ref: "#/components/schemas/BasicCredentials"
- $ref: "#/components/schemas/ApiKeyAndSecret"
- $ref: "#/components/schemas/OAuthCredentials"
- $ref: "#/components/schemas/KerberosCredentials"
nullable: true
ssl:
description: "The SSL configuration for connecting to the Kafka cluster.\
Expand Down Expand Up @@ -782,6 +824,24 @@ components:
type: object
allOf:
- $ref: "#/components/schemas/AuthErrors"
KerberosCredentials:
description: Kerberos authentication credentials
required:
- principal
- keytab_path
type: object
properties:
principal:
description: The Kerberos principal to use.
type: string
keytab_path:
description: The Kerberos keytab file path.
type: string
service_name:
description: Service name that matches the primary name of the Kafka brokers
configured in the Broker JAAS file. Defaults to 'kafka'.
default: kafka
type: string
KeyStore:
required:
- path
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
@JsonSubTypes({
@Type(value = BasicCredentials.class),
@Type(value = ApiKeyAndSecret.class),
@Type(value = OAuthCredentials.class)
@Type(value = OAuthCredentials.class),
@Type(value = KerberosCredentials.class),
})
@RegisterForReflection
public interface Credentials {
Expand All @@ -44,6 +45,7 @@ enum Type {
MUTUAL_TLS,
OAUTH2,
API_KEY_AND_SECRET,
KERBEROS
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package io.confluent.idesidecar.restapi.credentials;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.confluent.idesidecar.restapi.exceptions.Failure.Error;
import io.quarkus.runtime.annotations.RegisterForReflection;
import io.soabase.recordbuilder.core.RecordBuilder;
import org.apache.kafka.clients.CommonClientConfigs;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

@Schema(description = "Kerberos authentication credentials")
@RegisterForReflection
@RecordBuilder
public record KerberosCredentials(
@Schema(description = "The Kerberos principal to use.",
required = true)
@NotNull
String principal,

@Schema(description = "The Kerberos keytab file path.", required = true)
@JsonProperty(value = "keytab_path")
@NotNull
String keytabPath,

@Schema(description = "Service name that matches the primary name of the " +
"Kafka brokers configured in the Broker JAAS file. Defaults to 'kafka'.",
defaultValue = "kafka")
@JsonProperty(value = "service_name")
String serviceName
) implements Credentials {

private static final String KERBEROS_LOGIN_MODULE_CLASS =
"com.sun.security.auth.module.Krb5LoginModule";

@Override
public Type type() {
return Type.KERBEROS;
}

@Override
public Optional<Map<String, String>> kafkaClientProperties(KafkaConnectionOptions options) {
var config = new LinkedHashMap<String, String>();
var tlsConfig = options.tlsConfig();
if (tlsConfig.enabled()) {
config.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SASL_SSL");
} else {
config.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SASL_PLAINTEXT");
}

// See: https://docs.confluent.io/platform/current/security/authentication/sasl/gssapi/overview.html#clients
config.put("sasl.mechanism", "GSSAPI");

// See: https://docs.oracle.com/javase/8/docs/jre/api/security/jaas/spec/com/sun/security/auth/module/Krb5LoginModule.html
config.put(
"sasl.jaas.config",
("%s required " +
"useKeyTab=true " +
"doNotPrompt=true " +
"useTicketCache=false " +
"keyTab=\"%s\" " +
"principal=\"%s\";"
).formatted(
KERBEROS_LOGIN_MODULE_CLASS,
keytabPath,
principal
)
);

if (serviceName != null) {
config.put("sasl.kerberos.service.name", serviceName);
}

return Optional.of(config);
}

@Override
public Optional<Map<String, String>> schemaRegistryClientProperties(
SchemaRegistryConnectionOptions options
) {
// Kerberos is not supported as a client authentication mechanism for Schema Registry
return Optional.empty();
}

@Override
public void validate(
List<Error> errors,
String path,
String what
) {
if (principal == null || principal.isBlank()) {
Copy link
Contributor

@jlrobins jlrobins Jan 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither should have embedded double-quotes, else the formatting of sasl.jaas.config in kafkaClientProperties() will be corrupted.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I think double-quotes would have to be escaped.

errors.add(
Error.create()
.withDetail("%s principal is required and may not be blank", what)
.withSource("%s.principal".formatted(path))
);
}
if (keytabPath == null || keytabPath.isBlank()) {
errors.add(
Error.create()
.withDetail("%s keytab path is required and may not be blank", what)
.withSource("%s.keytabPath".formatted(path))
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ public record KafkaClusterConfig(
BasicCredentials.class,
ApiKeyAndSecret.class,
OAuthCredentials.class,
KerberosCredentials.class,
},
nullable = true
)
Expand Down
Loading