diff --git a/dev-support/ranger-docker/Dockerfile.ranger b/dev-support/ranger-docker/Dockerfile.ranger
index 875329bb80..a01a3f6804 100644
--- a/dev-support/ranger-docker/Dockerfile.ranger
+++ b/dev-support/ranger-docker/Dockerfile.ranger
@@ -25,12 +25,19 @@ ARG TARGETARCH
COPY ./dist/ranger-${RANGER_VERSION}-admin.tar.gz /home/ranger/dist/
COPY ./scripts/admin/ranger.sh ${RANGER_SCRIPTS}/
-COPY ./scripts/admin/create-ranger-services.py ${RANGER_SCRIPTS}/
+COPY ./scripts/admin/user_password_bootstrap.py ${RANGER_SCRIPTS}/
+COPY ./scripts/python/log_config.py ${RANGER_SCRIPTS}/
+COPY ./scripts/admin/dba.py ${RANGER_SCRIPTS}/
+COPY ./scripts/admin/ranger_admin_xml_config.py ${RANGER_SCRIPTS}/
+COPY ./scripts/admin/create-ranger-services.py ${RANGER_SCRIPTS}/create_services.py
RUN tar xvfz /home/ranger/dist/ranger-${RANGER_VERSION}-admin.tar.gz --directory=${RANGER_HOME} \
&& ln -s ${RANGER_HOME}/ranger-${RANGER_VERSION}-admin ${RANGER_HOME}/admin \
&& rm -f /home/ranger/dist/ranger-${RANGER_VERSION}-admin.tar.gz \
&& rm -f /opt/ranger/admin/install.properties \
+ && rm -f /opt/ranger/admin/setup.sh \
+ && rm -f /opt/ranger/admin/dba_script.py \
+ && rm -f /opt/ranger/admin/db_setup.py \
&& mkdir -p /var/run/ranger /var/log/ranger /usr/share/java/ \
&& chown -R ranger:ranger ${RANGER_HOME}/admin/ ${RANGER_SCRIPTS}/ /var/run/ranger/ /var/log/ranger/ \
&& chmod 755 ${RANGER_SCRIPTS}/ranger.sh
diff --git a/dev-support/ranger-docker/docker-compose.ranger.yml b/dev-support/ranger-docker/docker-compose.ranger.yml
index 3d92f5e065..354ad8915e 100644
--- a/dev-support/ranger-docker/docker-compose.ranger.yml
+++ b/dev-support/ranger-docker/docker-compose.ranger.yml
@@ -17,7 +17,9 @@ services:
- ./dist/version:/home/ranger/dist/version:ro
- ./scripts/kdc/krb5.conf:/etc/krb5.conf:ro
- ./scripts/hadoop/core-site.xml:/home/ranger/scripts/core-site.xml:ro
- - ./scripts/admin/ranger-admin-install-${RANGER_DB_TYPE}.properties:/opt/ranger/admin/install.properties
+ - ./scripts/admin/core-site.xml:/opt/ranger/admin/configs/core-site.xml:ro
+ - ./scripts/admin/ranger-admin-site.xml:/opt/ranger/admin/configs/ranger-admin-site.xml:ro
+ - ./scripts/admin/ranger-admin-default-site.xml:/opt/ranger/admin/configs/ranger-admin-default-site.xml:ro
stdin_open: true
tty: true
networks:
@@ -38,6 +40,10 @@ services:
- RANGER_DB_TYPE
- KERBEROS_ENABLED
- DEBUG_ADMIN=${DEBUG_ADMIN:-false}
+ - RANGER_ADMIN_DB_PASSWORD=rangerR0cks!
+ - RANGER_ADMIN_PASSWORD=rangerR0cks!
+ - RANGER_USERSYNC_PASSWORD=rangerR0cks!
+ - RANGER_TAGSYNC_PASSWORD=rangerR0cks!
command:
- /home/ranger/scripts/ranger.sh
diff --git a/dev-support/ranger-docker/scripts/admin/core-site.xml b/dev-support/ranger-docker/scripts/admin/core-site.xml
new file mode 100644
index 0000000000..89dc1c2a3c
--- /dev/null
+++ b/dev-support/ranger-docker/scripts/admin/core-site.xml
@@ -0,0 +1,47 @@
+
+
+
+
+ hadoop.security.authentication
+ simple
+
+
+ hadoop.security.authorization
+ true
+
+
+ fs.defaultFS
+ hdfs://localhost:9000
+
+
+ hadoop.rpc.protection
+ authentication
+
+
+ hadoop.security.key.provider.path
+ kms://http@localhost:9292/kms
+
+
+ zookeeper.quorum
+ localhost:2181
+
+
+ cluster.name
+ dev
+
+
diff --git a/dev-support/ranger-docker/scripts/admin/create-ranger-services.py b/dev-support/ranger-docker/scripts/admin/create-ranger-services.py
index 68d9b915e3..a94346786d 100644
--- a/dev-support/ranger-docker/scripts/admin/create-ranger-services.py
+++ b/dev-support/ranger-docker/scripts/admin/create-ranger-services.py
@@ -1,16 +1,17 @@
from apache_ranger.model.ranger_service import RangerService
-from apache_ranger.client.ranger_client import RangerClient
from json import JSONDecodeError
-ranger_client = RangerClient('http://ranger:6080', ('admin', 'rangerR0cks!'))
+from log_config import configure_logging, get_logger
+from ranger_admin_xml_config import get_ranger_client
+logger = get_logger(__name__)
-def service_not_exists(service):
+def service_not_exists(ranger_client, service):
try:
svc = ranger_client.get_service(service.name)
except JSONDecodeError:
- return 1
- return 0 if svc is not None else 1
+ return True
+ return svc is None
hdfs = RangerService({'name': 'dev_hdfs', 'type': 'hdfs',
@@ -148,11 +149,24 @@ def service_not_exists(service):
'ranger.plugin.super.users': 'solr',
'ranger.plugin.solr.policy.refresh.synchronous':'true'}})
-services = [hdfs, yarn, hive, hbase, kafka, knox, kms, trino, ozone, solr]
-for service in services:
- try:
- if service_not_exists(service):
- ranger_client.create_service(service)
- print(f" {service.name} service created!")
- except Exception as e:
- print(f"An exception occured: {e}")
+def main() -> int:
+ configure_logging()
+ ranger_client = get_ranger_client()
+ services = [hdfs, yarn, hive, hbase, kafka, knox, kms, trino, ozone, solr]
+
+ for service in services:
+ try:
+ if service_not_exists(ranger_client, service):
+ ranger_client.create_service(service)
+ logger.info("%s service created", service.name)
+ else:
+ logger.info("%s service already exists", service.name)
+ except Exception:
+ logger.exception("Failed to reconcile Ranger service %s", service.name)
+ return 1
+
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/dev-support/ranger-docker/scripts/admin/dba.py b/dev-support/ranger-docker/scripts/admin/dba.py
new file mode 100644
index 0000000000..32cdf84ea0
--- /dev/null
+++ b/dev-support/ranger-docker/scripts/admin/dba.py
@@ -0,0 +1,515 @@
+#
+# Licensed 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
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# 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. See accompanying LICENSE file.
+#
+
+import os
+import re
+import shlex
+import shutil
+import subprocess
+import sys
+import hashlib
+from dataclasses import dataclass
+
+from log_config import configure_logging, get_logger
+from ranger_admin_xml_config import load_ranger_admin_site_properties, parse_jdbc_url
+
+logger = get_logger(__name__)
+
+JISQL_DEBUG = True
+
+# Regex used to extract the database name from a JDBC override URL.
+_JDBC_DB_NAME_RE = re.compile(r"jdbc:postgresql://[^/]+:\d+/(\w+)")
+
+# Allowlist for sequence names interpolated into SQL commands.
+_SAFE_IDENTIFIER_RE = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
+
+RANGER_HOME = os.getenv("RANGER_HOME")
+
+# ---------------------------------------------------------------------------
+# Custom exceptions
+# ---------------------------------------------------------------------------
+class ConfigError(RuntimeError):
+ """Raised when required configuration is missing or invalid."""
+
+
+class ConnectionError(RuntimeError): # noqa: A001 — shadows builtin intentionally
+ """Raised when the database connection check fails."""
+
+
+class SchemaImportError(RuntimeError):
+ """Raised when a schema file import fails."""
+
+
+# ---------------------------------------------------------------------------
+# SSL configuration dataclass
+# ---------------------------------------------------------------------------
+@dataclass
+class SSLConfig:
+ """Holds all SSL-related settings with sensible defaults."""
+
+ enabled: bool = False
+ required: bool = False
+ verify_server_certificate: bool = False
+ auth_type: str = "2-way"
+ certificate_file: str = ""
+ key_store: str = ""
+ key_store_password: str = ""
+ key_store_type: str = "bcfks"
+ trust_store: str = ""
+ trust_store_password: str = ""
+ trust_store_type: str = "bcfks"
+ override_jdbc: bool = False
+ override_jdbc_connection_string: str = ""
+
+
+def _is_true(config: dict, key: str) -> bool:
+ """Return True if *config[key]* is the string 'true' (case-insensitive)."""
+ return config.get(key, "false").lower() == "true"
+
+
+def _extract_ssl_config(config: dict) -> SSLConfig:
+ """Build an SSLConfig from the raw config dictionary."""
+
+ ssl = SSLConfig()
+ ssl.override_jdbc = _is_true(config, "is_override_db_connection_string")
+ ssl.override_jdbc_connection_string = config.get("db_override_connection_string", "").strip()
+ ssl.enabled = _is_true(config, "db_ssl_enabled")
+
+ if not ssl.enabled:
+ return ssl
+
+ ssl.required = _is_true(config, "db_ssl_required")
+ ssl.verify_server_certificate = _is_true(config, "db_ssl_verifyServerCertificate")
+ ssl.auth_type = config.get("db_ssl_auth_type", "2-way").lower()
+ ssl.certificate_file = config.get("db_ssl_certificate_file", "")
+ ssl.trust_store = config.get("javax_net_ssl_trustStore", "")
+ ssl.trust_store_password = config.get("javax_net_ssl_trustStorePassword", "")
+ ssl.trust_store_type = config.get("javax_net_ssl_trustStore_type", "bcfks")
+ ssl.key_store = config.get("javax_net_ssl_keyStore", "")
+ ssl.key_store_password = config.get("javax_net_ssl_keyStorePassword", "")
+ ssl.key_store_type = config.get("javax_net_ssl_keyStore_type", "bcfks")
+
+ return ssl
+
+
+def _validate_ssl_files(ssl: SSLConfig) -> None:
+ """Validate that required SSL files and passwords exist.
+
+ Raises ConfigError instead of calling sys.exit so callers
+ can handle the error or let it propagate to main.
+ """
+ if not ssl.enabled or not ssl.verify_server_certificate:
+ return
+
+ if ssl.certificate_file:
+ if not os.path.exists(ssl.certificate_file):
+ raise ConfigError(f"SSL certificate file not found: {ssl.certificate_file}")
+ elif ssl.auth_type == "1-way":
+ if not os.path.exists(ssl.trust_store):
+ raise ConfigError(f"SSL truststore file not found: {ssl.trust_store}")
+ if not ssl.trust_store_password:
+ raise ConfigError("SSL truststore password is not set")
+
+ if ssl.auth_type == "2-way":
+ if not os.path.exists(ssl.key_store):
+ raise ConfigError(f"SSL keystore file not found: {ssl.key_store}")
+ if not ssl.key_store_password:
+ raise ConfigError("SSL keystore password is not set")
+
+
+# ---------------------------------------------------------------------------
+# Helper utilities
+# ---------------------------------------------------------------------------
+def _run_command(query: str) -> str:
+ """Run a shell command and return its stdout."""
+ result = subprocess.run(shlex.split(query), stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False)
+ return result.stdout
+
+
+def _log_jisql(query: str, db_password: str) -> None:
+ """Log a Jisql command with the password masked."""
+ if JISQL_DEBUG:
+ logger.info("JISQL %s", query.replace(f" -p '{db_password}'", " -p '********'"))
+
+
+def _validate_identifier(name: str) -> str:
+ """Return *name* if it matches a strict SQL identifier pattern.
+
+ Prevents accidental SQL injection when sequence/table names are
+ interpolated into command strings.
+ """
+ if not _SAFE_IDENTIFIER_RE.match(name):
+ raise ValueError(f"Unsafe SQL identifier rejected: {name!r}")
+ return name
+
+
+def load_runtime_config() -> dict:
+ """Load runtime config using XML for JDBC metadata and env for secrets.
+
+ Raises ConfigError if required settings are missing.
+ """
+ props = load_ranger_admin_site_properties()
+ if not props:
+ raise ConfigError(f"ranger-admin-site.xml not found or unreadable, RANGER_HOME={RANGER_HOME}")
+
+ jdbc_url = props.get("ranger.jpa.jdbc.url", "")
+ jdbc_info = parse_jdbc_url(jdbc_url)
+
+ db_flavor = (os.environ.get("RANGER_DB_TYPE") or jdbc_info.get("flavor") or "POSTGRES").upper()
+ if db_flavor == "POSTGRESQL":
+ db_flavor = "POSTGRES"
+
+ db_host = os.environ.get("RANGER_ADMIN_DB_HOSTNAME") or jdbc_info.get("host", "")
+ db_port = os.environ.get("RANGER_ADMIN_DB_PORT") or jdbc_info.get("port", "")
+ db_name = os.environ.get("RANGER_ADMIN_DB_DATABASE") or jdbc_info.get("database", "")
+ db_user = os.environ.get("RANGER_ADMIN_DB_USERNAME") or props.get("ranger.jpa.jdbc.user", "")
+ db_password = os.environ.get("RANGER_ADMIN_DB_PASSWORD", "")
+
+ if not all([db_host, db_name, db_user]):
+ raise ConfigError("Required JDBC settings (host/name/user) missing from ranger-admin-site.xml")
+ if not db_password:
+ raise ConfigError("Required env var RANGER_ADMIN_DB_PASSWORD is not set")
+
+ config = dict(props)
+ config["DB_FLAVOR"] = db_flavor
+ config["SQL_CONNECTOR_JAR"] = props.get("ranger.jdbc.sqlconnectorjar", "/usr/share/java/postgresql.jar")
+ config["db_name"] = db_name
+ config["db_host"] = f"{db_host}:{db_port}" if db_port else db_host
+ config["db_user"] = db_user
+ config["db_password"] = db_password
+ config["postgres_core_file"] = "db/postgres/optimized/current/ranger_core_db_postgres.sql"
+ return config
+
+
+# ---------------------------------------------------------------------------
+# Database abstraction
+# ---------------------------------------------------------------------------
+class BaseDB:
+ """Interface that every DB flavour must implement."""
+
+ def check_connection(self, db_name, db_user, db_password):
+ logger.info("---------- Verifying DB connection ----------")
+
+ def check_table(self, db_name, db_user, db_password, table_name):
+ logger.info("---------- Verifying table ----------")
+
+ def import_db_file(self, db_name, db_user, db_password, file_name):
+ logger.info("---------- Importing db schema ----------")
+
+
+class PostgresDB(BaseDB):
+ """PostgreSQL-specific implementation of the DB bootstrap interface."""
+
+ def __init__(self, *, host: str, sql_connector_jar: str, java_bin: str, ssl: SSLConfig):
+ self.host = host
+ self.sql_connector_jar = sql_connector_jar
+ self.java_bin = java_bin.strip("'")
+ self.ssl = ssl
+
+ # -- private helpers ---------------------------------------------------
+
+ def _resolve_db_name(self, db_name: str) -> str:
+ """Extract the actual DB name from a JDBC override URL if active."""
+ if not (self.ssl.override_jdbc and self.ssl.override_jdbc_connection_string):
+ return db_name
+ match = _JDBC_DB_NAME_RE.search(self.ssl.override_jdbc_connection_string)
+ return match.group(1) if match else db_name
+
+ def _build_jisql_classpath(self) -> str:
+ """Locate Jisql lib directories and build a Java classpath."""
+ candidates = [os.path.join(RANGER_HOME, "admin", "jisql", "lib"), os.path.join(RANGER_HOME, "jisql", "lib")]
+ found = [d for d in candidates if os.path.isdir(d)]
+ if not found:
+ found = [candidates[0]] # Fall back to the most common layout.
+ return os.pathsep.join(os.path.join(d, "*") for d in found)
+
+ def _build_ssl_params(self) -> tuple[str, str]:
+ """Return (url_param, jvm_cert_flags) for SSL."""
+ if not self.ssl.enabled:
+ return "", ""
+
+ ssl = self.ssl
+
+ if ssl.certificate_file:
+ return f"?ssl=true&sslmode=verify-full&sslrootcert={ssl.certificate_file}", ""
+
+ if ssl.verify_server_certificate or ssl.required:
+ url_param = "?ssl=true&sslmode=verify-full&sslfactory=org.postgresql.ssl.DefaultJavaSSLFactory"
+ if ssl.auth_type == "1-way":
+ cert_flags = (
+ f" -Djavax.net.ssl.trustStore={ssl.trust_store}"
+ f" -Djavax.net.ssl.trustStorePassword={ssl.trust_store_password}"
+ f" -Djavax.net.ssl.trustStoreType={ssl.trust_store_type}"
+ )
+ else:
+ cert_flags = (
+ f" -Djavax.net.ssl.keyStore={ssl.key_store}"
+ f" -Djavax.net.ssl.keyStorePassword={ssl.key_store_password}"
+ f" -Djavax.net.ssl.trustStore={ssl.trust_store}"
+ f" -Djavax.net.ssl.trustStorePassword={ssl.trust_store_password}"
+ f" -Djavax.net.ssl.trustStoreType={ssl.trust_store_type}"
+ f" -Djavax.net.ssl.keyStoreType={ssl.key_store_type}"
+ )
+ return url_param, cert_flags
+
+ return "?ssl=true", ""
+
+ def _get_jisql_cmd(self, user: str, password: str, db_name: str) -> str:
+ """Build the full Jisql invocation command string."""
+ classpath = self._build_jisql_classpath()
+ ssl_url, ssl_cert = self._build_ssl_params()
+ cp = f"{self.sql_connector_jar}{os.pathsep}{classpath}"
+
+ if self.ssl.override_jdbc and self.ssl.override_jdbc_connection_string:
+ cstring = self.ssl.override_jdbc_connection_string
+ else:
+ cstring = f"jdbc:postgresql://{self.host}/{db_name}{ssl_url}"
+
+ return (
+ f"{self.java_bin} {ssl_cert} -cp {cp} org.apache.util.sql.Jisql"
+ f" -driver postgresql -cstring '{cstring}'"
+ f" -u {user} -p '{password}' -noheader -trim -c \\;"
+ )
+
+ # -- public interface --------------------------------------------------
+
+ def check_connection(self, db_name: str, db_user: str, db_password: str) -> bool:
+ """Verify that we can reach the database. Raises ConnectionError on failure."""
+ logger.info("Checking connection to database %s", db_name)
+ cmd = self._get_jisql_cmd(db_user, db_password, db_name)
+ query = f'{cmd} -query "SELECT 1;"'
+ _log_jisql(query, db_password)
+
+ output = _run_command(query)
+ if "1" in output:
+ logger.info("Connection successful")
+ return True
+ raise ConnectionError(f"Cannot establish connection to database {db_name}")
+
+ def import_db_file(self, db_name: str, db_user: str, db_password: str, file_name: str) -> None:
+ """Import a SQL schema file into the database.
+
+ Raises FileNotFoundError if the file is missing, or
+ SchemaImportError if the import subprocess fails.
+ """
+ display_name = os.path.basename(file_name)
+ if not os.path.isfile(file_name):
+ raise FileNotFoundError(f"DB schema file not found: {display_name}")
+
+ logger.info("Importing schema to %s from file: %s", db_name, display_name)
+ cmd = self._get_jisql_cmd(db_user, db_password, db_name)
+ query = f"{cmd} -input {file_name}"
+ _log_jisql(query, db_password)
+
+ ret = subprocess.call(shlex.split(query))
+ if ret != 0:
+ raise SchemaImportError(f"Schema import failed for {display_name}")
+ logger.info("%s imported successfully", display_name)
+
+ def check_table(self, db_name: str, db_user: str, db_password: str, table_name: str) -> bool:
+ """Return True if *table_name* exists in *db_name*."""
+ db_name = self._resolve_db_name(db_name)
+ logger.info("Verifying table %s in database %s", table_name, db_name)
+
+ cmd = self._get_jisql_cmd(db_user, db_password, db_name)
+ query = (
+ f'{cmd} -query "SELECT table_name FROM information_schema.tables'
+ f" WHERE table_catalog='{db_name}' AND table_name='{table_name}';\""
+ )
+ _log_jisql(query, db_password)
+
+ try:
+ output = _run_command(query)
+ if output and table_name.lower() in output.lower():
+ logger.info("Table %s exists in %s", table_name, db_name)
+ return True
+ logger.info("Table %s does not exist in %s", table_name, db_name)
+ return False
+ except (subprocess.SubprocessError, OSError) as exc:
+ logger.error("Error checking table: %s", exc)
+ return False
+
+ def check_sequence(self, db_name: str, db_user: str, db_password: str, sequence_name: str) -> bool:
+ """Return True if *sequence_name* exists in *db_name*."""
+ db_name = self._resolve_db_name(db_name)
+ seq = _validate_identifier(sequence_name).lower()
+ logger.info("Verifying sequence %s in database %s", seq, db_name)
+
+ cmd = self._get_jisql_cmd(db_user, db_password, db_name)
+ query = (
+ f'{cmd} -query "SELECT sequence_name FROM information_schema.sequences'
+ f" WHERE sequence_schema='public' AND sequence_name='{seq}';\""
+ )
+ _log_jisql(query, db_password)
+
+ try:
+ output = _run_command(query)
+ if output and seq in output.lower():
+ logger.info("Sequence %s exists in %s", seq, db_name)
+ return True
+ logger.info("Sequence %s does not exist in %s", seq, db_name)
+ return False
+ except (subprocess.SubprocessError, OSError) as exc:
+ logger.error("Error checking sequence: %s", exc)
+ return False
+
+ def ensure_sequence(self, db_name: str, db_user: str, db_password: str, sequence_name: str) -> bool:
+ """Ensure a sequence exists, creating it if necessary.
+
+ Some Ranger distributions create sequences via patch SQL; when
+ bootstrapping with only the core schema, we may need to create
+ them explicitly.
+ """
+ if self.check_sequence(db_name, db_user, db_password, sequence_name):
+ return True
+
+ seq = _validate_identifier(sequence_name).lower()
+ logger.warning("Attempting to create missing sequence: %s", seq)
+ cmd = self._get_jisql_cmd(db_user, db_password, db_name)
+
+ for sql in (f"CREATE SEQUENCE {seq};", f"CREATE SEQUENCE IF NOT EXISTS {seq};"):
+ query = f'{cmd} -query "{sql}"'
+ _log_jisql(query, db_password)
+ try:
+ subprocess.call(shlex.split(query))
+ except (subprocess.SubprocessError, OSError) as exc:
+ logger.warning("Sequence create raised: %s", exc)
+
+ if self.check_sequence(db_name, db_user, db_password, sequence_name):
+ logger.info("Sequence %s is present", seq)
+ return True
+
+ logger.error("Failed to ensure required sequence: %s", seq)
+ return False
+
+ def update_portal_user_password(self, db_name: str, db_user: str, db_password: str, login_id: str, plain_password: str) -> None:
+ """Set a portal user's password using Ranger's legacy seed encoding.
+
+ Fresh schema imports seed admin/usersync/tagsync with fixed hashes in SQL.
+ Rewrite those seeded hashes from env before Ranger Admin starts so first
+ login matches the container configuration.
+ """
+ encoded_password = hashlib.md5(f"{plain_password}{{{login_id}}}".encode("utf-8")).hexdigest()
+ cmd = self._get_jisql_cmd(db_user, db_password, db_name)
+ query = (
+ f'{cmd} -query "UPDATE x_portal_user '
+ f"SET password='{encoded_password}' "
+ f"WHERE login_id='{login_id}';\""
+ )
+ logger.info("Setting initial password for Ranger user %s from environment", login_id)
+ _log_jisql(query, db_password)
+
+ ret = subprocess.call(shlex.split(query))
+ if ret != 0:
+ raise SchemaImportError(f"Failed to seed password for Ranger user {login_id}")
+
+
+# ---------------------------------------------------------------------------
+# Java binary resolution
+# ---------------------------------------------------------------------------
+def _resolve_java_bin() -> str:
+ """Determine the path to the java binary."""
+ java_home = os.environ.get("JAVA_HOME", "").strip()
+ if java_home:
+ return os.path.join(java_home, "bin", "java")
+ java_bin = shutil.which("java") or "java"
+ logger.warning("JAVA_HOME not set; using JAVA_BIN=%s", java_bin)
+ return java_bin
+
+
+# ---------------------------------------------------------------------------
+# Schema file resolution
+# ---------------------------------------------------------------------------
+def _resolve_schema_file(core_file_rel: str) -> str:
+ """Return the absolute path to the core schema SQL file.
+
+ Raises ConfigError if the file does not exist.
+ """
+ if os.getenv("RANGER_HOME"):
+ path = os.path.join(RANGER_HOME, "admin", core_file_rel)
+ else:
+ path = os.path.join(RANGER_HOME, core_file_rel)
+
+ logger.info("Schema file path: %s", path)
+ if not os.path.isfile(path):
+ raise ConfigError(f"Schema file not found: {path} (RANGER_HOME={RANGER_HOME})")
+ return path
+
+
+# ---------------------------------------------------------------------------
+# Entry point
+# ---------------------------------------------------------------------------
+VERSION_TABLE = "x_db_version_h"
+CRITICAL_SEQUENCE = "X_TRX_LOG_SEQ"
+
+
+def seed_initial_user_passwords(db: PostgresDB, db_name: str, db_user: str, db_password: str) -> None:
+ for login_id, env_var in (("admin", "RANGER_ADMIN_PASSWORD"), ("rangerusersync", "RANGER_USERSYNC_PASSWORD"), ("rangertagsync", "RANGER_TAGSYNC_PASSWORD")):
+ plain_password = os.environ.get(env_var, "")
+ if plain_password:
+ db.update_portal_user_password(db_name, db_user, db_password, login_id, plain_password)
+
+
+def main(argv: list[str]) -> None:
+
+ configure_logging()
+ config = load_runtime_config()
+
+ db_flavor = config["DB_FLAVOR"]
+ if db_flavor != "POSTGRES":
+ raise ConfigError("Ranger Admin docker currently supports only PostgreSQL")
+
+ java_bin = _resolve_java_bin()
+ logger.info("DB FLAVOR: %s", db_flavor)
+
+ ssl = _extract_ssl_config(config)
+ _validate_ssl_files(ssl)
+
+ db_name, db_user, db_password = config["db_name"], config["db_user"], config["db_password"]
+
+ db = PostgresDB(host=config["db_host"], sql_connector_jar=config["SQL_CONNECTOR_JAR"], java_bin=java_bin, ssl=ssl)
+ schema_file = _resolve_schema_file(config["postgres_core_file"])
+
+ logger.info("--------- Verifying Ranger DB connection ---------")
+ db.check_connection(db_name, db_user, db_password)
+
+ if len(argv) > 1:
+ return # Extra CLI arguments present — skip schema initialisation.
+
+ logger.info("--------- Verifying Ranger DB tables ---------")
+ if db.check_table(db_name, db_user, db_password, VERSION_TABLE):
+ logger.info("Database schema already initialised")
+ if not db.ensure_sequence(db_name, db_user, db_password, CRITICAL_SEQUENCE):
+ logger.warning("Critical sequence %s still missing, but schema appears initialised. Service creation may fail.", CRITICAL_SEQUENCE)
+ return
+
+ logger.info("--------- Importing Ranger Core DB Schema ---------")
+ db.import_db_file(db_name, db_user, db_password, schema_file)
+ seed_initial_user_passwords(db, db_name, db_user, db_password)
+
+ if not db.check_table(db_name, db_user, db_password, VERSION_TABLE):
+ raise SchemaImportError(f"Schema import completed but {VERSION_TABLE} table not found")
+
+ if not db.ensure_sequence(db_name, db_user, db_password, CRITICAL_SEQUENCE):
+ raise SchemaImportError( f"Sequence {CRITICAL_SEQUENCE} required for service creation. Schema import/patch may be incomplete.")
+
+ logger.info("Database schema imported successfully")
+
+
+if __name__ == "__main__":
+ try:
+ main(sys.argv)
+ except (ConfigError, ConnectionError, SchemaImportError) as exc:
+ logger.error("%s", exc)
+ sys.exit(1)
diff --git a/dev-support/ranger-docker/scripts/admin/ranger-admin-default-site.xml b/dev-support/ranger-docker/scripts/admin/ranger-admin-default-site.xml
new file mode 100644
index 0000000000..d020b5dff9
--- /dev/null
+++ b/dev-support/ranger-docker/scripts/admin/ranger-admin-default-site.xml
@@ -0,0 +1,615 @@
+
+
+
+
+
+
+ ranger.jdbc.sqlconnectorjar
+ /usr/share/java/postgresql.jar
+
+
+
+ ranger.service.user
+ ranger
+
+
+
+ ranger.service.group
+ ranger
+
+
+
+ ajp.enabled
+ false
+
+
+
+
+
+
+ ranger.db.maxrows.default
+ 200
+
+
+ ranger.db.min_inlist
+ 20
+
+
+ ranger.ui.defaultDateformat
+ MM/dd/yyyy
+
+
+ ranger.db.defaultDateformat
+ yyyy-MM-dd
+
+
+
+
+ ranger.ajax.auth.required.code
+ 401
+
+
+ ranger.ajax.auth.success.page
+ /ajax_success.html
+
+
+ ranger.logout.success.page
+ /login.jsp?action=logged_out
+
+
+ ranger.ajax.auth.failure.page
+ /ajax_failure.jsp
+
+
+
+
+ ranger.users.roles.list
+ ROLE_SYS_ADMIN, ROLE_USER, ROLE_OTHER, ROLE_ANON, ROLE_KEY_ADMIN, ROLE_ADMIN_AUDITOR, ROLE_KEY_ADMIN_AUDITOR
+
+
+
+ ranger.mail.enabled
+ true
+
+
+ ranger.mail.smtp.auth
+ false
+
+
+ ranger.mail.retry.sleep.ms
+ 2000
+
+
+ ranger.mail.retry.max.count
+ 5
+
+
+ ranger.mail.retry.sleep.incr_factor
+ 1
+
+
+ ranger.mail.listener.enable
+ false
+
+
+
+ ranger.second_level_cache
+ true
+
+
+ ranger.use_query_cache
+ true
+
+
+
+
+ ranger.user.firstname.maxlength
+ 16
+
+
+ ranger.bookmark.name.maxlen
+ 150
+
+
+
+
+ ranger.rbac.enable
+ false
+
+
+
+
+ ranger.rest.paths
+ org.apache.ranger.rest,xa.rest
+
+
+
+
+ ranger.password.hidden
+ *****
+
+
+ ranger.resource.accessControl.enabled
+ true
+
+
+ ranger.xuser.createdByUserId
+ 1
+
+
+
+
+
+ ranger.allow.hack
+ 1
+
+
+
+
+
+ ranger.log.SC_NOT_MODIFIED
+ false
+
+
+
+
+ ranger.servlet.mapping.url.pattern
+ service
+
+
+
+
+
+
+
+ ranger.file.separator
+ /
+
+
+
+ ranger.db.access.filter.enable
+ true
+
+
+ ranger.moderation.enabled
+ false
+
+
+ ranger.userpref.enabled
+ false
+
+
+
+
+
+ ranger.valve.errorreportvalve.showserverinfo
+ false
+
+
+ ranger.valve.errorreportvalve.showreport
+ false
+
+
+
+
+
+
+ ranger.unixauth.remote.login.enabled
+ true
+
+
+ ranger.unixauth.service.hostname
+ localhost
+
+
+ ranger.unixauth.service.port
+ 5151
+
+
+ ranger.unixauth.ssl.enabled
+ true
+
+
+ ranger.unixauth.debug
+ false
+
+
+ ranger.unixauth.server.cert.validation
+ false
+
+
+
+ ranger.unixauth.keystore
+ keystore.jks
+
+
+ ranger.unixauth.keystore.credential.alias
+ unixAuthKeyStoreAlias
+
+
+ ranger.unixauth.keystore.password
+ password
+
+
+ ranger.unixauth.truststore
+ cacerts
+
+
+ ranger.unixauth.truststore.credential.alias
+ unixAuthTrustStoreAlias
+
+
+ ranger.unixauth.truststore.password
+ changeit
+
+
+
+
+
+ maven.project.version
+ 0.5.0
+
+
+
+
+
+ ranger.service.shutdown.port
+ 6085
+
+
+
+ ranger.service.shutdown.command
+ SHUTDOWN
+
+
+
+ ranger.service.https.attrib.ssl.protocol
+ TLSv1.2
+
+
+
+ ranger.service.https.attrib.client.auth
+ false
+
+
+
+ ranger.accesslog.dateformat
+ yyyy-MM-dd
+
+
+
+ ranger.accesslog.pattern
+ %h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i"
+
+
+
+ ranger.contextName
+ /
+
+
+
+
+ ranger.jpa.showsql
+ false
+
+
+
+
+ ranger.env.local
+ true
+
+
+
+
+ ranger.jpa.jdbc.dialect
+ org.eclipse.persistence.platform.database.PostgreSQLPlatform
+
+
+
+
+ ranger.jpa.jdbc.maxpoolsize
+ 40
+
+
+
+
+ ranger.jpa.jdbc.minpoolsize
+ 5
+
+
+
+
+ ranger.jpa.jdbc.idletimeout
+ 300000
+
+
+
+
+ ranger.jpa.jdbc.maxlifetime
+ 1800000
+
+
+
+
+ ranger.jpa.jdbc.preferredtestquery
+ select 1
+
+
+
+
+ ranger.jpa.jdbc.connectiontimeout
+ 30000
+
+
+
+
+ ranger.jpa.jdbc.batch-clear.enable
+ true
+ property to enable bulk mode optimization
+
+
+
+ ranger.jpa.jdbc.batch-clear.size
+ 10
+ batch size (in number of policies) to flush and clear jdbc statements during batch policy import/delete
+
+
+
+ ranger.jpa.jdbc.batch-persist.size
+ 500
+ batch size (in number of objects) to flush and clear jdbc statements during jpa persistence
+
+
+
+ ranger.jpa.jdbc.credential.alias
+ ranger.db.password
+
+
+
+
+ ranger.credential.provider.path
+ /etc/ranger/admin/rangeradmin.jceks
+
+
+
+
+ ranger.logs.base.dir
+ user.home
+
+
+
+
+ ranger.jpa.audit.jdbc.dialect
+ org.eclipse.persistence.platform.database.PostgreSQLPlatform
+
+
+
+
+ ranger.jpa.audit.jdbc.credential.alias
+ ranger.auditdb.password
+
+
+
+
+ ranger.ldap.binddn.credential.alias
+ ranger.ldap.binddn.password
+
+
+
+
+ ranger.ldap.ad.binddn.credential.alias
+ ranger.ad.binddn.password
+
+
+
+
+ ranger.resource.lookup.timeout.value.in.ms
+ 1000
+
+
+
+
+ ranger.validate.config.timeout.value.in.ms
+ 10000
+
+
+
+
+ ranger.timed.executor.max.threadpool.size
+ 10
+
+
+
+ ranger.timed.executor.queue.size
+ 100
+
+
+ ranger.solr.audit.credential.alias
+ ranger.solr.password
+
+
+
+ ranger.audit.solr.time.interval
+ 60000
+ Time in milliseconds
+
+
+ ranger.audit.solr.bootstrap.enabled
+ true
+
+
+ ranger.audit.solr.max.retry
+ 30
+ Maximum no. of retry to setup solr
+
+
+ ranger.sha256Password.update.disable
+ false
+
+
+
+ ranger.password.history.count
+ 4
+
+
+
+
+
+ ranger.jpa.audit.jdbc.driver
+ net.sf.log4jdbc.DriverSpy
+
+
+
+ ranger.jpa.audit.jdbc.url
+ jdbc:log4jdbc:mysql://localhost/rangeraudit
+
+
+
+ ranger.jpa.audit.jdbc.user
+ rangerlogger
+
+
+
+ ranger.jpa.audit.jdbc.password
+ rangerlogger
+
+
+
+ ranger.supportedcomponents
+
+
+
+
+ ranger.sso.cookiename
+ hadoop-jwt
+
+
+ ranger.sso.query.param.originalurl
+ originalUrl
+
+
+ ranger.rest-csrf.enabled
+ true
+
+
+ ranger.rest-csrf.custom-header
+ X-XSRF-HEADER
+
+
+ ranger.rest-csrf.methods-to-ignore
+ GET,OPTIONS,HEAD,TRACE
+
+
+ ranger.rest-csrf.browser-useragents-regex
+ Mozilla,Opera,Chrome
+
+
+ ranger.krb.browser-useragents-regex
+ Mozilla,Opera,Chrome
+
+
+ ranger.db.ssl.enabled
+ false
+
+
+ ranger.db.ssl.required
+ false
+
+
+ ranger.db.ssl.verifyServerCertificate
+ false
+
+
+ ranger.db.ssl.auth.type
+ 2-way
+
+
+ ranger.db.ssl.certificateFile
+
+
+
+ ranger.truststore.file.type
+ jks
+
+
+ ranger.keystore.file.type
+ jks
+
+
+ ranger.keystore.file
+
+
+
+ ranger.keystore.alias
+ keyStoreAlias
+
+
+ ranger.keystore.password
+
+
+
+ ranger.truststore.file
+
+
+
+ ranger.truststore.alias
+ trustStoreAlias
+
+
+ ranger.truststore.password
+
+
+
+ ranger.service.https.attrib.ssl.enabled.protocols
+ TLSv1.2
+
+
+
+ ranger.password.encryption.key
+ tzL1AKl5uc4NKYaoQ4P3WLGIBFPXWPWdu1fRm9004jtQiV
+
+
+ ranger.password.salt
+ f77aLYLo
+
+
+ ranger.password.iteration.count
+ 1000
+
+
+ ranger.password.encryption.algorithm
+ PBEWithHmacSHA512AndAES_128
+
+
+ ranger.default.browser-useragents
+ Mozilla,Opera,Chrome
+
+
+ ranger.admin.cookie.name
+ RANGERADMINSESSIONID
+
+
+ ranger.tomcat.work.dir
+
+
+
+ ranger.allow.kerberos.auth.login.browser
+ false
+
+
diff --git a/dev-support/ranger-docker/scripts/admin/ranger-admin-install-postgres.properties b/dev-support/ranger-docker/scripts/admin/ranger-admin-install-postgres.properties
deleted file mode 100644
index cf8a58feca..0000000000
--- a/dev-support/ranger-docker/scripts/admin/ranger-admin-install-postgres.properties
+++ /dev/null
@@ -1,109 +0,0 @@
-# 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
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# 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.
-
-#
-# This file provides a list of the deployment variables for the Policy Manager Web Application
-#
-
-PYTHON_COMMAND_INVOKER=python3
-RANGER_ADMIN_LOG_DIR=/var/log/ranger
-RANGER_PID_DIR_PATH=/var/run/ranger
-DB_FLAVOR=POSTGRES
-SQL_CONNECTOR_JAR=/usr/share/java/postgresql.jar
-RANGER_ADMIN_LOGBACK_CONF_FILE=/opt/ranger/admin/ews/webapp/WEB-INF/classes/conf/logback.xml
-
-db_root_user=postgres
-db_root_password=rangerR0cks!
-db_host=ranger-db
-
-db_name=ranger
-db_user=rangeradmin
-db_password=rangerR0cks!
-
-postgres_core_file=db/postgres/optimized/current/ranger_core_db_postgres.sql
-postgres_audit_file=db/postgres/xa_audit_db_postgres.sql
-mysql_core_file=db/mysql/optimized/current/ranger_core_db_mysql.sql
-mysql_audit_file=db/mysql/xa_audit_db.sql
-
-rangerAdmin_password=rangerR0cks!
-rangerTagsync_password=rangerR0cks!
-rangerUsersync_password=rangerR0cks!
-keyadmin_password=rangerR0cks!
-
-
-audit_store=solr
-audit_solr_urls=http://ranger-solr:8983/solr/ranger_audits
-audit_solr_collection_name=ranger_audits
-
-# audit_store=elasticsearch
-audit_elasticsearch_urls=
-audit_elasticsearch_port=9200
-audit_elasticsearch_protocol=http
-audit_elasticsearch_user=elastic
-audit_elasticsearch_password=elasticsearch
-audit_elasticsearch_index=ranger_audits
-audit_elasticsearch_bootstrap_enabled=true
-
-policymgr_external_url=http://ranger-admin:6080
-policymgr_http_enabled=true
-
-unix_user=ranger
-unix_user_pwd=ranger
-unix_group=ranger
-
-# Following variables are referenced in db_setup.py. Do not remove these
-oracle_core_file=
-sqlserver_core_file=
-sqlanywhere_core_file=
-cred_keystore_filename=
-
-# ################# DO NOT MODIFY ANY VARIABLES BELOW #########################
-#
-# --- These deployment variables are not to be modified unless you understand the full impact of the changes
-#
-################################################################################
-XAPOLICYMGR_DIR=$PWD
-app_home=$PWD/ews/webapp
-TMPFILE=$PWD/.fi_tmp
-LOGFILE=$PWD/logfile
-LOGFILES="$LOGFILE"
-
-JAVA_BIN='java'
-JAVA_VERSION_REQUIRED='1.8'
-
-ranger_admin_max_heap_size=1g
-#retry DB and Java patches after the given time in seconds.
-PATCH_RETRY_INTERVAL=120
-STALE_PATCH_ENTRY_HOLD_TIME=10
-
-hadoop_conf=
-authentication_method=UNIX
-
-#------------ Kerberos Config -----------------
-spnego_principal=HTTP/ranger.rangernw@EXAMPLE.COM
-spnego_keytab=/etc/keytabs/HTTP.keytab
-token_valid=30
-admin_principal=rangeradmin/ranger.rangernw@EXAMPLE.COM
-admin_keytab=/etc/keytabs/rangeradmin.keytab
-lookup_principal=rangerlookup/ranger.rangernw@EXAMPLE.COM
-lookup_keytab=/etc/keytabs/rangerlookup.keytab
-audit_jaas_client_loginModuleName=com.sun.security.auth.module.Krb5LoginModule
-audit_jaas_client_loginModuleControlFlag=required
-audit_jaas_client_option_useKeyTab=true
-audit_jaas_client_option_storeKey=true
-audit_jaas_client_option_useTicketCache=true
-audit_jaas_client_option_serviceName=ranger
-audit_jaas_client_option_keyTab=/etc/keytabs/rangeradmin.keytab
-audit_jaas_client_option_principal=rangeradmin/ranger.rangernw@EXAMPLE.COM
diff --git a/dev-support/ranger-docker/scripts/admin/ranger-admin-site.xml b/dev-support/ranger-docker/scripts/admin/ranger-admin-site.xml
new file mode 100644
index 0000000000..e8ee8a7902
--- /dev/null
+++ b/dev-support/ranger-docker/scripts/admin/ranger-admin-site.xml
@@ -0,0 +1,523 @@
+
+
+ ranger.externalurl
+ http://ranger-admin:6080
+
+
+
+ ranger.scheduler.enabled
+ true
+
+
+
+ ranger.jdbc.sqlconnectorjar
+ /usr/share/java/postgresql.jar
+ SQL connector JAR file path for the dockerized Ranger Admin runtime
+
+
+ ranger.jpa.jdbc.driver
+ org.postgresql.Driver
+
+
+
+ ranger.jpa.jdbc.url
+ jdbc:postgresql://ranger-db:5432/ranger
+
+
+
+ ranger.jpa.jdbc.user
+ rangeradmin
+
+
+
+ ranger.jpa.jdbc.dialect
+ org.eclipse.persistence.platform.database.PostgreSQLPlatform
+ JPA database dialect for PostgreSQL
+
+
+ ranger.jpa.jdbc.maxpoolsize
+ 40
+ Maximum connection pool size
+
+
+ ranger.jpa.jdbc.minpoolsize
+ 5
+ Minimum connection pool size
+
+
+ ranger.jpa.jdbc.idletimeout
+ 300000
+ Idle connection timeout in milliseconds (5 minutes)
+
+
+ ranger.jpa.jdbc.maxlifetime
+ 1800000
+ Maximum connection lifetime in milliseconds (30 minutes)
+
+
+ ranger.jpa.jdbc.preferredtestquery
+ select 1
+ Query used to test database connections
+
+
+ ranger.jpa.jdbc.connectiontimeout
+ 30000
+ Connection timeout in milliseconds (30 seconds)
+
+
+ ranger.jpa.jdbc.batch-clear.enable
+ true
+ Enable bulk mode optimization for batch operations
+
+
+ ranger.jpa.jdbc.batch-clear.size
+ 10
+ Batch size (in number of policies) to flush and clear JDBC statements during batch policy import/delete
+
+
+ ranger.jpa.jdbc.batch-persist.size
+ 500
+ Batch size (in number of objects) to flush and clear JDBC statements during JPA persistence
+
+
+ ranger.jpa.jdbc.credential.alias
+ ranger.db.password
+ Credential alias for database password in credential store
+
+
+ ranger.credential.provider.path
+ /etc/ranger/admin/rangeradmin.jceks
+ Path to credential provider keystore file
+
+
+ ranger.jpa.audit.jdbc.dialect
+ org.eclipse.persistence.platform.database.PostgreSQLPlatform
+ JPA database dialect for audit database (PostgreSQL)
+
+
+ ranger.jpa.audit.jdbc.credential.alias
+ ranger.auditdb.password
+ Credential alias for audit database password in credential store
+
+
+ ranger.jpa.showsql
+ false
+ Enable SQL query logging (for debugging)
+
+
+ ranger.env.local
+ true
+ Local environment flag
+
+
+ ranger.db.maxrows.default
+ 200
+ Default maximum rows returned from database queries
+
+
+ ranger.db.min_inlist
+ 20
+ Minimum items for IN clause optimization
+
+
+ ranger.db.defaultDateformat
+ yyyy-MM-dd
+ Default date format for database operations
+
+
+ ranger.service.http.enabled
+ true
+
+
+
+ ranger.authentication.method
+ UNIX
+
+
+
+
+ ranger.spnego.kerberos.principal
+ HTTP/ranger.rangernw@EXAMPLE.COM
+
+
+
+ ranger.spnego.kerberos.keytab
+ /etc/keytabs/HTTP.keytab
+
+
+
+ ranger.admin.kerberos.token.valid.seconds
+ 30
+
+
+
+ ranger.admin.kerberos.principal
+ rangeradmin/ranger.rangernw@EXAMPLE.COM
+
+
+
+ ranger.admin.kerberos.keytab
+ /etc/keytabs/rangeradmin.keytab
+
+
+
+ ranger.lookup.kerberos.principal
+ rangerlookup/ranger.rangernw@EXAMPLE.COM
+
+
+
+ ranger.lookup.kerberos.keytab
+ /etc/keytabs/rangerlookup.keytab
+
+
+
+ xasecure.audit.jaas.Client.loginModuleName
+ com.sun.security.auth.module.Krb5LoginModule
+
+
+
+ xasecure.audit.jaas.Client.loginModuleControlFlag
+ required
+
+
+
+ xasecure.audit.jaas.Client.option.useKeyTab
+ true
+
+
+
+ xasecure.audit.jaas.Client.option.storeKey
+ true
+
+
+
+ xasecure.audit.jaas.Client.option.useTicketCache
+ true
+
+
+
+ xasecure.audit.jaas.Client.option.serviceName
+ ranger
+
+
+
+ xasecure.audit.jaas.Client.option.keyTab
+ /etc/keytabs/rangeradmin.keytab
+
+
+
+ xasecure.audit.jaas.Client.option.principal
+ rangeradmin/ranger.rangernw@EXAMPLE.COM
+
+
+
+
+ ranger.ldap.url
+ ldap://
+
+
+
+ ranger.ldap.user.dnpattern
+ uid={0},ou=users,dc=xasecure,dc=net
+
+
+
+ ranger.ldap.group.searchbase
+ ou=groups,dc=xasecure,dc=net
+
+
+
+ ranger.ldap.group.searchfilter
+ (member=uid={0},ou=users,dc=xasecure,dc=net)
+
+
+
+ ranger.ldap.group.roleattribute
+ cn
+
+
+
+ ranger.ldap.base.dn
+
+ LDAP base dn or search base
+
+
+ ranger.ldap.bind.dn
+
+ LDAP bind dn or manager dn
+
+
+ ranger.ldap.bind.password
+
+ LDAP bind password
+
+
+ ranger.ldap.default.role
+ ROLE_USER
+
+
+ ranger.ldap.referral
+
+ follow or ignore
+
+
+ ranger.ldap.ad.domain
+ example.com
+
+
+
+ ranger.ldap.ad.url
+
+ ldap://
+
+
+ ranger.ldap.ad.base.dn
+ dc=example,dc=com
+ AD base dn or search base
+
+
+ ranger.ldap.ad.bind.dn
+ cn=administrator,ou=users,dc=example,dc=com
+ AD bind dn or manager dn
+
+
+ ranger.ldap.ad.bind.password
+
+ AD bind password
+
+
+ ranger.ldap.ad.referral
+
+ follow or ignore
+
+
+ ranger.service.https.attrib.ssl.enabled
+ false
+
+
+ ranger.service.https.attrib.keystore.keyalias
+ myKey
+
+
+ ranger.service.https.attrib.keystore.pass
+ _
+
+
+ ranger.service.host
+ localhost
+
+
+ ranger.service.http.port
+ 6080
+
+
+ ranger.service.https.port
+ 6080
+
+
+ ranger.service.https.attrib.keystore.file
+ /etc/ranger/admin/keys/server.jks
+
+
+ ranger.ldap.user.searchfilter
+ (uid={0})
+
+
+
+ ranger.ldap.ad.user.searchfilter
+ (sAMAccountName={0})
+
+
+
+ ranger.authentication.allow.trustedproxy
+ false
+
+
+ ranger.sso.providerurl
+
+
+
+ ranger.sso.enabled
+ true
+
+
+ ranger.sso.browser.useragent
+ Mozilla,chrome
+
+
+ ranger.supportedcomponents
+
+
+
+ ranger.downloadpolicy.session.log.enabled
+ false
+
+
+ ranger.kms.service.user.hdfs
+ hdfs
+
+
+ ranger.kms.service.user.hive
+ hive
+
+
+ ranger.kms.service.user.om
+ om
+
+
+ ranger.kms.service.user.kudu
+ kudu
+
+
+ ranger.audit.hive.query.visibility
+ true
+
+
+
+ ranger.service.https.attrib.keystore.credential.alias
+ keyStoreCredentialAlias
+
+
+ ranger.tomcat.ciphers
+
+
+
+ ranger.admin.cookie.name
+ RANGERADMINSESSIONID
+
+
+ ranger.plugins.hdfs.serviceuser
+ hdfs
+
+
+ ranger.plugins.hive.serviceuser
+ hive
+
+
+ ranger.plugins.knox.serviceuser
+ knox
+
+
+ ranger.plugins.yarn.serviceuser
+ yarn
+
+
+ ranger.plugins.kafka.serviceuser
+ kafka
+
+
+ ranger.plugins.kraft.serviceuser
+ kraft
+
+
+ ranger.plugins.mirror_maker.serviceuser
+ kafka_mirror_maker
+
+
+ ranger.plugins.kudu.serviceuser
+ kudu
+
+
+ ranger.plugins.cruise_control.serviceuser
+ cruisecontrol
+
+
+ ranger.plugins.cruise_control_mr.serviceuser
+ cc_metric_reporter
+
+
+ ranger.plugins.schemaregistry.serviceuser
+ schemaregistry
+
+
+ ranger.plugins.streams_messaging_manager.serviceuser
+ streamsmsgmgr
+
+
+ ranger.plugins.streams_replication_manager.serviceuser
+ streamsrepmgr
+
+
+ ranger.plugins.sql_stream_builder.serviceuser
+ ssb
+
+
+ ranger.plugins.nifi.serviceuser
+ nifi
+
+
+ ranger.plugins.atlas.serviceuser
+ atlas
+
+
+ ranger.plugins.nifiregistry.serviceuser
+ nifiregistry
+
+
+ ranger.plugins.impala.serviceuser
+ impala
+
+
+ ranger.plugins.ozone.serviceuser
+ om
+
+
+ ranger.plugins.solr.serviceuser
+ solr
+
+
+ ranger.plugins.tagsync.serviceuser
+ rangertagsync
+
+
+ ranger.plugins.hue.serviceuser
+ hue
+
+
+ ranger.plugins.trino.serviceuser
+ trino
+
+
+ ranger.plugins.polaris.serviceuser
+ polaris
+
+
+ ranger.default.policy.groups
+ c_ranger_admin_groups
+
+
+ ranger.contextName
+ /
+
+
+
+ ranger.audit.source.type
+ solr
+ solr (indexed audits) or db (legacy RDBMS)
+
+
+ ranger.audit.solr.urls
+ http://ranger-solr:8983/solr/ranger_audits
+ Standalone Solr audit (HTTP); core created by udf Solr image
+
+
+ ranger.audit.solr.zookeepers
+
+ Empty for Solr HTTP mode (no SolrCloud ZK)
+
+
+ ranger.audit.solr.collection.name
+ ranger_audits
+
+
+
+ ranger.solr.audit.user
+
+
+
+
+ ranger.solr.audit.user.password
+
+
+
+
\ No newline at end of file
diff --git a/dev-support/ranger-docker/scripts/admin/ranger.sh b/dev-support/ranger-docker/scripts/admin/ranger.sh
index 6a8c26eb46..7aa3c31862 100755
--- a/dev-support/ranger-docker/scripts/admin/ranger.sh
+++ b/dev-support/ranger-docker/scripts/admin/ranger.sh
@@ -1,67 +1,288 @@
#!/bin/bash
-# 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
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# 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.
-
-if [ ! -e ${RANGER_HOME}/.setupDone ]
-then
- SETUP_RANGER=true
-else
- SETUP_RANGER=false
-fi
+set -euo pipefail
+
+RANGER_HOME="${RANGER_HOME:-/opt/ranger}"
+RANGER_ADMIN_DIR="${RANGER_HOME}/admin"
+CONF_DIR="${RANGER_ADMIN_CONF:-${RANGER_ADMIN_DIR}/ews/webapp/WEB-INF/classes/conf}"
+CONFIGS_DIR="${RANGER_ADMIN_DIR}/configs"
+CONFIG_XML_DEST="${CONF_DIR}/ranger-admin-site.xml"
+ADMIN_XML_HELPER="/home/ranger/scripts/ranger_admin_xml_config.py"
+USER_PASSWORD_BOOTSTRAP_HELPER="/home/ranger/scripts/user_password_bootstrap.py"
+SERVICES_MARKER="/opt/ranger/.rangeradminservicescreated"
+
+sync_admin_configs() {
+ local conf_file
+ mkdir -p "${CONF_DIR}"
+
+ for conf_file in \
+ "ranger-admin-site.xml" \
+ "core-site.xml" \
+ "ranger-admin-default-site.xml"; do
+ if [ -f "${CONFIGS_DIR}/${conf_file}" ]; then
+ cp -f "${CONFIGS_DIR}/${conf_file}" "${CONF_DIR}/${conf_file}"
+ fi
+ done
+}
+
+xml_prop() {
+ local key="$1"
+ local file="${2:-${CONFIG_XML_DEST}}"
+
+ [ -f "${file}" ] || return 0
+ python3 "${ADMIN_XML_HELPER}" get-property --file "${file}" --name "${key}"
+}
+
+get_config_value() {
+ local env_key="$1"
+ local xml_key="$2"
+ local default_value="${3:-}"
+ local value="${!env_key:-}"
+
+ if [ -n "${value}" ]; then
+ printf '%s\n' "${value}"
+ return 0
+ fi
+
+ value="$(xml_prop "${xml_key}")"
+ if [ -n "${value}" ]; then
+ printf '%s\n' "${value}"
+ return 0
+ fi
+
+ printf '%s\n' "${default_value}"
+}
+
+db_config_field() {
+ local field="$1"
+ python3 "${ADMIN_XML_HELPER}" get-db-field --file "${CONFIG_XML_DEST}" --field "${field}"
+}
+
+sync_db_password_property() {
+ local pass="${RANGER_ADMIN_DB_PASSWORD:-}"
-if [ "${SETUP_RANGER}" == "true" ]
-then
- if [ "${KERBEROS_ENABLED}" == "true" ]
- then
- ${RANGER_SCRIPTS}/wait_for_keytab.sh rangeradmin.keytab
- ${RANGER_SCRIPTS}/wait_for_keytab.sh rangerlookup.keytab
- ${RANGER_SCRIPTS}/wait_for_keytab.sh HTTP.keytab
- ${RANGER_SCRIPTS}/wait_for_testusers_keytab.sh
+ if [ -z "${pass}" ] || [ ! -f "${CONFIG_XML_DEST}" ]; then
+ return 0
fi
- cd "${RANGER_HOME}"/admin || exit
- if ./setup.sh;
- then
- if [ "${KERBEROS_ENABLED}" == "true" ]
- then
- cp ${RANGER_SCRIPTS}/core-site.xml ${RANGER_HOME}/admin/conf/core-site.xml
+ python3 "${ADMIN_XML_HELPER}" set-property \
+ --file "${CONFIG_XML_DEST}" \
+ --name "ranger.jpa.jdbc.password" \
+ --value "${pass}" \
+ --create
+}
+
+ensure_jdbc_driver() {
+ # ensure the JDBC driver is visible to the webapp classloader.
+ local src
+ src="$(get_config_value "SQL_CONNECTOR_JAR" "ranger.jdbc.sqlconnectorjar" "/usr/share/java/postgresql.jar")"
+ local libdir="${RANGER_ADMIN_DIR}/ews/webapp/WEB-INF/lib"
+
+ if [ -f "${src}" ]; then
+ mkdir -p "${libdir}" 2>/dev/null || true
+ if [ ! -f "${libdir}/$(basename "${src}")" ]; then
+ cp -f "${src}" "${libdir}/" 2>/dev/null || true
fi
+ fi
+}
- touch "${RANGER_HOME}"/.setupDone
- else
- echo "Ranger Admin Setup Script didn't complete proper execution."
+prepare_admin_runtime() {
+ local log_dir="${RANGER_ADMIN_LOG_DIR:-/var/log/ranger}"
+ local logback_conf="${RANGER_ADMIN_LOGBACK_CONF_FILE:-${CONF_DIR}/logback.xml}"
+ local pid_dir="${RANGER_PID_DIR_PATH:-/var/run/ranger}"
+ local env_logdir="${CONF_DIR}/ranger-admin-env-logdir.sh"
+ local env_logback="${CONF_DIR}/ranger-admin-env-logback-conf-file.sh"
+ local legacy_log_dir="${RANGER_ADMIN_DIR}/ews/logs"
+ local conf_dist_dir="${RANGER_ADMIN_DIR}/ews/webapp/WEB-INF/classes/conf.dist"
+
+ export RANGER_ADMIN_LOG_DIR="${log_dir}"
+ export RANGER_ADMIN_LOGBACK_CONF_FILE="${logback_conf}"
+ export RANGER_PID_DIR_PATH="${pid_dir}"
+
+ mkdir -p "${CONF_DIR}" "${log_dir}" "${pid_dir}"
+
+ for conf_file in "security-applicationContext.xml" "logback.xml"; do
+ if [ -f "${conf_dist_dir}/${conf_file}" ]; then
+ cp -f "${conf_dist_dir}/${conf_file}" "${CONF_DIR}/${conf_file}"
+ fi
+ done
+
+ if [ ! -e "${legacy_log_dir}" ]; then
+ ln -s "${log_dir}" "${legacy_log_dir}" 2>/dev/null || mkdir -p "${legacy_log_dir}"
fi
-fi
-cd ${RANGER_HOME}/admin && ./ews/ranger-admin-services.sh start
+ printf 'export RANGER_ADMIN_LOG_DIR=%s\n' "${RANGER_ADMIN_LOG_DIR}" > "${env_logdir}"
+ chmod 755 "${env_logdir}"
-if [ "${SETUP_RANGER}" == "true" ]
-then
- # Wait for Ranger Admin to become ready
- sleep 30
- python3 ${RANGER_SCRIPTS}/create-ranger-services.py
-fi
+ printf 'export RANGER_ADMIN_LOGBACK_CONF_FILE=%s\n' "${RANGER_ADMIN_LOGBACK_CONF_FILE}" > "${env_logback}"
+ chmod 755 "${env_logback}"
+}
+
+admin_pid() {
+ local pid_dir="${RANGER_PID_DIR_PATH:-/var/run/ranger}"
+ local pid_name="${RANGER_ADMIN_PID_NAME:-rangeradmin.pid}"
+ local pidf="${pid_dir}/${pid_name}"
+
+ if [ -f "${pidf}" ]; then
+ cat "${pidf}" 2>/dev/null || true
+ return 0
+ fi
+
+ ps -ef | grep java | grep -- '-Dproc_rangeradmin' | grep -v grep | awk '{ print $2 }' | head -n 1
+}
-RANGER_ADMIN_PID=`ps -ef | grep -v grep | grep -i "org.apache.ranger.server.tomcat.EmbeddedServer" | awk '{print $2}'`
+wait_for_admin() {
+ local timeout_s="${1:-180}"
+ local start
+ start="$(date +%s)"
-# prevent the container from exiting
-if [ -z "$RANGER_ADMIN_PID" ]
-then
- echo "Ranger Admin process probably exited, no process id found!"
-else
- tail --pid=$RANGER_ADMIN_PID -f /dev/null
+ while true; do
+ local pid
+ pid="$(admin_pid || true)"
+ if [ -n "${pid}" ] && ps -p "${pid}" >/dev/null 2>&1; then
+ if command -v curl >/dev/null 2>&1; then
+ # login.jsp returns 200 once the webapp is fully initialized
+ if curl -fsS "http://127.0.0.1:6080/login.jsp" >/dev/null 2>&1; then
+ return 0
+ fi
+ else
+ return 0
+ fi
+ fi
+
+ if [ $(( $(date +%s) - start )) -ge "${timeout_s}" ]; then
+ return 1
+ fi
+ sleep 3
+ done
+}
+
+port_open() {
+ local host="$1"
+ local port="$2"
+
+ if command -v nc >/dev/null 2>&1; then
+ nc -z -w 2 "${host}" "${port}" >/dev/null 2>&1
+ return $?
+ fi
+
+ # Fallback: bash /dev/tcp (may be disabled in some environments)
+ (exec 3<>"/dev/tcp/${host}/${port}") >/dev/null 2>&1
+}
+
+check_db_ready() {
+ local flavor="$1"
+ local host="$2"
+ local port="$3"
+ local db="$4"
+ local user="$5"
+ local pass="$6"
+
+ case "${flavor}" in
+ POSTGRES|postgres|Postgres|POSTGRESQL|postgresql)
+ if command -v pg_isready >/dev/null 2>&1; then
+ PGPASSWORD="${pass}" pg_isready -h "${host}" -p "${port}" -U "${user}" -d "${db}" >/dev/null 2>&1
+ return $?
+ fi
+ if command -v psql >/dev/null 2>&1; then
+ PGPASSWORD="${pass}" psql "host=${host} port=${port} user=${user} dbname=${db} sslmode=disable" \
+ -v ON_ERROR_STOP=1 -tAc "select 1" >/dev/null 2>&1
+ return $?
+ fi
+ port_open "${host}" "${port}"
+ return $?
+ ;;
+ MYSQL|mysql|MySQL|MARIADB|mariadb)
+ if command -v mysqladmin >/dev/null 2>&1; then
+ MYSQL_PWD="${pass}" mysqladmin ping -h "${host}" -P "${port}" -u "${user}" --silent >/dev/null 2>&1
+ return $?
+ fi
+ if command -v mysql >/dev/null 2>&1; then
+ MYSQL_PWD="${pass}" mysql -h "${host}" -P "${port}" -u "${user}" -D "${db}" -e "select 1" >/dev/null 2>&1
+ return $?
+ fi
+ port_open "${host}" "${port}"
+ return $?
+ ;;
+ *)
+ port_open "${host}" "${port}"
+ return $?
+ ;;
+ esac
+}
+
+wait_for_db_ready_or_timeout() {
+ local timeout_s="${1:-600}"
+
+ local flavor host port db user pass
+ flavor="${DB_FLAVOR:-$(db_config_field flavor)}"
+ host="${RANGER_ADMIN_DB_HOSTNAME:-$(db_config_field host)}"
+ port="${RANGER_ADMIN_DB_PORT:-$(db_config_field port)}"
+ db="${RANGER_ADMIN_DB_DATABASE:-$(db_config_field database)}"
+ user="${RANGER_ADMIN_DB_USERNAME:-$(db_config_field user)}"
+ pass="${RANGER_ADMIN_DB_PASSWORD:-}"
+
+ if [ -z "${host}" ] || [ -z "${port}" ]; then
+ echo "WARNING: DB host/port not configured in ranger-admin-site.xml; skipping DB wait" >&2
+ return 0
+ fi
+ if [ -z "${flavor}" ]; then
+ flavor="POSTGRES"
+ fi
+
+ echo "Waiting for DB connectivity (flavor=${flavor} host=${host} port=${port} db=${db:-}) with timeout ${timeout_s}s" >&2
+
+ local start now elapsed
+ start="$(date +%s)"
+
+ while true; do
+ if check_db_ready "${flavor}" "${host}" "${port}" "${db:-postgres}" "${user:-postgres}" "${pass:-}"; then
+ echo "DB is reachable" >&2
+ return 0
+ fi
+
+ now="$(date +%s)"
+ elapsed=$(( now - start ))
+ if [ "${elapsed}" -ge "${timeout_s}" ]; then
+ echo "ERROR: Timed out after ${timeout_s}s waiting for DB connectivity (flavor=${flavor} host=${host} port=${port} db=${db:-})" >&2
+ return 1
+ fi
+
+ echo "Waiting for DB connectivity... elapsed=${elapsed}s remaining=$(( timeout_s - elapsed ))s" >&2
+ sleep 5
+ done
+}
+
+cd "${RANGER_ADMIN_DIR}"
+sync_admin_configs
+sync_db_password_property
+ensure_jdbc_driver
+prepare_admin_runtime
+
+wait_for_db_ready_or_timeout 600
+python3 "/home/ranger/scripts/dba.py"
+./ews/ranger-admin-services.sh start
+
+if [ ! -f "${SERVICES_MARKER}" ]; then
+ if wait_for_admin 240; then
+ if python3 "${USER_PASSWORD_BOOTSTRAP_HELPER}"; then
+ if python3 "/home/ranger/scripts/create_services.py"; then
+ touch "${SERVICES_MARKER}" 2>/dev/null || true
+ else
+ echo "Warning: service creation failed" >&2
+ fi
+ else
+ echo "Warning: admin bootstrap failed; skipping service creation" >&2
+ fi
+ else
+ echo "ERROR: Ranger Admin did not become ready in time; skipping service creation" >&2
+ fi
fi
+
+pid="$(admin_pid || true)"
+if [ -n "${pid}" ]; then
+ tail --pid="${pid}" -f /dev/null
+fi
+
+echo "Ranger Admin process id not found; keeping container alive for debugging" >&2
+tail -f /dev/null
diff --git a/dev-support/ranger-docker/scripts/admin/ranger_admin_xml_config.py b/dev-support/ranger-docker/scripts/admin/ranger_admin_xml_config.py
new file mode 100644
index 0000000000..c68bf50d14
--- /dev/null
+++ b/dev-support/ranger-docker/scripts/admin/ranger_admin_xml_config.py
@@ -0,0 +1,209 @@
+import argparse
+import os
+import re
+import sys
+import xml.etree.ElementTree as ET
+
+DEFAULT_RANGER_ADMIN_SITE_CANDIDATES = (
+ os.environ.get("RANGER_ADMIN_SITE_XML"),
+ os.path.join(
+ os.environ.get(
+ "RANGER_ADMIN_CONF",
+ "/opt/ranger/admin/ews/webapp/WEB-INF/classes/conf",
+ ),
+ "ranger-admin-site.xml",
+ ),
+ "/opt/ranger/admin/configs/ranger-admin-site.xml",
+ "/opt/ranger/admin/ews/webapp/WEB-INF/classes/conf/ranger-admin-site.xml",
+)
+
+
+class RangerAdminXmlConfig:
+ def __init__(self, property_file=None):
+ self.property_file = property_file
+
+ def find_ranger_admin_site_xml(self):
+ candidates = []
+ if self.property_file:
+ candidates.append(self.property_file)
+ for candidate in DEFAULT_RANGER_ADMIN_SITE_CANDIDATES:
+ if candidate:
+ candidates.append(candidate)
+
+ for candidate in candidates:
+ if os.path.isfile(candidate):
+ return candidate
+ return ""
+
+ def load_properties(self):
+ property_file = self.find_ranger_admin_site_xml()
+ properties = {}
+ if not property_file:
+ return properties
+
+ try:
+ tree = ET.parse(property_file)
+ root = tree.getroot()
+ for child in root.findall("property"):
+ name_elem = child.find("name")
+ value_elem = child.find("value")
+ if name_elem is None or value_elem is None or not name_elem.text:
+ continue
+ properties[name_elem.text.strip()] = (value_elem.text or "").strip()
+ except (ET.ParseError, AttributeError, OSError):
+ pass
+ return properties
+
+ def get_property(self, property_name):
+ return self.load_properties().get(property_name, "")
+
+ def get_config_value(self, env_var, xml_property, default=""):
+ return os.environ.get(env_var) or self.get_property(xml_property) or default
+
+ @staticmethod
+ def parse_jdbc_url(jdbc_url):
+ info = {"flavor": "", "host": "", "port": "", "database": ""}
+ if not jdbc_url:
+ return info
+
+ match = re.match(r"jdbc:(postgresql|mysql)://([^/:;]+)(?::(\d+))?/([^?;]+)", jdbc_url)
+ if match:
+ flavor = match.group(1).upper()
+ info["flavor"] = "POSTGRES" if flavor == "POSTGRESQL" else flavor
+ info["host"] = match.group(2)
+ info["port"] = match.group(3) or ("5432" if flavor == "POSTGRESQL" else "3306")
+ info["database"] = match.group(4)
+ return info
+
+ match = re.match(r"jdbc:sqlserver://([^:;]+)(?::(\d+))?(?:;|$)", jdbc_url)
+ if match:
+ info["flavor"] = "MSSQL"
+ info["host"] = match.group(1)
+ info["port"] = match.group(2) or "1433"
+ db_match = re.search(r"(?:^|;)databaseName=([^;]+)", jdbc_url)
+ if db_match:
+ info["database"] = db_match.group(1)
+ return info
+
+ match = re.match(r"jdbc:oracle:thin:@//([^/:]+)(?::(\d+))?/([^?;]+)", jdbc_url)
+ if match:
+ info["flavor"] = "ORACLE"
+ info["host"] = match.group(1)
+ info["port"] = match.group(2) or "1521"
+ info["database"] = match.group(3)
+ return info
+
+ match = re.match(r"jdbc:oracle:thin:@([^:]+):(\d+):([^?;]+)", jdbc_url)
+ if match:
+ info["flavor"] = "ORACLE"
+ info["host"] = match.group(1)
+ info["port"] = match.group(2)
+ info["database"] = match.group(3)
+ return info
+
+ def get_db_field(self, field):
+ props = self.load_properties()
+ jdbc_info = self.parse_jdbc_url(props.get("ranger.jpa.jdbc.url", ""))
+ values = {
+ "flavor": jdbc_info.get("flavor", ""),
+ "host": jdbc_info.get("host", ""),
+ "port": jdbc_info.get("port", ""),
+ "database": jdbc_info.get("database", ""),
+ "user": props.get("ranger.jpa.jdbc.user", ""),
+ "password": os.environ.get("RANGER_ADMIN_DB_PASSWORD", ""),
+ }
+ return values.get(field, "")
+
+ def set_property(self, name, value, required, create=False):
+ property_file = self.find_ranger_admin_site_xml()
+ try:
+ tree = ET.parse(property_file)
+ root = tree.getroot()
+ except (ET.ParseError, OSError) as exc:
+ raise SystemExit(f"ERROR: failed to parse {property_file}: {exc}")
+
+ updated = False
+ for prop in root.findall("property"):
+ name_elem = prop.find("name")
+ value_elem = prop.find("value")
+ if name_elem is None or value_elem is None:
+ continue
+ if (name_elem.text or "").strip() != name:
+ continue
+ value_elem.text = value
+ updated = True
+ break
+
+ if create and not updated:
+ prop = ET.SubElement(root, "property")
+ name_elem = ET.SubElement(prop, "name")
+ name_elem.text = name
+ value_elem = ET.SubElement(prop, "value")
+ value_elem.text = value
+ updated = True
+
+ if required and not updated:
+ raise SystemExit(f"ERROR: {name} missing in {property_file}")
+
+ if updated:
+ tree.write(property_file, encoding="unicode")
+
+
+def load_ranger_admin_site_properties(property_file=None):
+ return RangerAdminXmlConfig(property_file).load_properties()
+
+
+def parse_jdbc_url(jdbc_url):
+ return RangerAdminXmlConfig.parse_jdbc_url(jdbc_url)
+
+
+def get_ranger_client():
+ from apache_ranger.client.ranger_client import RangerClient
+
+ admin_pass = os.environ.get("RANGER_ADMIN_PASSWORD", "")
+ return RangerClient("http://localhost:6080", ("admin", admin_pass))
+
+
+def build_cli_parser():
+ parser = argparse.ArgumentParser(description="Read and update Ranger admin XML properties")
+ subparsers = parser.add_subparsers(dest="command", required=True)
+
+ get_prop_parser = subparsers.add_parser("get-property")
+ get_prop_parser.add_argument("--file")
+ get_prop_parser.add_argument("--name", required=True)
+
+ get_db_field_parser = subparsers.add_parser("get-db-field")
+ get_db_field_parser.add_argument("--file")
+ get_db_field_parser.add_argument("--field", required=True)
+
+ set_prop_parser = subparsers.add_parser("set-property")
+ set_prop_parser.add_argument("--file")
+ set_prop_parser.add_argument("--name", required=True)
+ set_prop_parser.add_argument("--value", required=True)
+ set_prop_parser.add_argument("--required", action="store_true")
+ set_prop_parser.add_argument("--create", action="store_true")
+
+ return parser
+
+
+def main(argv=None):
+ args = build_cli_parser().parse_args(argv)
+ config = RangerAdminXmlConfig(args.file)
+
+ if args.command == "get-property":
+ print(config.get_property(args.name))
+ return 0
+
+ if args.command == "get-db-field":
+ print(config.get_db_field(args.field))
+ return 0
+
+ if args.command == "set-property":
+ config.set_property(args.name, args.value, args.required, args.create)
+ return 0
+
+ return 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/dev-support/ranger-docker/scripts/admin/user_password_bootstrap.py b/dev-support/ranger-docker/scripts/admin/user_password_bootstrap.py
new file mode 100644
index 0000000000..c14fe07aa4
--- /dev/null
+++ b/dev-support/ranger-docker/scripts/admin/user_password_bootstrap.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+
+import os
+import sys
+
+from apache_ranger.client.ranger_client import RangerClient
+from apache_ranger.client.ranger_user_mgmt_client import RangerUserMgmtClient
+from apache_ranger.exceptions import RangerServiceException
+
+from log_config import configure_logging, get_logger
+
+logger = get_logger(__name__)
+
+DEFAULT_BASE_URL = os.environ.get("RANGER_ADMIN_BASE_URL", "http://127.0.0.1:6080").rstrip("/")
+
+
+class UserPasswordBootstrap:
+ def __init__(self, base_url: str):
+ self.base_url = base_url.rstrip("/")
+
+ def _get_user_mgmt_client(self, username: str, password: str) -> RangerUserMgmtClient:
+ return RangerUserMgmtClient(RangerClient(self.base_url, (username, password)))
+
+ def auth_probe_status(self, username: str, password: str) -> int:
+ try:
+ user_mgmt = self._get_user_mgmt_client(username, password)
+ result = user_mgmt.client_http.call_api(RangerUserMgmtClient.FIND_USERS)
+ return 200 if result is not None else 0
+ except RangerServiceException as exc:
+ if exc.statusCode == 403:
+ return 403
+ if exc.statusCode == 401:
+ return 401
+ logger.debug("apache-ranger auth probe failed for %s: %s", username, exc, exc_info=True)
+ return exc.statusCode
+ except Exception as exc:
+ logger.debug("apache-ranger auth probe failed for %s: %s", username, exc, exc_info=True)
+ return 0
+
+ def can_authenticate(self, username: str, password: str) -> bool:
+ return self.auth_probe_status(username, password) in (200, 403)
+
+ def _update_user_password_with_client(self, admin_password: str, username: str, desired: str) -> None:
+ user_mgmt = self._get_user_mgmt_client("admin", admin_password)
+ user = user_mgmt.get_user(username)
+ if user is None or user.id is None:
+ raise ValueError(f"Unable to find Ranger user {username}")
+
+ user.password = desired
+ user_mgmt.update_user_by_id(user.id, user)
+
+ def update_user_password(self, admin_password: str, username: str, desired: str) -> int:
+ try:
+ self._update_user_password_with_client(admin_password, username, desired)
+ return 0
+ except RangerServiceException as exc:
+ logger.error("apache-ranger client update failed for %s: status=%s, message=%s", username, exc.statusCode, exc.msgDesc or exc)
+ return 1
+ except Exception as exc:
+ logger.error("apache-ranger client update failed for %s: %s", username, exc)
+ return 1
+
+ def set_admin_password_if_needed(self, desired: str) -> int:
+ if not desired:
+ logger.warning("Ranger admin password not configured; skipping admin password update")
+ return 0
+
+ if self.can_authenticate("admin", desired):
+ return 0
+
+ logger.warning("Unable to authenticate to Ranger as admin with RANGER_ADMIN_PASSWORD.")
+ logger.warning(" admin: -> %s", self.auth_probe_status("admin", desired))
+ logger.warning("For fresh installs, dba.py seeds the initial admin password from RANGER_ADMIN_PASSWORD during schema import. "
+ "If the database already exists, changing only RANGER_ADMIN_PASSWORD will not rotate the stored admin password."
+ )
+ return 1
+
+ def update_user_password_if_needed(self, admin_password: str, username: str, desired: str) -> int:
+ if not desired:
+ logger.warning("No password configured for %s; skipping password update", username)
+ return 0
+
+ if self.can_authenticate(username, desired):
+ return 0
+
+ if not self.can_authenticate("admin", admin_password):
+ logger.warning("Unable to authenticate as admin; skipping password update for %s.", username)
+ logger.warning(" admin: -> %s", self.auth_probe_status("admin", admin_password))
+ return 0
+
+ logger.info("Updating Ranger user password for %s to configured value", username)
+ if self.update_user_password(admin_password, username, desired) != 0:
+ return 1
+
+ if not self.can_authenticate(username, desired):
+ logger.error("Password update succeeded but auth check failed for %s", username)
+ return 1
+
+ return 0
+
+ def run(self, admin_password: str, usersync_password: str, tagsync_password: str) -> int:
+ if self.set_admin_password_if_needed(admin_password) != 0:
+ return 1
+ if self.update_user_password_if_needed(admin_password, "rangerusersync", usersync_password) != 0:
+ return 1
+ if self.update_user_password_if_needed(admin_password, "rangertagsync", tagsync_password) != 0:
+ return 1
+ return 0
+
+
+def main() -> int:
+ configure_logging()
+
+ bootstrap = UserPasswordBootstrap(DEFAULT_BASE_URL)
+ return bootstrap.run(
+ admin_password=os.environ.get("RANGER_ADMIN_PASSWORD", ""),
+ usersync_password=os.environ.get("RANGER_USERSYNC_PASSWORD", ""),
+ tagsync_password=os.environ.get("RANGER_TAGSYNC_PASSWORD", ""),
+ )
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/dev-support/ranger-docker/scripts/python/log_config.py b/dev-support/ranger-docker/scripts/python/log_config.py
new file mode 100644
index 0000000000..9441b5f31d
--- /dev/null
+++ b/dev-support/ranger-docker/scripts/python/log_config.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+
+import logging
+import os
+
+
+DEFAULT_LOG_LEVEL = os.environ.get("RANGER_ADMIN_PY_LOG_LEVEL", "DEBUG").upper()
+DEFAULT_LOG_FORMAT = os.environ.get("RANGER_ADMIN_PY_LOG_FORMAT", "%(asctime)-15s %(levelname)s %(message)s")
+DEFAULT_LOGGER_LEVELS = {
+ "apache_ranger": os.environ.get("RANGER_ADMIN_PY_APACHE_RANGER_LOG_LEVEL", "INFO"),
+}
+
+_LOGGING_CONFIGURED = False
+
+
+def _parse_level(level_name):
+ if isinstance(level_name, int):
+ return level_name
+ return getattr(logging, str(level_name).upper(), logging.INFO)
+
+
+def configure_logging(default_level=DEFAULT_LOG_LEVEL, logger_levels=None):
+ global _LOGGING_CONFIGURED
+
+ if _LOGGING_CONFIGURED:
+ return
+
+ logging.basicConfig(format=DEFAULT_LOG_FORMAT, level=_parse_level(default_level))
+
+ levels = dict(DEFAULT_LOGGER_LEVELS)
+ if logger_levels:
+ levels.update(logger_levels)
+
+ for logger_name, logger_level in levels.items():
+ logging.getLogger(logger_name).setLevel(_parse_level(logger_level))
+
+ _LOGGING_CONFIGURED = True
+
+
+def get_logger(name):
+ return logging.getLogger(name)