diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b249e6..dc06efa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,16 @@ and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
## [Unreleased]
+## [0.52.0] - 2026-10-26
+
+### Added
+
+- **`Vault.open(DirContext, LDAPConnection)`:** a vault over an extended-operation connection the
+ caller has already connected and bound. Whatever the caller set on it applies to every engine
+ request, such as RFC 4370 proxied authorization, so a service account's connection acts for one
+ person; Directory Console uses it for Identity Manager under single sign-on. The engine keeps the
+ last identity it was given on a connection, so such a connection belongs to that one person.
+
## [0.51.0] - 2026-10-26
### Changed
diff --git a/pom.xml b/pom.xml
index 9ac1333..cb1fb87 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
com.pointblue.dirxml
dirxml-dev
- 0.51.0
+ 0.52.0
jar
DirXML Dev
diff --git a/src/main/java/com/pointblue/dirxml/dev/deploy/Vault.java b/src/main/java/com/pointblue/dirxml/dev/deploy/Vault.java
index bc2269a..485c690 100644
--- a/src/main/java/com/pointblue/dirxml/dev/deploy/Vault.java
+++ b/src/main/java/com/pointblue/dirxml/dev/deploy/Vault.java
@@ -209,6 +209,31 @@ public static Vault open(DirContext ldap, String host, int port, javax.net.ssl.S
return v;
}
+ /**
+ * A vault over two connections the caller already holds: {@code ldap}, used as is and left open by
+ * {@link #close}, and {@code ops}, the extended-operation connection, already connected and bound by
+ * the caller and closed by {@link #close}. Whatever the caller set on {@code ops} applies to every
+ * engine request: for example default controls, such as RFC 4370 proxied authorization so a service
+ * account's connection acts for one person (Directory Console's single sign-on). Such a connection
+ * belongs to that one person: the engine keeps the last identity it was given on a connection.
+ */
+ public static Vault open(DirContext ldap, LDAPConnection ops) {
+ if (ldap == null || ops == null) {
+ throw new IllegalArgumentException("open needs the directory connection and the extended-operation connection");
+ }
+ if (!ops.isConnected()) {
+ throw new IllegalArgumentException("the extended-operation connection is not connected");
+ }
+ Config c = new Config();
+ String host = ops.getHost();
+ c.url = "ldaps://" + (host != null && host.contains(":") && !host.startsWith("[") ? "[" + host + "]" : host) + ":" + ops.getPort();
+ c.bindDn = ops.getAuthenticationDN();
+ Vault v = new Vault(c, ldap);
+ v.ownsLdap = false;
+ v.ops = ops;
+ return v;
+ }
+
@Override
public void close() {
if (ownsLdap) {
diff --git a/src/test/java/com/pointblue/dirxml/dev/deploy/VaultOpenTest.java b/src/test/java/com/pointblue/dirxml/dev/deploy/VaultOpenTest.java
index 0e9f603..e25a817 100644
--- a/src/test/java/com/pointblue/dirxml/dev/deploy/VaultOpenTest.java
+++ b/src/test/java/com/pointblue/dirxml/dev/deploy/VaultOpenTest.java
@@ -35,4 +35,49 @@ public void aFailedBindNamesTheServerAndLeavesTheCallersConnectionAlone() throws
assertFalse("the password never appears in the message", e.getMessage().contains("never-sent"));
assertFalse("the caller's connection is not closed", closed.get());
}
+
+ @Test
+ public void aCallersEngineConnectionIsUsedAsGivenAndClosedWithTheVault() {
+ AtomicBoolean ldapClosed = new AtomicBoolean();
+ DirContext lent = (DirContext) Proxy.newProxyInstance(DirContext.class.getClassLoader(), new Class>[] {DirContext.class},
+ (proxy, method, args) -> {
+ if (method.getName().equals("close")) {
+ ldapClosed.set(true);
+ }
+ return null;
+ });
+ AtomicBoolean opsClosed = new AtomicBoolean();
+ com.novell.ldap.LDAPConnection ops = new com.novell.ldap.LDAPConnection() {
+ @Override
+ public boolean isConnected() {
+ return true;
+ }
+
+ @Override
+ public String getHost() {
+ return "edir1.acme.example";
+ }
+
+ @Override
+ public int getPort() {
+ return 636;
+ }
+
+ @Override
+ public void disconnect() {
+ opsClosed.set(true);
+ }
+ };
+ Vault v = Vault.open(lent, ops);
+ v.close();
+ assertTrue("the engine connection is the vault's to close", opsClosed.get());
+ assertFalse("the caller's directory connection is not closed", ldapClosed.get());
+ }
+
+ @Test
+ public void anEngineConnectionMustBeConnected() {
+ DirContext lent = (DirContext) Proxy.newProxyInstance(DirContext.class.getClassLoader(), new Class>[] {DirContext.class}, (p, m, a) -> null);
+ assertThrows(IllegalArgumentException.class, () -> Vault.open(lent, new com.novell.ldap.LDAPConnection()));
+ assertThrows(IllegalArgumentException.class, () -> Vault.open(null, new com.novell.ldap.LDAPConnection()));
+ }
}