diff --git a/.github/workflows/ca-clone-replicated-ds-test.yml b/.github/workflows/ca-clone-replicated-ds-test.yml index be7b57b40a0..e20810c0797 100644 --- a/.github/workflows/ca-clone-replicated-ds-test.yml +++ b/.github/workflows/ca-clone-replicated-ds-test.yml @@ -103,6 +103,10 @@ jobs: docker exec secondary pki-server create docker exec secondary pki-server nss-create --no-password + - name: Create secondary CA subsystem + run: | + docker exec secondary pki-server ca-create -v + - name: Import system certs and keys into secondary CA run: | docker exec secondary pki \ @@ -143,6 +147,22 @@ jobs: --bind-dn="cn=Replication Manager,cn=config" \ --bind-passwd=Secret.123 + # check replication manager + docker exec primaryds ldapsearch \ + -H ldap://primaryds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -x \ + -b "cn=Replication Manager,cn=config" + + # check replica object + docker exec primaryds ldapsearch \ + -H ldap://primaryds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -x \ + -b "cn=replica,cn=dc\3Dca\2Cdc\3Dpki\2Cdc\3Dexample\2Cdc\3Dcom,cn=mapping tree,cn=config" + - name: Enable replication on secondary DS run: | docker exec secondaryds dsconf \ @@ -156,7 +176,23 @@ jobs: --bind-dn="cn=Replication Manager,cn=config" \ --bind-passwd=Secret.123 - - name: Create replication manager on primary DS + # check replication manager + docker exec secondaryds ldapsearch \ + -H ldap://secondaryds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -x \ + -b "cn=Replication Manager,cn=config" + + # check replica object + docker exec secondaryds ldapsearch \ + -H ldap://secondaryds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -x \ + -b "cn=replica,cn=dc\3Dca\2Cdc\3Dpki\2Cdc\3Dexample\2Cdc\3Dcom,cn=mapping tree,cn=config" + + - name: Create replication agreement on primary DS run: | docker exec primaryds dsconf \ -D "cn=Directory Manager" \ @@ -172,7 +208,16 @@ jobs: --bind-method=SIMPLE \ primaryds-to-secondaryds - - name: Create replication manager on secondary DS + # check replication agreement + docker exec primaryds ldapsearch \ + -H ldap://primaryds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -x \ + -b "cn=primaryds-to-secondaryds,cn=replica,cn=dc\3Dca\2Cdc\3Dpki\2Cdc\3Dexample\2Cdc\3Dcom,cn=mapping tree,cn=config" \ + dn + + - name: Create replication agreement on secondary DS run: | docker exec secondaryds dsconf \ -D "cn=Directory Manager" \ @@ -188,38 +233,24 @@ jobs: --bind-method=SIMPLE \ secondaryds-to-primaryds - - name: Initializing replication agreement - run: | - # start initialization - docker exec primaryds dsconf \ + # check replication agreement + docker exec secondaryds ldapsearch \ + -H ldap://secondaryds.example.com:3389 \ -D "cn=Directory Manager" \ -w Secret.123 \ - ldap://primaryds.example.com:3389 \ - repl-agmt init \ - --suffix=dc=ca,dc=pki,dc=example,dc=com \ - primaryds-to-secondaryds + -x \ + -b "cn=secondaryds-to-primaryds,cn=replica,cn=dc\3Dca\2Cdc\3Dpki\2Cdc\3Dexample\2Cdc\3Dcom,cn=mapping tree,cn=config" \ + dn - # wait for initialization to complete - counter=0 - while [[ "$counter" -lt 30 ]]; do - sleep 1 - - docker exec primaryds dsconf \ - -D "cn=Directory Manager" \ - -w Secret.123 \ - ldap://primaryds.example.com:3389 \ - repl-agmt init-status \ - --suffix=dc=ca,dc=pki,dc=example,dc=com \ - primaryds-to-secondaryds \ - > >(tee stdout) 2> >(tee stderr >&2) || true - - STDOUT=$(cat stdout) - if [ "$STDOUT" = "Agreement successfully initialized." ]; then - break - fi - - counter=$((counter+1)) - done + - name: Initializing replication agreement + run: | + docker exec secondary pki-server ca-db-repl-agmt-init \ + --url ldap://primaryds.example.com:3389 \ + --bind-dn "cn=Directory Manager" \ + --bind-password Secret.123 \ + --suffix dc=ca,dc=pki,dc=example,dc=com \ + -v \ + primaryds-to-secondaryds - name: Check entries in primary DS and secondary DS run: | diff --git a/base/server/python/pki/server/cli/db.py b/base/server/python/pki/server/cli/db.py index f7f961591db..70b5f5a4ac8 100644 --- a/base/server/python/pki/server/cli/db.py +++ b/base/server/python/pki/server/cli/db.py @@ -27,6 +27,7 @@ import subprocess import sys import textwrap +import urllib.parse import pki.cli import pki.server.instance @@ -220,6 +221,7 @@ def __init__(self, parent): self.add_module(SubsystemDBAccessCLI(self)) self.add_module(SubsystemDBIndexCLI(self)) + self.add_module(SubsystemDBReplicationCLI(self)) self.add_module(SubsystemDBVLVCLI(self)) @staticmethod @@ -1325,6 +1327,159 @@ def execute(self, argv): subsystem.rebuild_indexes() +class SubsystemDBReplicationCLI(pki.cli.CLI): + ''' + {subsystem} replication management commands + ''' + + def __init__(self, parent): + super(SubsystemDBReplicationCLI, self).__init__( + 'repl', + inspect.cleandoc(self.__class__.__doc__).format( + subsystem=parent.parent.name.upper())) + + self.parent = parent + self.add_module(SubsystemDBReplicationAgreementCLI(self)) + + +class SubsystemDBReplicationAgreementCLI(pki.cli.CLI): + ''' + {subsystem} replication agreement management commands + ''' + + def __init__(self, parent): + super(SubsystemDBReplicationAgreementCLI, self).__init__( + 'agmt', + inspect.cleandoc(self.__class__.__doc__).format( + subsystem=parent.parent.name.upper())) + + self.parent = parent + self.add_module(SubsystemDBReplicationAgreementInitCLI(self)) + + +class SubsystemDBReplicationAgreementInitCLI(pki.cli.CLI): + ''' + Initialize {subsystem} replication agreement + ''' + + help = '''\ + Usage: pki-server {subsystem}-db-repl-agmt-init [OPTIONS] + + -i, --instance Instance ID (default: pki-tomcat) + --url Database URL + --bind-dn Database bind DN + --bind-password Database bind password + --suffix Database suffix DN + -v, --verbose Run in verbose mode. + --debug Run in debug mode. + --help Show help message. + ''' + + def __init__(self, parent): + super(SubsystemDBReplicationAgreementInitCLI, self).__init__( + 'init', + inspect.cleandoc(self.__class__.__doc__).format( + subsystem=parent.parent.parent.parent.name.upper())) + + self.parent = parent + + def print_help(self): + print(textwrap.dedent(self.__class__.help).format( + subsystem=self.parent.parent.parent.parent.name)) + + def execute(self, argv): + try: + opts, args = getopt.gnu_getopt(argv, 'i:v', [ + 'instance=', + 'url=', 'bind-dn=', 'bind-password=', 'suffix=', + 'verbose', 'debug', 'help']) + + except getopt.GetoptError as e: + logger.error(e) + self.print_help() + sys.exit(1) + + instance_name = 'pki-tomcat' + subsystem_name = self.parent.parent.parent.parent.name + url = None + bind_dn = None + bind_password = None + suffix = None + + for o, a in opts: + if o in ('-i', '--instance'): + instance_name = a + + elif o == '--url': + url = urllib.parse.urlparse(a) + + elif o == '--bind-dn': + bind_dn = a + + elif o == '--bind-password': + bind_password = a + + elif o == '--suffix': + suffix = a + + elif o in ('-v', '--verbose'): + logging.getLogger().setLevel(logging.INFO) + + elif o == '--debug': + logging.getLogger().setLevel(logging.DEBUG) + + elif o == '--help': + self.print_help() + sys.exit() + + else: + logger.error('Invalid option: %s', o) + self.print_help() + sys.exit(1) + + if len(args) < 1: + logger.error('Missing replication agreement name') + self.print_help() + sys.exit(1) + + name = args[0] + + instance = pki.server.instance.PKIInstance(instance_name) + + if not instance.exists(): + logger.error('Invalid instance: %s', instance_name) + sys.exit(1) + + instance.load() + + subsystem = instance.get_subsystem(subsystem_name) + + if not subsystem: + logger.error('No %s subsystem in instance %s.', + subsystem_name.upper(), instance_name) + sys.exit(1) + + ldap_config = {} + + if url.scheme == 'ldaps': + ldap_config['ldapconn.secureConn'] = 'true' + else: + ldap_config['ldapconn.secureConn'] = 'false' + + ldap_config['ldapconn.host'] = url.hostname + ldap_config['ldapconn.port'] = str(url.port) + + ldap_config['ldapauth.authtype'] = 'BasicAuth' + ldap_config['ldapauth.bindDN'] = bind_dn + ldap_config['ldapauth.bindPassword'] = bind_password + + ldap_config['basedn'] = suffix + + subsystem.init_replication_agreement( + name, + ldap_config) + + class SubsystemDBVLVCLI(pki.cli.CLI): def __init__(self, parent): diff --git a/base/server/python/pki/server/deployment/__init__.py b/base/server/python/pki/server/deployment/__init__.py index ab709121f32..7d4b0165870 100644 --- a/base/server/python/pki/server/deployment/__init__.py +++ b/base/server/python/pki/server/deployment/__init__.py @@ -1598,6 +1598,41 @@ def setup_database(self, subsystem, master_config): replica_replication_port=replica_replication_port, replication_security=replication_security) + logger.info('Initializing replication agreement') + + hostname = self.mdict['pki_hostname'] + agreement_name = 'masterAgreement1-%s-%s' % (hostname, self.instance.name) + + # get master's database config + ldap_config = {} + for name in master_config['Properties']: + + match = re.match(r'internaldb\.(.*)$', name) + + if not match: + continue + + new_name = match.group(1) # strip internaldb prefix + + if new_name == 'replication.password': # unused + continue + + elif new_name == 'ldapauth.bindPWPrompt': # unused + continue + + elif new_name.startswith('_'): # comments + continue + + elif new_name == 'ldapauth.password': # rename + new_name = 'ldapauth.bindPassword' + + value = master_config['Properties'][name] + ldap_config[new_name] = value + + subsystem.init_replication_agreement( + agreement_name, + ldap_config) + # For security a PKI subsystem can be configured to use a database user # that only has a limited access to the database (instead of cn=Directory # Manager that has a full access to the database). diff --git a/base/server/python/pki/server/subsystem.py b/base/server/python/pki/server/subsystem.py index 453a0ee372f..ab1dfe14afa 100644 --- a/base/server/python/pki/server/subsystem.py +++ b/base/server/python/pki/server/subsystem.py @@ -1326,6 +1326,35 @@ def setup_replication( finally: shutil.rmtree(tmpdir) + def init_replication_agreement( + self, + name, + ldap_config): + + tmpdir = tempfile.mkdtemp() + try: + ldap_config_file = os.path.join(tmpdir, 'ldap.conf') + pki.util.store_properties(ldap_config_file, ldap_config) + pki.util.chown(tmpdir, self.instance.uid, self.instance.gid) + + cmd = [self.name + '-db-repl-agmt-init'] + + if ldap_config_file: + cmd.extend(['--ldap-config', ldap_config_file]) + + if logger.isEnabledFor(logging.DEBUG): + cmd.append('--debug') + + elif logger.isEnabledFor(logging.INFO): + cmd.append('--verbose') + + cmd.append(name) + + self.run(cmd) + + finally: + shutil.rmtree(tmpdir) + def find_vlv(self, as_current_user=False): cmd = [self.name + '-db-vlv-find'] diff --git a/base/server/src/main/java/org/dogtagpki/server/cli/SubsystemDBReplicationAgreementCLI.java b/base/server/src/main/java/org/dogtagpki/server/cli/SubsystemDBReplicationAgreementCLI.java new file mode 100644 index 00000000000..b2b6e67b945 --- /dev/null +++ b/base/server/src/main/java/org/dogtagpki/server/cli/SubsystemDBReplicationAgreementCLI.java @@ -0,0 +1,23 @@ +// +// Copyright Red Hat, Inc. +// +// SPDX-License-Identifier: GPL-2.0-or-later +// +package org.dogtagpki.server.cli; + +import org.dogtagpki.cli.CLI; + +/** + * @author Endi S. Dewata + */ +public class SubsystemDBReplicationAgreementCLI extends CLI { + + public SubsystemDBReplicationAgreementCLI(CLI parent) { + super( + "agmt", + parent.parent.parent.name.toUpperCase() + " replication agreement management commands", + parent); + + addModule(new SubsystemDBReplicationAgreementInitCLI(this)); + } +} diff --git a/base/server/src/main/java/org/dogtagpki/server/cli/SubsystemDBReplicationAgreementInitCLI.java b/base/server/src/main/java/org/dogtagpki/server/cli/SubsystemDBReplicationAgreementInitCLI.java new file mode 100644 index 00000000000..7084d4fe88f --- /dev/null +++ b/base/server/src/main/java/org/dogtagpki/server/cli/SubsystemDBReplicationAgreementInitCLI.java @@ -0,0 +1,98 @@ +// +// Copyright Red Hat, Inc. +// +// SPDX-License-Identifier: GPL-2.0-or-later +// +package org.dogtagpki.server.cli; + +import org.apache.commons.cli.CommandLine; +import org.dogtagpki.cli.CLI; +import org.dogtagpki.cli.CLIException; +import org.dogtagpki.util.logging.PKILogger; +import org.dogtagpki.util.logging.PKILogger.LogLevel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.netscape.cms.servlet.csadmin.LDAPConfigurator; +import com.netscape.cmscore.apps.EngineConfig; +import com.netscape.cmscore.base.ConfigStorage; +import com.netscape.cmscore.base.FileConfigStorage; +import com.netscape.cmscore.ldapconn.LDAPConfig; +import com.netscape.cmscore.ldapconn.LdapBoundConnFactory; +import com.netscape.cmscore.ldapconn.PKISocketConfig; + +import netscape.ldap.LDAPConnection; + +/** + * @author Endi S. Dewata + */ +public class SubsystemDBReplicationAgreementInitCLI extends SubsystemCLI { + + public static Logger logger = LoggerFactory.getLogger(SubsystemDBReplicationAgreementInitCLI.class); + + public SubsystemDBReplicationAgreementInitCLI(CLI parent) { + super( + "init", + "Initialize " + parent.parent.parent.parent.getName().toUpperCase() + " replication agreement", + parent); + } + + @Override + public void createOptions() { + + options.addOption(null, "ldap-config", true, "LDAP configuration file"); + + options.addOption("v", "verbose", false, "Run in verbose mode."); + options.addOption(null, "debug", false, "Run in debug mode."); + options.addOption(null, "help", false, "Show help message."); + } + + @Override + public void execute(CommandLine cmd) throws Exception { + + String[] cmdArgs = cmd.getArgs(); + + if (cmdArgs.length < 1) { + throw new CLIException("Missing replication agreement name"); + } + + String agreementName = cmdArgs[0]; + + if (cmd.hasOption("debug")) { + PKILogger.setLevel(LogLevel.DEBUG); + + } else if (cmd.hasOption("verbose")) { + PKILogger.setLevel(LogLevel.INFO); + } + + String ldapConfigFile = cmd.getOptionValue("ldap-config"); + + if (ldapConfigFile == null) { + throw new Exception("Missing LDAP configuration file"); + } + + initializeTomcatJSS(); + String subsystem = parent.parent.parent.parent.getName(); + EngineConfig cs = getEngineConfig(subsystem); + cs.load(); + + PKISocketConfig socketConfig = cs.getSocketConfig(); + + logger.info("Loading {}", ldapConfigFile); + ConfigStorage configStorage = new FileConfigStorage(ldapConfigFile); + LDAPConfig ldapConfig = new LDAPConfig(configStorage); + ldapConfig.load(); + + LdapBoundConnFactory connFactory = new LdapBoundConnFactory("LDAPConfigurator"); + connFactory.init(socketConfig, ldapConfig); + LDAPConnection conn = connFactory.getConn(); + + try { + LDAPConfigurator configurator = new LDAPConfigurator(conn, ldapConfig); + configurator.initializeReplicationAgreement(agreementName); + + } finally { + if (conn != null) conn.disconnect(); + } + } +} diff --git a/base/server/src/main/java/org/dogtagpki/server/cli/SubsystemDBReplicationCLI.java b/base/server/src/main/java/org/dogtagpki/server/cli/SubsystemDBReplicationCLI.java index 064b9df2eb8..7daa3220a7b 100644 --- a/base/server/src/main/java/org/dogtagpki/server/cli/SubsystemDBReplicationCLI.java +++ b/base/server/src/main/java/org/dogtagpki/server/cli/SubsystemDBReplicationCLI.java @@ -16,5 +16,6 @@ public SubsystemDBReplicationCLI(CLI parent) { super("repl", parent.parent.name.toUpperCase() + " database replication management commands", parent); addModule(new SubsystemDBReplicationSetupCLI(this)); + addModule(new SubsystemDBReplicationAgreementCLI(this)); } } diff --git a/base/server/src/main/java/org/dogtagpki/server/cli/SubsystemDBReplicationSetupCLI.java b/base/server/src/main/java/org/dogtagpki/server/cli/SubsystemDBReplicationSetupCLI.java index 02d9cb9fbbe..5207b71ab15 100644 --- a/base/server/src/main/java/org/dogtagpki/server/cli/SubsystemDBReplicationSetupCLI.java +++ b/base/server/src/main/java/org/dogtagpki/server/cli/SubsystemDBReplicationSetupCLI.java @@ -181,9 +181,6 @@ public void setupReplication( logger.info("New replica number range: " + beginReplicaNumber + "-" + endReplicaNumber); dbConfig.putString("beginReplicaNumber", Integer.toString(beginReplicaNumber)); - logger.info("Initializing replication agreement"); - masterConfigurator.initializeReplicationAgreement(masterAgreementName); - } finally { if (masterConn != null) masterConn.disconnect(); }