Skip to content

Commit 434bc65

Browse files
committed
feat: extend Kerby KDC Testcontainers with support for multiple principals
- Added methods to configure additional principals and service principals in the `KerbyKdcContainer`. - Enhanced E2E tests to validate service tickets and additional user authentication. - Updated documentation to highlight new support for multiple principals and keytab handling. - Introduced `log4j.properties` for improved logging during tests.
1 parent 693e1ac commit 434bc65

4 files changed

Lines changed: 121 additions & 2 deletions

File tree

kerby-testcontainers/README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,24 @@ Use it from a test:
4646
try (KerbyKdcContainer kdc = new KerbyKdcContainer()
4747
.withRealm("EXAMPLE.COM")
4848
.withClientPrincipal("alice", "alice-secret")
49-
.withServicePrincipal("HTTP/service.example.com")) {
49+
.withServicePrincipal("HTTP/service.example.com")
50+
.withServicePrincipals(
51+
"HTTP/api.example.com@EXAMPLE.COM",
52+
"hive/hiveserver2.example.com@EXAMPLE.COM",
53+
"kafka/broker1.example.com@EXAMPLE.COM")
54+
.withPrincipal("app_user@EXAMPLE.COM", "app-user-secret")) {
5055
kdc.start();
5156
String krb5Conf = kdc.getKrb5Conf();
5257
}
5358
```
5459

60+
`withServicePrincipals(...)` adds service principals and exports a keytab for
61+
each principal under `/var/lib/kerby/keytabs`. Use
62+
`copyServiceKeytabTo(principal, targetPath)` to copy one of those generated
63+
keytabs to the host test filesystem. Use
64+
`withAdditionalServicePrincipal(principal, keytabPath)` when a specific
65+
container-side keytab path is required.
66+
5567
The container supports these environment variables:
5668

5769
`KERBY_REALM`, `KERBY_KDC_HOST`, `KERBY_KDC_BIND_HOST`,

kerby-testcontainers/src/main/java/org/apache/kerby/testcontainers/KerbyKdcContainer.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
import java.nio.file.Path;
2626
import java.time.Duration;
27+
import java.util.LinkedHashMap;
28+
import java.util.Map;
2729

2830
/**
2931
* Testcontainers wrapper for the Kerby KDC Docker image.
@@ -36,13 +38,16 @@ public class KerbyKdcContainer extends GenericContainer<KerbyKdcContainer> {
3638
public static final String DEFAULT_CLIENT_PRINCIPAL = "client";
3739
public static final String DEFAULT_CLIENT_PASSWORD = "client";
3840
public static final String DEFAULT_SERVICE_PRINCIPAL = "HTTP/localhost";
41+
public static final String DEFAULT_KEYTAB_DIR = "/var/lib/kerby/keytabs";
3942
public static final String DEFAULT_SERVICE_KEYTAB = "/var/lib/kerby/keytabs/service.keytab";
4043

4144
private String realm = DEFAULT_REALM;
4245
private String clientPrincipal = DEFAULT_CLIENT_PRINCIPAL;
4346
private String clientPassword = DEFAULT_CLIENT_PASSWORD;
4447
private String servicePrincipal = DEFAULT_SERVICE_PRINCIPAL;
4548
private String serviceKeytab = DEFAULT_SERVICE_KEYTAB;
49+
private final Map<String, String> extraPrincipals = new LinkedHashMap<>();
50+
private final Map<String, String> extraServicePrincipals = new LinkedHashMap<>();
4651

4752
public KerbyKdcContainer() {
4853
this(DEFAULT_IMAGE_NAME);
@@ -82,6 +87,33 @@ public KerbyKdcContainer withServicePrincipal(String principal, String keytabPat
8287
return applyConfiguration();
8388
}
8489

90+
public KerbyKdcContainer withPrincipal(String principal, String password) {
91+
extraPrincipals.put(principal, password);
92+
return applyConfiguration();
93+
}
94+
95+
public KerbyKdcContainer withPrincipals(Map<String, String> principals) {
96+
extraPrincipals.putAll(principals);
97+
return applyConfiguration();
98+
}
99+
100+
public KerbyKdcContainer withServicePrincipals(String... principals) {
101+
for (String principal : principals) {
102+
extraServicePrincipals.put(principal, null);
103+
}
104+
return applyConfiguration();
105+
}
106+
107+
public KerbyKdcContainer withAdditionalServicePrincipal(String principal) {
108+
extraServicePrincipals.put(principal, null);
109+
return applyConfiguration();
110+
}
111+
112+
public KerbyKdcContainer withAdditionalServicePrincipal(String principal, String keytabPath) {
113+
extraServicePrincipals.put(principal, keytabPath);
114+
return applyConfiguration();
115+
}
116+
85117
public KerbyKdcContainer withExtraPrincipals(String principals) {
86118
withEnv("KERBY_EXTRA_PRINCIPALS", principals);
87119
return this;
@@ -112,10 +144,22 @@ public String getServiceKeytab() {
112144
return serviceKeytab;
113145
}
114146

147+
public String getServiceKeytab(String principal) {
148+
String keytab = extraServicePrincipals.get(principal);
149+
if (keytab != null) {
150+
return keytab;
151+
}
152+
return defaultKeytabPath(principal);
153+
}
154+
115155
public void copyServiceKeytabTo(Path target) {
116156
copyFileFromContainer(serviceKeytab, target.toAbsolutePath().toString());
117157
}
118158

159+
public void copyServiceKeytabTo(String principal, Path target) {
160+
copyFileFromContainer(getServiceKeytab(principal), target.toAbsolutePath().toString());
161+
}
162+
119163
public String getKdcHost() {
120164
return getHost();
121165
}
@@ -145,6 +189,8 @@ private KerbyKdcContainer applyConfiguration() {
145189
withEnv("KERBY_CLIENT_PASSWORD", clientPassword);
146190
withEnv("KERBY_SERVICE_PRINCIPAL", servicePrincipal);
147191
withEnv("KERBY_SERVICE_KEYTAB", serviceKeytab);
192+
withEnv("KERBY_EXTRA_PRINCIPALS", joinPasswordPrincipals());
193+
withEnv("KERBY_EXTRA_SERVICE_PRINCIPALS", joinServicePrincipals());
148194
return this;
149195
}
150196

@@ -154,4 +200,36 @@ private String qualifyPrincipal(String principal) {
154200
}
155201
return principal + "@" + realm;
156202
}
203+
204+
private String joinPasswordPrincipals() {
205+
StringBuilder value = new StringBuilder();
206+
for (Map.Entry<String, String> entry : extraPrincipals.entrySet()) {
207+
appendSeparator(value);
208+
value.append(entry.getKey()).append(':').append(entry.getValue());
209+
}
210+
return value.toString();
211+
}
212+
213+
private String joinServicePrincipals() {
214+
StringBuilder value = new StringBuilder();
215+
for (Map.Entry<String, String> entry : extraServicePrincipals.entrySet()) {
216+
appendSeparator(value);
217+
value.append(entry.getKey());
218+
if (entry.getValue() != null) {
219+
value.append(':').append(entry.getValue());
220+
}
221+
}
222+
return value.toString();
223+
}
224+
225+
private void appendSeparator(StringBuilder value) {
226+
if (value.length() > 0) {
227+
value.append(',');
228+
}
229+
}
230+
231+
private String defaultKeytabPath(String principal) {
232+
return DEFAULT_KEYTAB_DIR + "/" + qualifyPrincipal(principal)
233+
.replace('/', '_').replace('@', '_') + ".keytab";
234+
}
157235
}

kerby-testcontainers/src/test/java/org/apache/kerby/testcontainers/KerbyKdcContainerE2ETest.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ public class KerbyKdcContainerE2ETest {
6565
private static final String CLIENT = "alice";
6666
private static final String CLIENT_PASSWORD = "alice-secret";
6767
private static final String SERVICE = "HTTP/localhost";
68+
private static final String API_SERVICE = "HTTP/api.example.com@EXAMPLE.COM";
69+
private static final String HIVE_SERVICE = "hive/hiveserver2.example.com@EXAMPLE.COM";
70+
private static final String KAFKA_SERVICE = "kafka/broker1.example.com@EXAMPLE.COM";
71+
private static final String APP_USER = "app_user@EXAMPLE.COM";
72+
private static final String APP_USER_PASSWORD = "app-user-secret";
6873
private static final String MESSAGE = "kerby-testcontainers-e2e";
6974

7075
@TempDir
@@ -90,21 +95,31 @@ public void clientObtainsServiceTicketAndAuthenticatesToService() throws Excepti
9095
try (KerbyKdcContainer kdc = new KerbyKdcContainer(imageName)
9196
.withRealm("EXAMPLE.COM")
9297
.withClientPrincipal(CLIENT, CLIENT_PASSWORD)
93-
.withServicePrincipal(SERVICE)) {
98+
.withServicePrincipal(SERVICE)
99+
.withServicePrincipals(API_SERVICE, HIVE_SERVICE, KAFKA_SERVICE)
100+
.withPrincipal(APP_USER, APP_USER_PASSWORD)) {
94101
kdc.start();
95102

96103
configureJavaKerberos(kdc);
97104

98105
Path serviceKeytab = testDir.resolve("service.keytab");
106+
Path hiveKeytab = testDir.resolve("hive.keytab");
99107
Path ticketCache = testDir.resolve("alice.ccache");
100108
kdc.copyServiceKeytabTo(serviceKeytab);
109+
kdc.copyServiceKeytabTo(HIVE_SERVICE, hiveKeytab);
101110

102111
KrbClient client = createKerbyClient(kdc);
103112
TgtTicket tgt = client.requestTgt(kdc.getClientPrincipal(), kdc.getClientPassword());
104113
assertThat(tgt).isNotNull();
105114

106115
SgtTicket sgt = client.requestSgt(tgt, kdc.getServicePrincipal());
107116
assertThat(sgt).isNotNull();
117+
assertServiceTicket(client, tgt, API_SERVICE);
118+
assertServiceTicket(client, tgt, HIVE_SERVICE);
119+
assertServiceTicket(client, tgt, KAFKA_SERVICE);
120+
121+
TgtTicket appUserTgt = client.requestTgt(APP_USER, APP_USER_PASSWORD);
122+
assertThat(appUserTgt).isNotNull();
108123

109124
client.storeTicket(tgt, ticketCache.toFile());
110125

@@ -116,6 +131,12 @@ public void clientObtainsServiceTicketAndAuthenticatesToService() throws Excepti
116131
}
117132
}
118133

134+
private void assertServiceTicket(KrbClient client, TgtTicket tgt, String servicePrincipal)
135+
throws Exception {
136+
SgtTicket sgt = client.requestSgt(tgt, servicePrincipal);
137+
assertThat(sgt).isNotNull();
138+
}
139+
119140
private void assumeDockerAvailable() {
120141
try {
121142
DockerClientFactory.instance().client();
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
log4j.rootLogger=INFO, console
2+
log4j.appender.console=org.apache.log4j.ConsoleAppender
3+
log4j.appender.console.Target=System.err
4+
log4j.appender.console.layout=org.apache.log4j.PatternLayout
5+
log4j.appender.console.layout.ConversionPattern=%d{HH:mm:ss.SSS} %-5p [%t] %c - %m%n
6+
7+
log4j.logger.org.testcontainers=INFO
8+
log4j.logger.com.github.dockerjava=WARN

0 commit comments

Comments
 (0)