You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@knox.apache.org by sm...@apache.org on 2026/09/03 13:43:02 UTC

(knox) branch master updated: KNOX-3426: Embedded-Derby delegation policy service (#1374)

This is an automated email from the ASF dual-hosted git repository.

smolnar82 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git


The following commit(s) were added to refs/heads/master by this push:
     new af3b2989a KNOX-3426:  Embedded-Derby delegation policy service (#1374)
af3b2989a is described below

commit af3b2989a142f9196f81308a3779df6531fc40ca
Author: Sandor Molnar <sm...@apache.org>
AuthorDate: Thu Sep 3 15:42:56 2026 +0200

    KNOX-3426:  Embedded-Derby delegation policy service (#1374)
---
 .../gateway/database/DerbyDatabaseCredentials.java |  49 +++++++
 .../EmbeddedDerbyDatabase.java}                    |  83 +++++------
 .../apache/knox/gateway/database/JDBCUtils.java    |  11 +-
 .../apache/knox/gateway/database/KnoxDatabase.java |  60 ++++++++
 .../factory/DelegationPolicyServiceFactory.java    |  46 +++++-
 .../delegation/DelegationPolicyDatabase.java       |  40 +-----
 .../delegation/DerbyDBDelegationPolicyService.java |  67 +++++++++
 .../delegation/JdbcDelegationPolicyService.java    |   4 +
 .../DerbyDBFederatedIdentityService.java           |  48 +------
 .../DerbyDBTrustedOidcIssuerService.java           |  48 +------
 .../token/impl/DerbyDBTokenStateService.java       |  58 +-------
 .../knox/gateway/database/KnoxDatabaseTest.java    | 158 +++++++++++++++++++++
 .../DelegationPolicyServiceFactoryTest.java        |  98 +++++++------
 .../services/factory/ServiceFactoryTest.java       |   9 +-
 14 files changed, 506 insertions(+), 273 deletions(-)

diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/database/DerbyDatabaseCredentials.java b/gateway-server/src/main/java/org/apache/knox/gateway/database/DerbyDatabaseCredentials.java
new file mode 100644
index 000000000..060df9d88
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/database/DerbyDatabaseCredentials.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.knox.gateway.database;
+
+import static org.apache.knox.gateway.database.AbstractDataSourceFactory.DATABASE_PASSWORD_ALIAS_NAME;
+import static org.apache.knox.gateway.database.AbstractDataSourceFactory.DATABASE_USER_ALIAS_NAME;
+
+import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.services.security.MasterService;
+
+/**
+ * Shared resolution of the embedded-Derby connection credentials used by the self-provisioning
+ * {@code DerbyDB*} services (token state, federated identity, trusted OIDC issuer, delegation
+ * policy). All of these back onto the single embedded Derby database and therefore resolve the
+ * same connection user and password: read from the gateway alias store when present, otherwise
+ * falling back to a well-known default user and the master secret.
+ */
+public final class DerbyDatabaseCredentials {
+
+  /** Default connection user for the embedded Derby database when no alias is configured. */
+  public static final String DEFAULT_DB_USER_NAME = "knox";
+
+  private DerbyDatabaseCredentials() {
+  }
+
+  public static String getDatabaseUserName(AliasService aliasService) throws Exception {
+    final char[] dbUserAliasValue = aliasService.getPasswordFromAliasForGateway(DATABASE_USER_ALIAS_NAME);
+    return dbUserAliasValue != null ? new String(dbUserAliasValue) : DEFAULT_DB_USER_NAME;
+  }
+
+  public static String getDatabasePassword(AliasService aliasService, MasterService masterService) throws Exception {
+    final char[] dbPasswordAliasValue = aliasService.getPasswordFromAliasForGateway(DATABASE_PASSWORD_ALIAS_NAME);
+    return dbPasswordAliasValue != null ? new String(dbPasswordAliasValue) : new String(masterService.getMasterSecret());
+  }
+}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DerbyDBTokenStateService.java b/gateway-server/src/main/java/org/apache/knox/gateway/database/EmbeddedDerbyDatabase.java
similarity index 51%
copy from gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DerbyDBTokenStateService.java
copy to gateway-server/src/main/java/org/apache/knox/gateway/database/EmbeddedDerbyDatabase.java
index 471542b32..aeaa686e2 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DerbyDBTokenStateService.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/database/EmbeddedDerbyDatabase.java
@@ -5,87 +5,77 @@
  * licenses this file to you under the Apache License, Version 2.0 (the
  * "License"); you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- *
+ * <p>
  * http://www.apache.org/licenses/LICENSE-2.0
- *
+ * <p>
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.knox.gateway.services.token.impl;
+package org.apache.knox.gateway.database;
 
 import static org.apache.knox.gateway.config.impl.GatewayConfigImpl.GATEWAY_DATABASE_NAME;
 import static org.apache.knox.gateway.config.impl.GatewayConfigImpl.GATEWAY_DATABASE_TYPE;
-import static org.apache.knox.gateway.services.security.AliasService.NO_CLUSTER_NAME;
 import static org.apache.knox.gateway.database.AbstractDataSourceFactory.DATABASE_PASSWORD_ALIAS_NAME;
 import static org.apache.knox.gateway.database.AbstractDataSourceFactory.DATABASE_USER_ALIAS_NAME;
 import static org.apache.knox.gateway.database.DatabaseType.DERBY;
+import static org.apache.knox.gateway.services.security.AliasService.NO_CLUSTER_NAME;
 
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
 import org.apache.hadoop.conf.Configuration;
 import org.apache.knox.gateway.config.GatewayConfig;
 import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.security.AliasService;
 import org.apache.knox.gateway.services.security.MasterService;
 import org.apache.knox.gateway.shell.jdbc.derby.DerbyDatabase;
 import org.apache.knox.gateway.util.FileUtils;
 
-public class DerbyDBTokenStateService extends JDBCTokenStateService {
+/**
+ * Manages the lifecycle of the single embedded Apache Derby database that the KnoxIDF
+ * self-provisioning services share under {@code ${securityDir}/tokens}.
+ * <p>
+ * Extracted so that {@code DerbyDBTokenStateService} and the KnoxIDF {@code DerbyDB*Service}s
+ * (federated identity, trusted OIDC issuer, delegation policy) do not each duplicate the identical
+ * boot/shutdown logic. The {@code ;create=true} JDBC URL is idempotent, so several services
+ * starting their own instance simply connect to the already-booted database.
+ */
+public final class EmbeddedDerbyDatabase {
 
-  public static final String DEFAULT_TOKEN_DB_USER_NAME = "knox";
+  /** Folder name (under the gateway security dir) of the shared embedded Derby database. */
   public static final String DB_NAME = "tokens";
 
+  private final Path folder;
   private DerbyDatabase derbyDatabase;
-  private Path derbyDatabaseFolder;
-  private MasterService masterService;
 
-  public void setMasterService(MasterService masterService) {
-    this.masterService = masterService;
+  public EmbeddedDerbyDatabase(GatewayConfig config) {
+    this.folder = Paths.get(config.getGatewaySecurityDir(), DB_NAME);
   }
 
-  @Override
-  public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException {
-    try {
-      derbyDatabaseFolder = Paths.get(config.getGatewaySecurityDir(), DB_NAME);
-      startDerby();
-      ((Configuration) config).set(GATEWAY_DATABASE_TYPE, DERBY.type());
-      ((Configuration) config).set(GATEWAY_DATABASE_NAME, derbyDatabaseFolder.toString());
-      getAliasService().addAliasForCluster(NO_CLUSTER_NAME, DATABASE_USER_ALIAS_NAME, getDatabaseUserName());
-      getAliasService().addAliasForCluster(NO_CLUSTER_NAME, DATABASE_PASSWORD_ALIAS_NAME, getDatabasePassword());
-      super.init(config, options);
+  /**
+   * Boots the embedded Derby database and points the shared {@link GatewayConfig} at it: starts the
+   * network server, sets the database type/name, ensures the connection user/password aliases exist
+   * and tightens the folder permissions. Callers delegate the actual persistence to their JDBC
+   * service by invoking {@code super.init(...)} afterwards.
+   */
+  public void start(GatewayConfig config, AliasService aliasService, MasterService masterService) throws Exception {
+    bootDerby();
+    ((Configuration) config).set(GATEWAY_DATABASE_TYPE, DERBY.type());
+    ((Configuration) config).set(GATEWAY_DATABASE_NAME, folder.toString());
+    aliasService.addAliasForCluster(NO_CLUSTER_NAME, DATABASE_USER_ALIAS_NAME, DerbyDatabaseCredentials.getDatabaseUserName(aliasService));
+    aliasService.addAliasForCluster(NO_CLUSTER_NAME, DATABASE_PASSWORD_ALIAS_NAME, DerbyDatabaseCredentials.getDatabasePassword(aliasService, masterService));
 
-      // we need the "x" permission too to be able to browse that folder (600 is not enough)
-      if (Files.exists(derbyDatabaseFolder)) {
-        FileUtils.chmod("700", derbyDatabaseFolder.toFile());
-      }
-    } catch (Exception e) {
-      throw new ServiceLifecycleException("Error while initiating DerbyDBTokenStateService: " + e, e);
+    // we need the "x" permission too to be able to browse that folder (600 is not enough)
+    if (Files.exists(folder)) {
+      FileUtils.chmod("700", folder.toFile());
     }
   }
 
-  private void startDerby() throws Exception {
-    derbyDatabase = new DerbyDatabase(derbyDatabaseFolder.toString());
-    derbyDatabase.create();
-    TimeUnit.SECONDS.sleep(1); // give a bit of time for the server to start
-  }
-
-  private String getDatabasePassword() throws Exception {
-    final char[] dbPasswordAliasValue = getAliasService().getPasswordFromAliasForGateway(DATABASE_PASSWORD_ALIAS_NAME);
-    return dbPasswordAliasValue != null ? new String(dbPasswordAliasValue) : new String(masterService.getMasterSecret());
-  }
-
-  private String getDatabaseUserName() throws Exception {
-    final char[] dbUserAliasValue = getAliasService().getPasswordFromAliasForGateway(DATABASE_USER_ALIAS_NAME);
-    return dbUserAliasValue != null ? new String(dbUserAliasValue) : DEFAULT_TOKEN_DB_USER_NAME;
-  }
-
-  @Override
   public void stop() throws ServiceLifecycleException {
     try {
       if (derbyDatabase != null) {
@@ -96,4 +86,9 @@ public class DerbyDBTokenStateService extends JDBCTokenStateService {
     }
   }
 
+  private void bootDerby() throws Exception {
+    derbyDatabase = new DerbyDatabase(folder.toString());
+    derbyDatabase.create();
+    TimeUnit.SECONDS.sleep(1); // give a bit of time for the server to start
+  }
 }
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/database/JDBCUtils.java b/gateway-server/src/main/java/org/apache/knox/gateway/database/JDBCUtils.java
index 30e2ab59f..39b9836cc 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/database/JDBCUtils.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/database/JDBCUtils.java
@@ -63,9 +63,14 @@ public class JDBCUtils {
     }
 
     public static void createTableFromSQL(String createSqlFileName, DataSource dataSource, ClassLoader classLoader) throws Exception {
-        try (InputStream is = classLoader.getResourceAsStream(createSqlFileName);
-             Connection connection = dataSource.getConnection();Statement createTableStatement = connection.createStatement()) {
-            String createTableSql = IOUtils.toString(is, UTF_8);
+        try (InputStream is = classLoader.getResourceAsStream(createSqlFileName)) {
+            createTableFromSQL(IOUtils.toString(is, UTF_8), dataSource);
+        }
+    }
+
+    public static void createTableFromSQL(String createTableSql, DataSource dataSource) throws Exception {
+        try (Connection connection = dataSource.getConnection();
+             Statement createTableStatement = connection.createStatement()) {
             createTableStatement.execute(createTableSql);
         }
     }
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/database/KnoxDatabase.java b/gateway-server/src/main/java/org/apache/knox/gateway/database/KnoxDatabase.java
index dd6ee68e8..f5a92a8b8 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/database/KnoxDatabase.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/database/KnoxDatabase.java
@@ -16,10 +16,26 @@
  */
 package org.apache.knox.gateway.database;
 
+import org.apache.commons.io.IOUtils;
+
 import javax.sql.DataSource;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 public class KnoxDatabase {
 
+    // Matches the leading "CREATE TABLE [IF NOT EXISTS] <name>" of a DDL statement so the table name
+    // can be handed to the existence check. Case-insensitive; tolerates the IF NOT EXISTS that the
+    // standard (PostgreSQL/MySQL/HSQL) scripts use but Derby and Oracle omit.
+    private static final Pattern CREATE_TABLE_PATTERN =
+        Pattern.compile("(?i)^\\s*CREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?([\\w.]+)");
+
     protected final DataSource dataSource;
 
     public KnoxDatabase(DataSource dataSource) {
@@ -34,4 +50,48 @@ public class KnoxDatabase {
             JDBCUtils.createTableFromSQL(createSqlFileName, dataSource, getClass().getClassLoader());
         }
     }
+
+    protected void createTablesIfNotExist(String sqlFileName) throws Exception {
+        final Map<String, String> createSqlByTableName = parseCreateTableStatements(sqlFileName);
+        for (Map.Entry<String, String> entry : createSqlByTableName.entrySet()) {
+            if (!JDBCUtils.tableExists(entry.getKey(), dataSource)) {
+                JDBCUtils.createTableFromSQL(entry.getValue(), dataSource);
+            }
+        }
+    }
+
+    protected Map<String, String> parseCreateTableStatements(String sqlFileName) throws IOException {
+        try (InputStream is = getClass().getClassLoader().getResourceAsStream(sqlFileName)) {
+            if (is == null) {
+                throw new IllegalStateException("DDL script not found on classpath: " + sqlFileName);
+            }
+            final String script = IOUtils.toString(is, UTF_8);
+            final String withoutLicenseHeader = removeLicenseHeader(script);
+            final Map<String, String> createSqlByTableName = new LinkedHashMap<>();
+            for (String statement : withoutLicenseHeader.split(";")) {
+                final String trimmed = statement.trim();
+                if (!trimmed.isEmpty()) {
+                    createSqlByTableName.put(extractTableName(trimmed), trimmed);
+                }
+            }
+            return createSqlByTableName;
+        }
+    }
+
+    /* The DDL scripts begin with the ASF license as a block of "--" comment lines. Stripping only
+    the comment lines (rather than cutting everything before the first CREATE TABLE) removes the
+    header while leaving every SQL statement intact -- notably the PostgreSQL pg_advisory_lock
+    statements that guard concurrent CREATE TABLE IF NOT EXISTS, which the earlier approach cut.
+     */
+    private String removeLicenseHeader(String script) {
+        return script.replaceAll("(?m)^\\s*--.*$", "");
+    }
+
+    private  String extractTableName(String createTableStatement) {
+        final Matcher matcher = CREATE_TABLE_PATTERN.matcher(createTableStatement);
+        if (!matcher.find()) {
+            throw new IllegalStateException("Expected a CREATE TABLE statement but found: " + createTableStatement);
+        }
+        return matcher.group(1);
+    }
 }
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/DelegationPolicyServiceFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/DelegationPolicyServiceFactory.java
index 01f0b2bcc..065ab0684 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/DelegationPolicyServiceFactory.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/DelegationPolicyServiceFactory.java
@@ -18,11 +18,13 @@ package org.apache.knox.gateway.services.factory;
 
 import org.apache.knox.gateway.GatewayMessages;
 import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.database.DatabaseType;
 import org.apache.knox.gateway.i18n.messages.MessagesFactory;
 import org.apache.knox.gateway.services.GatewayServices;
 import org.apache.knox.gateway.services.Service;
 import org.apache.knox.gateway.services.ServiceLifecycleException;
 import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.services.knoxidf.delegation.DerbyDBDelegationPolicyService;
 import org.apache.knox.gateway.services.knoxidf.delegation.EmptyDelegationPolicyService;
 import org.apache.knox.gateway.services.knoxidf.delegation.JdbcDelegationPolicyService;
 
@@ -41,14 +43,18 @@ public class DelegationPolicyServiceFactory extends AbstractServiceFactory {
       throws ServiceLifecycleException {
 
     String implementationToUse = implementation;
+    // No explicit impl configured: auto-select a persistence backend when KnoxIDF is deployed.
+    // Otherwise honor the configured impl (very likely a prod JDBC store).
     if (isEmptyDefaultImplementation(implementationToUse) && isKnoxIdfEnabledInAnyTopology(gatewayServices, gatewayConfig)) {
-      implementationToUse = JdbcDelegationPolicyService.class.getName();
+      implementationToUse = chooseAutoImplementation(gatewayConfig);
     }
 
     Service service = null;
     if (shouldCreateService(implementationToUse)) {
       if (matchesImplementation(implementationToUse, EmptyDelegationPolicyService.class, true)) {
         service = new EmptyDelegationPolicyService();
+      } else if (matchesImplementation(implementationToUse, DerbyDBDelegationPolicyService.class)) {
+        service = createDerbyService(gatewayServices, gatewayConfig, options);
       } else if (matchesImplementation(implementationToUse, JdbcDelegationPolicyService.class)) {
         service = createJdbcService(gatewayServices, gatewayConfig, options);
       }
@@ -59,6 +65,42 @@ public class DelegationPolicyServiceFactory extends AbstractServiceFactory {
     return service;
   }
 
+  /**
+   * Chooses the auto-enabled implementation when KnoxIDF is deployed with no explicit impl: an
+   * operator-configured external database wins (very likely a prod JDBC store), otherwise a
+   * self-provisioning embedded Derby store (the {@code none}/{@code derbydb} default) so the
+   * delegation policy registry works out of the box without any extra infrastructure.
+   */
+  String chooseAutoImplementation(GatewayConfig gatewayConfig) {
+    return isExternalDatabaseConfigured(gatewayConfig)
+        ? JdbcDelegationPolicyService.class.getName()
+        : DerbyDBDelegationPolicyService.class.getName();
+  }
+
+  private boolean isExternalDatabaseConfigured(GatewayConfig gatewayConfig) {
+    final String databaseType = gatewayConfig.getDatabaseType();
+    try {
+      return DatabaseType.fromString(databaseType) != DatabaseType.DERBY;
+    } catch (IllegalArgumentException e) {
+      // "none" (the default) or any unrecognized value: no real external DB -> use Derby.
+      return false;
+    }
+  }
+
+  private Service createDerbyService(GatewayServices gatewayServices, GatewayConfig gatewayConfig, Map<String, String> options)
+      throws ServiceLifecycleException {
+    try {
+      final DerbyDBDelegationPolicyService derbyService = new DerbyDBDelegationPolicyService();
+      derbyService.setAliasService(getAliasService(gatewayServices));
+      derbyService.setMasterService(getMasterService(gatewayServices));
+      derbyService.init(gatewayConfig, options);
+      return derbyService;
+    } catch (ServiceLifecycleException e) {
+      LOG.errorInitializingService(DerbyDBDelegationPolicyService.class.getName(), e.getMessage(), e);
+      return new EmptyDelegationPolicyService();
+    }
+  }
+
   private Service createJdbcService(GatewayServices gatewayServices, GatewayConfig gatewayConfig, Map<String, String> options)
       throws ServiceLifecycleException {
     try {
@@ -79,6 +121,6 @@ public class DelegationPolicyServiceFactory extends AbstractServiceFactory {
 
   @Override
   protected Collection<String> getKnownImplementations() {
-    return List.of(DEFAULT_IMPLEMENTATION, JdbcDelegationPolicyService.class.getName());
+    return List.of(DEFAULT_IMPLEMENTATION, JdbcDelegationPolicyService.class.getName(), DerbyDBDelegationPolicyService.class.getName());
   }
 }
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/DelegationPolicyDatabase.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/DelegationPolicyDatabase.java
index 6631feef5..516dedb95 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/DelegationPolicyDatabase.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/DelegationPolicyDatabase.java
@@ -16,13 +16,10 @@
  */
 package org.apache.knox.gateway.services.knoxidf.delegation;
 
-import org.apache.commons.io.IOUtils;
 import org.apache.knox.gateway.database.DatabaseType;
-import org.apache.knox.gateway.database.JDBCUtils;
 import org.apache.knox.gateway.database.KnoxDatabase;
 
 import javax.sql.DataSource;
-import java.io.InputStream;
 import java.sql.Connection;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
@@ -37,8 +34,6 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.UUID;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
-
 /**
  * JDBC helper for the five DELEGATION_POLICIES tables.
  * All SQL uses {@link PreparedStatement} with {@code ?} parameters only.
@@ -128,40 +123,7 @@ class DelegationPolicyDatabase extends KnoxDatabase {
     this.selectAllSql = SELECT_ALL_BASE_SQL + " FETCH FIRST " + (listMaxTotal + 1) + " ROWS ONLY";
     this.selectAllFilteredSql = SELECT_ALL_BASE_SQL + " WHERE actor_authority = ? FETCH FIRST " + (listMaxPerAuthority + 1) + " ROWS ONLY";
     final DatabaseType databaseType = DatabaseType.fromString(dbType);
-    createDelegationTablesIfNotExists(databaseType.delegationPolicyTablesSql());
-  }
-
-  /**
-   * Multi-statement DDL runner: checks if DELEGATION_POLICIES exists, then strips SQL line
-   * comments, splits on {@code ;}, and executes each non-empty statement individually.
-   * {@link JDBCUtils#createTableFromSQL} handles only single statements; delegation needs five.
-   * Comment stripping must happen before the split because the ASF license header contains a
-   * semicolon inside a {@code --} comment line, which would otherwise produce a spurious token.
-   */
-  private void createDelegationTablesIfNotExists(String sqlFileName) throws Exception {
-    if (!JDBCUtils.tableExists(CORE_TABLE, dataSource)) {
-      try (InputStream is = getClass().getClassLoader().getResourceAsStream(sqlFileName);
-           Connection connection = dataSource.getConnection()) {
-        if (is == null) {
-          throw new IllegalStateException("DDL script not found on classpath: " + sqlFileName);
-        }
-        final String script = IOUtils.toString(is, UTF_8);
-        final StringBuilder stripped = new StringBuilder();
-        for (String line : script.split("\n")) {
-          if (!line.trim().startsWith("--")) {
-            stripped.append(line).append('\n');
-          }
-        }
-        for (String statement : stripped.toString().split(";")) {
-          final String trimmed = statement.trim();
-          if (!trimmed.isEmpty()) {
-            try (java.sql.Statement stmt = connection.createStatement()) {
-              stmt.execute(trimmed);
-            }
-          }
-        }
-      }
-    }
+    createTablesIfNotExist(databaseType.delegationPolicyTablesSql());
   }
 
   String insertPolicy(DelegationPolicy policy) throws SQLException {
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/DerbyDBDelegationPolicyService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/DerbyDBDelegationPolicyService.java
new file mode 100644
index 000000000..0446bc174
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/DerbyDBDelegationPolicyService.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.knox.gateway.services.knoxidf.delegation;
+
+import java.util.Map;
+
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.database.EmbeddedDerbyDatabase;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.security.MasterService;
+import org.apache.knox.gateway.services.token.impl.DerbyDBTokenStateService;
+
+/**
+ * A self-provisioning, embedded-Derby backed {@link DelegationPolicyService}. This is the
+ * auto-enabled default when KnoxIDF is deployed without an operator-configured external database,
+ * mirroring how {@link DerbyDBTokenStateService} is the default token-state service and
+ * {@code DerbyDBFederatedIdentityService}/{@code DerbyDBTrustedOidcIssuerService} are the default
+ * federated-identity and trusted-OIDC-issuer services.
+ * <p>
+ * It reuses the single embedded Derby database that the token-state service already provisions
+ * under {@code ${securityDir}/tokens} (the {@code ;create=true} JDBC URL is idempotent, so
+ * connecting to an already-booted database simply connects), sets the shared {@link GatewayConfig}
+ * to point at it, ensures the connection user/password aliases exist, and then delegates all
+ * persistence to {@link JdbcDelegationPolicyService} (which builds the
+ * {@link DelegationPolicyDatabase} and self-creates its tables).
+ */
+public class DerbyDBDelegationPolicyService extends JdbcDelegationPolicyService {
+
+  private EmbeddedDerbyDatabase embeddedDerbyDatabase;
+  private MasterService masterService;
+
+  public void setMasterService(MasterService masterService) {
+    this.masterService = masterService;
+  }
+
+  @Override
+  public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException {
+    try {
+      embeddedDerbyDatabase = new EmbeddedDerbyDatabase(config);
+      embeddedDerbyDatabase.start(config, getAliasService(), masterService);
+      super.init(config, options);
+    } catch (Exception e) {
+      throw new ServiceLifecycleException("Error while initiating DerbyDBDelegationPolicyService: " + e, e);
+    }
+  }
+
+  @Override
+  public void stop() throws ServiceLifecycleException {
+    if (embeddedDerbyDatabase != null) {
+      embeddedDerbyDatabase.stop();
+    }
+  }
+}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/JdbcDelegationPolicyService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/JdbcDelegationPolicyService.java
index fe1def125..8bb74765b 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/JdbcDelegationPolicyService.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/JdbcDelegationPolicyService.java
@@ -87,6 +87,10 @@ public class JdbcDelegationPolicyService implements DelegationPolicyService {
     this.aliasService = aliasService;
   }
 
+  protected AliasService getAliasService() {
+    return aliasService;
+  }
+
   @Override
   public DelegationPolicy register(DelegationPolicy policy) {
     try {
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/DerbyDBFederatedIdentityService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/DerbyDBFederatedIdentityService.java
index aa4bec22c..475b285e7 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/DerbyDBFederatedIdentityService.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/DerbyDBFederatedIdentityService.java
@@ -16,24 +16,13 @@
  */
 package org.apache.knox.gateway.services.knoxidf.federation;
 
-import static org.apache.knox.gateway.config.impl.GatewayConfigImpl.GATEWAY_DATABASE_NAME;
-import static org.apache.knox.gateway.config.impl.GatewayConfigImpl.GATEWAY_DATABASE_TYPE;
-import static org.apache.knox.gateway.database.AbstractDataSourceFactory.DATABASE_PASSWORD_ALIAS_NAME;
-import static org.apache.knox.gateway.database.AbstractDataSourceFactory.DATABASE_USER_ALIAS_NAME;
-import static org.apache.knox.gateway.database.DatabaseType.DERBY;
-import static org.apache.knox.gateway.services.security.AliasService.NO_CLUSTER_NAME;
-
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.Map;
-import java.util.concurrent.TimeUnit;
 
-import org.apache.hadoop.conf.Configuration;
 import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.database.EmbeddedDerbyDatabase;
 import org.apache.knox.gateway.services.ServiceLifecycleException;
 import org.apache.knox.gateway.services.security.MasterService;
 import org.apache.knox.gateway.services.token.impl.DerbyDBTokenStateService;
-import org.apache.knox.gateway.shell.jdbc.derby.DerbyDatabase;
 
 /**
  * A self-provisioning, embedded-Derby backed {@link FederatedIdentityService}. This is the
@@ -49,8 +38,7 @@ import org.apache.knox.gateway.shell.jdbc.derby.DerbyDatabase;
  */
 public class DerbyDBFederatedIdentityService extends JdbcFederatedIdentityService {
 
-  private DerbyDatabase derbyDatabase;
-  private Path derbyDatabaseFolder;
+  private EmbeddedDerbyDatabase embeddedDerbyDatabase;
   private MasterService masterService;
 
   public void setMasterService(MasterService masterService) {
@@ -60,42 +48,18 @@ public class DerbyDBFederatedIdentityService extends JdbcFederatedIdentityServic
   @Override
   public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException {
     try {
-      derbyDatabaseFolder = Paths.get(config.getGatewaySecurityDir(), DerbyDBTokenStateService.DB_NAME);
-      startDerby();
-      ((Configuration) config).set(GATEWAY_DATABASE_TYPE, DERBY.type());
-      ((Configuration) config).set(GATEWAY_DATABASE_NAME, derbyDatabaseFolder.toString());
-      getAliasService().addAliasForCluster(NO_CLUSTER_NAME, DATABASE_USER_ALIAS_NAME, getDatabaseUserName());
-      getAliasService().addAliasForCluster(NO_CLUSTER_NAME, DATABASE_PASSWORD_ALIAS_NAME, getDatabasePassword());
+      embeddedDerbyDatabase = new EmbeddedDerbyDatabase(config);
+      embeddedDerbyDatabase.start(config, getAliasService(), masterService);
       super.init(config, options);
     } catch (Exception e) {
       throw new ServiceLifecycleException("Error while initiating DerbyDBFederatedIdentityService: " + e, e);
     }
   }
 
-  private void startDerby() throws Exception {
-    derbyDatabase = new DerbyDatabase(derbyDatabaseFolder.toString());
-    derbyDatabase.create();
-    TimeUnit.SECONDS.sleep(1); // give a bit of time for the server to start
-  }
-
-  private String getDatabasePassword() throws Exception {
-    final char[] dbPasswordAliasValue = getAliasService().getPasswordFromAliasForGateway(DATABASE_PASSWORD_ALIAS_NAME);
-    return dbPasswordAliasValue != null ? new String(dbPasswordAliasValue) : new String(masterService.getMasterSecret());
-  }
-
-  private String getDatabaseUserName() throws Exception {
-    final char[] dbUserAliasValue = getAliasService().getPasswordFromAliasForGateway(DATABASE_USER_ALIAS_NAME);
-    return dbUserAliasValue != null ? new String(dbUserAliasValue) : DerbyDBTokenStateService.DEFAULT_TOKEN_DB_USER_NAME;
-  }
-
   @Override
   public void stop() throws ServiceLifecycleException {
-    try {
-      if (derbyDatabase != null) {
-        derbyDatabase.shutdown();
-      }
-    } catch (Exception e) {
-      throw new ServiceLifecycleException("Error while shutting down Derby Database", e);
+    if (embeddedDerbyDatabase != null) {
+      embeddedDerbyDatabase.stop();
     }
   }
 }
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/DerbyDBTrustedOidcIssuerService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/DerbyDBTrustedOidcIssuerService.java
index f1c73e9b3..3f0bbfc2e 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/DerbyDBTrustedOidcIssuerService.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/DerbyDBTrustedOidcIssuerService.java
@@ -16,24 +16,13 @@
  */
 package org.apache.knox.gateway.services.knoxidf.trustedoidcissuer;
 
-import static org.apache.knox.gateway.config.impl.GatewayConfigImpl.GATEWAY_DATABASE_NAME;
-import static org.apache.knox.gateway.config.impl.GatewayConfigImpl.GATEWAY_DATABASE_TYPE;
-import static org.apache.knox.gateway.database.AbstractDataSourceFactory.DATABASE_PASSWORD_ALIAS_NAME;
-import static org.apache.knox.gateway.database.AbstractDataSourceFactory.DATABASE_USER_ALIAS_NAME;
-import static org.apache.knox.gateway.database.DatabaseType.DERBY;
-import static org.apache.knox.gateway.services.security.AliasService.NO_CLUSTER_NAME;
-
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.Map;
-import java.util.concurrent.TimeUnit;
 
-import org.apache.hadoop.conf.Configuration;
 import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.database.EmbeddedDerbyDatabase;
 import org.apache.knox.gateway.services.ServiceLifecycleException;
 import org.apache.knox.gateway.services.security.MasterService;
 import org.apache.knox.gateway.services.token.impl.DerbyDBTokenStateService;
-import org.apache.knox.gateway.shell.jdbc.derby.DerbyDatabase;
 
 /**
  * A self-provisioning, embedded-Derby backed {@link TrustedOidcIssuerService}. This is the
@@ -50,8 +39,7 @@ import org.apache.knox.gateway.shell.jdbc.derby.DerbyDatabase;
  */
 public class DerbyDBTrustedOidcIssuerService extends JdbcTrustedOidcIssuerService {
 
-  private DerbyDatabase derbyDatabase;
-  private Path derbyDatabaseFolder;
+  private EmbeddedDerbyDatabase embeddedDerbyDatabase;
   private MasterService masterService;
 
   public void setMasterService(MasterService masterService) {
@@ -61,42 +49,18 @@ public class DerbyDBTrustedOidcIssuerService extends JdbcTrustedOidcIssuerServic
   @Override
   public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException {
     try {
-      derbyDatabaseFolder = Paths.get(config.getGatewaySecurityDir(), DerbyDBTokenStateService.DB_NAME);
-      startDerby();
-      ((Configuration) config).set(GATEWAY_DATABASE_TYPE, DERBY.type());
-      ((Configuration) config).set(GATEWAY_DATABASE_NAME, derbyDatabaseFolder.toString());
-      getAliasService().addAliasForCluster(NO_CLUSTER_NAME, DATABASE_USER_ALIAS_NAME, getDatabaseUserName());
-      getAliasService().addAliasForCluster(NO_CLUSTER_NAME, DATABASE_PASSWORD_ALIAS_NAME, getDatabasePassword());
+      embeddedDerbyDatabase = new EmbeddedDerbyDatabase(config);
+      embeddedDerbyDatabase.start(config, getAliasService(), masterService);
       super.init(config, options);
     } catch (Exception e) {
       throw new ServiceLifecycleException("Error while initiating DerbyDBTrustedOidcIssuerService: " + e, e);
     }
   }
 
-  private void startDerby() throws Exception {
-    derbyDatabase = new DerbyDatabase(derbyDatabaseFolder.toString());
-    derbyDatabase.create();
-    TimeUnit.SECONDS.sleep(1); // give a bit of time for the server to start
-  }
-
-  private String getDatabasePassword() throws Exception {
-    final char[] dbPasswordAliasValue = getAliasService().getPasswordFromAliasForGateway(DATABASE_PASSWORD_ALIAS_NAME);
-    return dbPasswordAliasValue != null ? new String(dbPasswordAliasValue) : new String(masterService.getMasterSecret());
-  }
-
-  private String getDatabaseUserName() throws Exception {
-    final char[] dbUserAliasValue = getAliasService().getPasswordFromAliasForGateway(DATABASE_USER_ALIAS_NAME);
-    return dbUserAliasValue != null ? new String(dbUserAliasValue) : DerbyDBTokenStateService.DEFAULT_TOKEN_DB_USER_NAME;
-  }
-
   @Override
   public void stop() throws ServiceLifecycleException {
-    try {
-      if (derbyDatabase != null) {
-        derbyDatabase.shutdown();
-      }
-    } catch (Exception e) {
-      throw new ServiceLifecycleException("Error while shutting down Derby Database", e);
+    if (embeddedDerbyDatabase != null) {
+      embeddedDerbyDatabase.stop();
     }
   }
 }
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DerbyDBTokenStateService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DerbyDBTokenStateService.java
index 471542b32..5d281856a 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DerbyDBTokenStateService.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DerbyDBTokenStateService.java
@@ -16,33 +16,16 @@
  */
 package org.apache.knox.gateway.services.token.impl;
 
-import static org.apache.knox.gateway.config.impl.GatewayConfigImpl.GATEWAY_DATABASE_NAME;
-import static org.apache.knox.gateway.config.impl.GatewayConfigImpl.GATEWAY_DATABASE_TYPE;
-import static org.apache.knox.gateway.services.security.AliasService.NO_CLUSTER_NAME;
-import static org.apache.knox.gateway.database.AbstractDataSourceFactory.DATABASE_PASSWORD_ALIAS_NAME;
-import static org.apache.knox.gateway.database.AbstractDataSourceFactory.DATABASE_USER_ALIAS_NAME;
-import static org.apache.knox.gateway.database.DatabaseType.DERBY;
-
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.Map;
-import java.util.concurrent.TimeUnit;
 
-import org.apache.hadoop.conf.Configuration;
 import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.database.EmbeddedDerbyDatabase;
 import org.apache.knox.gateway.services.ServiceLifecycleException;
 import org.apache.knox.gateway.services.security.MasterService;
-import org.apache.knox.gateway.shell.jdbc.derby.DerbyDatabase;
-import org.apache.knox.gateway.util.FileUtils;
 
 public class DerbyDBTokenStateService extends JDBCTokenStateService {
 
-  public static final String DEFAULT_TOKEN_DB_USER_NAME = "knox";
-  public static final String DB_NAME = "tokens";
-
-  private DerbyDatabase derbyDatabase;
-  private Path derbyDatabaseFolder;
+  private EmbeddedDerbyDatabase embeddedDerbyDatabase;
   private MasterService masterService;
 
   public void setMasterService(MasterService masterService) {
@@ -52,47 +35,18 @@ public class DerbyDBTokenStateService extends JDBCTokenStateService {
   @Override
   public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException {
     try {
-      derbyDatabaseFolder = Paths.get(config.getGatewaySecurityDir(), DB_NAME);
-      startDerby();
-      ((Configuration) config).set(GATEWAY_DATABASE_TYPE, DERBY.type());
-      ((Configuration) config).set(GATEWAY_DATABASE_NAME, derbyDatabaseFolder.toString());
-      getAliasService().addAliasForCluster(NO_CLUSTER_NAME, DATABASE_USER_ALIAS_NAME, getDatabaseUserName());
-      getAliasService().addAliasForCluster(NO_CLUSTER_NAME, DATABASE_PASSWORD_ALIAS_NAME, getDatabasePassword());
+      embeddedDerbyDatabase = new EmbeddedDerbyDatabase(config);
+      embeddedDerbyDatabase.start(config, getAliasService(), masterService);
       super.init(config, options);
-
-      // we need the "x" permission too to be able to browse that folder (600 is not enough)
-      if (Files.exists(derbyDatabaseFolder)) {
-        FileUtils.chmod("700", derbyDatabaseFolder.toFile());
-      }
     } catch (Exception e) {
       throw new ServiceLifecycleException("Error while initiating DerbyDBTokenStateService: " + e, e);
     }
   }
 
-  private void startDerby() throws Exception {
-    derbyDatabase = new DerbyDatabase(derbyDatabaseFolder.toString());
-    derbyDatabase.create();
-    TimeUnit.SECONDS.sleep(1); // give a bit of time for the server to start
-  }
-
-  private String getDatabasePassword() throws Exception {
-    final char[] dbPasswordAliasValue = getAliasService().getPasswordFromAliasForGateway(DATABASE_PASSWORD_ALIAS_NAME);
-    return dbPasswordAliasValue != null ? new String(dbPasswordAliasValue) : new String(masterService.getMasterSecret());
-  }
-
-  private String getDatabaseUserName() throws Exception {
-    final char[] dbUserAliasValue = getAliasService().getPasswordFromAliasForGateway(DATABASE_USER_ALIAS_NAME);
-    return dbUserAliasValue != null ? new String(dbUserAliasValue) : DEFAULT_TOKEN_DB_USER_NAME;
-  }
-
   @Override
   public void stop() throws ServiceLifecycleException {
-    try {
-      if (derbyDatabase != null) {
-        derbyDatabase.shutdown();
-      }
-    } catch (Exception e) {
-      throw new ServiceLifecycleException("Error while shutting down Derby Database", e);
+    if (embeddedDerbyDatabase != null) {
+      embeddedDerbyDatabase.stop();
     }
   }
 
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/database/KnoxDatabaseTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/database/KnoxDatabaseTest.java
new file mode 100644
index 000000000..b5b3f8a70
--- /dev/null
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/database/KnoxDatabaseTest.java
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.knox.gateway.database;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import java.sql.Connection;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import org.hsqldb.jdbc.JDBCDataSource;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Exercises {@link KnoxDatabase#createTablesIfNotExist(String)} and its
+ * {@link KnoxDatabase#parseCreateTableStatements(String)} helper directly against an in-memory HSQL
+ * database, using the standard (PostgreSQL/HSQL) KnoxIDF delegation-policy DDL as a representative
+ * multi-statement, foreign-key-ordered script.
+ */
+public class KnoxDatabaseTest {
+
+  public static final String USER = "sa";
+  public static final String PASSWORD = "";
+
+  private static final String DDL = AbstractDataSourceFactory.KNOXIDF_DELEGATION_POLICY_TABLES_SQL;
+
+  // Parent-first: matches the order the statements appear in the DDL and the order they must be
+  // created in to satisfy the foreign keys.
+  private static final List<String> EXPECTED_TABLES = Arrays.asList(
+      "DELEGATION_POLICIES",
+      "DELEGATION_POLICY_USERS",
+      "DELEGATION_POLICY_GROUPS",
+      "DELEGATION_POLICY_RESOURCES",
+      "DELEGATION_POLICY_RESOURCE_SCOPES");
+
+  private static JDBCDataSource dataSource;
+  private KnoxDatabase db;
+
+  @BeforeClass
+  public static void setUpClass() {
+    dataSource = new JDBCDataSource();
+    dataSource.setDatabaseName("knox_database_test");
+    dataSource.setUser(USER);
+    dataSource.setPassword(PASSWORD);
+    dataSource.setUrl("jdbc:hsqldb:mem:knoxdatabasetest;sql.syntax_pgs=true"); // sql.syntax_pgs => use postgres syntax
+  }
+
+  @Before
+  public void setUp() {
+    db = new KnoxDatabase(dataSource);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    // Child-first so the foreign keys don't block the drops; leaves a clean schema for the next test.
+    final List<String> reversed = new ArrayList<>(EXPECTED_TABLES);
+    java.util.Collections.reverse(reversed);
+    try (Connection connection = dataSource.getConnection(USER, PASSWORD);
+         Statement statement = connection.createStatement()) {
+      for (String table : reversed) {
+        statement.execute("DROP TABLE IF EXISTS " + table);
+      }
+    }
+  }
+
+  @AfterClass
+  public static void tearDownClass() throws Exception {
+    try (Connection connection = dataSource.getConnection(USER, PASSWORD);
+         Statement statement = connection.createStatement()) {
+      statement.execute("SHUTDOWN");
+    }
+  }
+
+  @Test
+  public void shouldParseAllCreateStatementsInOrderWithoutLicenseHeader() throws Exception {
+    final Map<String, String> statements = db.parseCreateTableStatements(DDL);
+
+    assertEquals(EXPECTED_TABLES, new ArrayList<>(statements.keySet()));
+    for (Map.Entry<String, String> entry : statements.entrySet()) {
+      assertTrue("statement for " + entry.getKey() + " should start with CREATE TABLE",
+          entry.getValue().toUpperCase(java.util.Locale.ROOT).startsWith("CREATE TABLE"));
+      assertFalse("license header should have been stripped",
+          entry.getValue().contains("Licensed to the Apache"));
+    }
+  }
+
+  @Test
+  public void shouldCreateAllTables() throws Exception {
+    for (String table : EXPECTED_TABLES) {
+      assertFalse(table + " should not exist before creation", JDBCUtils.tableExists(table, dataSource));
+    }
+
+    db.createTablesIfNotExist(DDL);
+
+    for (String table : EXPECTED_TABLES) {
+      assertTrue(table + " should exist after creation", JDBCUtils.tableExists(table, dataSource));
+    }
+  }
+
+  @Test
+  public void shouldBeIdempotentWhenAllTablesAlreadyExist() throws Exception {
+    db.createTablesIfNotExist(DDL);
+    // A second run must not fail even though every table is already present.
+    db.createTablesIfNotExist(DDL);
+
+    for (String table : EXPECTED_TABLES) {
+      assertTrue(table + " should still exist", JDBCUtils.tableExists(table, dataSource));
+    }
+  }
+
+  @Test
+  public void shouldRecreateOnlyMissingTables() throws Exception {
+    db.createTablesIfNotExist(DDL);
+
+    // Drop a leaf table (nothing references it) to simulate a partially provisioned schema.
+    final String leaf = "DELEGATION_POLICY_RESOURCE_SCOPES";
+    try (Connection connection = dataSource.getConnection(USER, PASSWORD);
+         Statement statement = connection.createStatement()) {
+      statement.execute("DROP TABLE " + leaf);
+    }
+    assertFalse(JDBCUtils.tableExists(leaf, dataSource));
+
+    db.createTablesIfNotExist(DDL);
+
+    for (String table : EXPECTED_TABLES) {
+      assertTrue(table + " should exist after re-provisioning", JDBCUtils.tableExists(table, dataSource));
+    }
+  }
+
+  @Test
+  public void shouldFailWhenDdlScriptIsMissing() {
+    assertThrows(IllegalStateException.class, () -> db.parseCreateTableStatements("does-not-exist.sql"));
+  }
+}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/factory/DelegationPolicyServiceFactoryTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/factory/DelegationPolicyServiceFactoryTest.java
index 1cec419e3..eba048a01 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/services/factory/DelegationPolicyServiceFactoryTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/factory/DelegationPolicyServiceFactoryTest.java
@@ -22,21 +22,23 @@ import org.apache.knox.gateway.database.DatabaseType;
 import org.apache.knox.gateway.services.GatewayServices;
 import org.apache.knox.gateway.services.Service;
 import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.services.knoxidf.delegation.DerbyDBDelegationPolicyService;
 import org.apache.knox.gateway.services.knoxidf.delegation.EmptyDelegationPolicyService;
 import org.apache.knox.gateway.services.knoxidf.delegation.JdbcDelegationPolicyService;
 import org.apache.knox.gateway.services.knoxidf.delegation.PolicyCheckRequest;
 import org.apache.knox.gateway.services.knoxidf.delegation.PolicyDecision;
 import org.apache.knox.gateway.services.knoxidf.delegation.DelegationPolicyService;
 import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.services.security.MasterService;
 import org.apache.knox.gateway.services.topology.TopologyService;
 import org.apache.knox.gateway.topology.Topology;
+import org.apache.knox.test.TestUtils;
 import org.easymock.EasyMock;
 import org.junit.After;
-import org.junit.BeforeClass;
 import org.junit.Test;
 
 import java.io.File;
-import java.sql.DriverManager;
+import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
@@ -53,13 +55,6 @@ public class DelegationPolicyServiceFactoryTest {
   private File tempDir;
   private Service createdService;
 
-  @BeforeClass
-  public static void setUpClass() throws Exception {
-    java.util.Locale.setDefault(java.util.Locale.US);
-    DriverManager.getConnection("jdbc:derby:memory:DelegationPolicyServiceFactoryTest_knoxidf;create=true").close();
-    DriverManager.getConnection("jdbc:derby:memory:DelegationPolicyServiceFactoryTest_knoxidf_admin;create=true").close();
-  }
-
   @After
   public void tearDown() throws Exception {
     if (createdService != null) {
@@ -103,69 +98,82 @@ public class DelegationPolicyServiceFactoryTest {
   }
 
   // ------------------------------------------------------------------
-  // JDBC when KNOXIDF deployed
+  // Auto-implementation selection
   // ------------------------------------------------------------------
 
   @Test
-  public void shouldSelectJdbcWhenKnoxIdfDeployed() throws Exception {
-    tempDir = org.apache.knox.test.TestUtils.createTempDir(getClass().getName());
-    final AliasService aliasService = EasyMock.createNiceMock(AliasService.class);
-    EasyMock.replay(aliasService);
-
-    final TopologyService topologyService = EasyMock.createNiceMock(TopologyService.class);
-    EasyMock.expect(topologyService.getTopologies()).andReturn(
-        Collections.singletonList(topologyWithRole("KNOXIDF"))).anyTimes();
-    EasyMock.replay(topologyService);
-
-    final GatewayServices gws = EasyMock.createNiceMock(GatewayServices.class);
-    EasyMock.expect(gws.getService(ServiceType.TOPOLOGY_SERVICE)).andReturn(topologyService).anyTimes();
-    EasyMock.expect(gws.getService(ServiceType.ALIAS_SERVICE)).andReturn(aliasService).anyTimes();
-    EasyMock.replay(gws);
+  public void shouldChooseDerbyWhenNoDatabaseConfigured() {
+    final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class);
+    EasyMock.expect(config.getDatabaseType()).andReturn("none").anyTimes();
+    EasyMock.replay(config);
+    assertEquals(DerbyDBDelegationPolicyService.class.getName(), factory.chooseAutoImplementation(config));
+  }
 
+  @Test
+  public void shouldChooseDerbyWhenDatabaseTypeIsDerby() {
     final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class);
     EasyMock.expect(config.getDatabaseType()).andReturn(DatabaseType.DERBY.type()).anyTimes();
-    EasyMock.expect(config.getDatabaseName())
-        .andReturn("memory:" + getClass().getSimpleName() + "_knoxidf").anyTimes();
-    EasyMock.expect(config.getDelegationServiceTokenTtlSec()).andReturn(3600).anyTimes();
-    EasyMock.expect(config.getDelegationServiceListMaxTotal()).andReturn(
-         org.apache.knox.gateway.config.GatewayConfig.DELEGATION_SERVICE_LIST_MAX_TOTAL_DEFAULT).anyTimes();
-    EasyMock.expect(config.getDelegationServiceListMaxPerAuthority()).andReturn(
-         org.apache.knox.gateway.config.GatewayConfig.DELEGATION_SERVICE_LIST_MAX_PER_AUTHORITY_DEFAULT).anyTimes();
     EasyMock.replay(config);
+    assertEquals(DerbyDBDelegationPolicyService.class.getName(), factory.chooseAutoImplementation(config));
+  }
 
-    createdService = factory.create(gws, ServiceType.DELEGATION_POLICY_SERVICE, config, options, "");
+  @Test
+  public void shouldChooseJdbcWhenExternalDatabaseConfigured() {
+    final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class);
+    EasyMock.expect(config.getDatabaseType()).andReturn(DatabaseType.POSTGRESQL.type()).anyTimes();
+    EasyMock.replay(config);
+    assertEquals(JdbcDelegationPolicyService.class.getName(), factory.chooseAutoImplementation(config));
+  }
+
+  // ------------------------------------------------------------------
+  // Derby auto-provisioning
+  // ------------------------------------------------------------------
+
+  @Test
+  public void shouldAutoSelectDerbyServiceWhenKnoxIdfDeployedWithoutExternalDatabase() throws Exception {
+    createdService = createAutoProvisionedDerbyService(topologyWithRole("KNOXIDF"));
     assertNotNull(createdService);
-    assertTrue("Expected JdbcDelegationPolicyService when KNOXIDF is deployed, got "
-        + createdService.getClass().getName(), createdService instanceof JdbcDelegationPolicyService);
+    assertTrue("Expected a self-provisioning Derby-backed delegation policy service, got "
+        + createdService.getClass().getName(), createdService instanceof DerbyDBDelegationPolicyService);
   }
 
   @Test
-  public void shouldSelectJdbcWhenKnoxIdfAdminDeployed() throws Exception {
-    tempDir = org.apache.knox.test.TestUtils.createTempDir(getClass().getName());
+  public void shouldAutoSelectDerbyServiceWhenKnoxIdfAdminDeployedWithoutExternalDatabase() throws Exception {
+    createdService = createAutoProvisionedDerbyService(topologyWithRole("KNOXIDF_ADMIN"));
+    assertNotNull(createdService);
+    assertTrue("Expected a self-provisioning Derby-backed delegation policy service, got "
+        + createdService.getClass().getName(), createdService instanceof DerbyDBDelegationPolicyService);
+  }
+
+  private Service createAutoProvisionedDerbyService(Topology topology) throws Exception {
+    tempDir = TestUtils.createTempDir(this.getClass().getName());
+    final MasterService masterService = EasyMock.createNiceMock(MasterService.class);
+    EasyMock.expect(masterService.getMasterSecret()).andReturn("M4st3RSecret!".toCharArray()).anyTimes();
+    EasyMock.replay(masterService);
     final AliasService aliasService = EasyMock.createNiceMock(AliasService.class);
     EasyMock.replay(aliasService);
 
     final TopologyService topologyService = EasyMock.createNiceMock(TopologyService.class);
-    EasyMock.expect(topologyService.getTopologies()).andReturn(
-        Collections.singletonList(topologyWithRole("KNOXIDF_ADMIN"))).anyTimes();
+    EasyMock.expect(topologyService.getTopologies()).andReturn(Collections.singletonList(topology)).anyTimes();
     EasyMock.replay(topologyService);
-
     final GatewayServices gws = EasyMock.createNiceMock(GatewayServices.class);
     EasyMock.expect(gws.getService(ServiceType.TOPOLOGY_SERVICE)).andReturn(topologyService).anyTimes();
     EasyMock.expect(gws.getService(ServiceType.ALIAS_SERVICE)).andReturn(aliasService).anyTimes();
+    EasyMock.expect(gws.getService(ServiceType.MASTER_SERVICE)).andReturn(masterService).anyTimes();
     EasyMock.replay(gws);
 
     final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class);
     EasyMock.expect(config.getDatabaseType()).andReturn(DatabaseType.DERBY.type()).anyTimes();
-    EasyMock.expect(config.getDatabaseName())
-        .andReturn("memory:" + getClass().getSimpleName() + "_knoxidf_admin").anyTimes();
+    EasyMock.expect(config.getGatewaySecurityDir()).andReturn(tempDir.getAbsolutePath()).anyTimes();
+    EasyMock.expect(config.getDatabaseName()).andReturn(Paths.get(tempDir.getAbsolutePath(), "tokens").toString()).anyTimes();
     EasyMock.expect(config.getDelegationServiceTokenTtlSec()).andReturn(3600).anyTimes();
+    EasyMock.expect(config.getDelegationServiceListMaxTotal()).andReturn(
+        org.apache.knox.gateway.config.GatewayConfig.DELEGATION_SERVICE_LIST_MAX_TOTAL_DEFAULT).anyTimes();
+    EasyMock.expect(config.getDelegationServiceListMaxPerAuthority()).andReturn(
+        org.apache.knox.gateway.config.GatewayConfig.DELEGATION_SERVICE_LIST_MAX_PER_AUTHORITY_DEFAULT).anyTimes();
     EasyMock.replay(config);
 
-    createdService = factory.create(gws, ServiceType.DELEGATION_POLICY_SERVICE, config, options, "");
-    assertNotNull(createdService);
-    assertTrue("Expected JdbcDelegationPolicyService when KNOXIDF_ADMIN is deployed, got "
-        + createdService.getClass().getName(), createdService instanceof JdbcDelegationPolicyService);
+    return factory.create(gws, ServiceType.DELEGATION_POLICY_SERVICE, config, options, "");
   }
 
   // ------------------------------------------------------------------
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/factory/ServiceFactoryTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/factory/ServiceFactoryTest.java
index 213370f3e..0854e4add 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/services/factory/ServiceFactoryTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/factory/ServiceFactoryTest.java
@@ -22,6 +22,8 @@ import org.apache.commons.lang3.reflect.FieldUtils;
 import org.apache.knox.gateway.config.impl.GatewayConfigImpl;
 import org.apache.knox.gateway.database.AbstractDataSourceFactory;
 import org.apache.knox.gateway.database.DatabaseType;
+import org.apache.knox.gateway.database.DerbyDatabaseCredentials;
+import org.apache.knox.gateway.database.EmbeddedDerbyDatabase;
 import org.apache.knox.gateway.services.GatewayServices;
 import org.apache.knox.gateway.services.Service;
 import org.apache.knox.gateway.services.ServiceLifecycleException;
@@ -32,7 +34,6 @@ import org.apache.knox.gateway.services.security.AliasService;
 import org.apache.knox.gateway.services.security.AliasServiceException;
 import org.apache.knox.gateway.services.security.KeystoreService;
 import org.apache.knox.gateway.services.security.MasterService;
-import org.apache.knox.gateway.services.token.impl.DerbyDBTokenStateService;
 import org.apache.knox.test.TestUtils;
 import org.easymock.EasyMock;
 import org.junit.After;
@@ -82,18 +83,18 @@ class ServiceFactoryTest {
     final AliasService aliasService = EasyMock.createNiceMock(AliasService.class);
     if (expectDbCredentialLookup) {
       try {
-        aliasService.addAliasForCluster(NO_CLUSTER_NAME, AbstractDataSourceFactory.DATABASE_USER_ALIAS_NAME, DerbyDBTokenStateService.DEFAULT_TOKEN_DB_USER_NAME);
+        aliasService.addAliasForCluster(NO_CLUSTER_NAME, AbstractDataSourceFactory.DATABASE_USER_ALIAS_NAME, DerbyDatabaseCredentials.DEFAULT_DB_USER_NAME);
         EasyMock.expectLastCall().anyTimes();
         aliasService.addAliasForCluster(NO_CLUSTER_NAME, AbstractDataSourceFactory.DATABASE_PASSWORD_ALIAS_NAME, masterSecret);
         EasyMock.expectLastCall().anyTimes();
-        expect(aliasService.getPasswordFromAliasForGateway(AbstractDataSourceFactory.DATABASE_USER_ALIAS_NAME)).andReturn(DerbyDBTokenStateService.DEFAULT_TOKEN_DB_USER_NAME.toCharArray()).anyTimes();
+        expect(aliasService.getPasswordFromAliasForGateway(AbstractDataSourceFactory.DATABASE_USER_ALIAS_NAME)).andReturn(DerbyDatabaseCredentials.DEFAULT_DB_USER_NAME.toCharArray()).anyTimes();
         expect(aliasService.getPasswordFromAliasForGateway(AbstractDataSourceFactory.DATABASE_PASSWORD_ALIAS_NAME)).andReturn(masterSecret.toCharArray()).anyTimes();
 
         // prepare GatewayConfig
         expect(gatewayConfig.getDatabaseType()).andReturn(DatabaseType.DERBY.type()).anyTimes();
         tempDbFolder = TestUtils.createTempDir(this.getClass().getName());
         expect(gatewayConfig.getGatewaySecurityDir()).andReturn(tempDbFolder.getAbsolutePath()).anyTimes();
-        expect(gatewayConfig.getDatabaseName()).andReturn(Paths.get(tempDbFolder.getAbsolutePath(), DerbyDBTokenStateService.DB_NAME).toString()).anyTimes();
+        expect(gatewayConfig.getDatabaseName()).andReturn(Paths.get(tempDbFolder.getAbsolutePath(), EmbeddedDerbyDatabase.DB_NAME).toString()).anyTimes();
       } catch (AliasServiceException | IOException e) {
         // NOP
       }