From abe49da255ec1e53c3bcff90bbe51f15390d4a66 Mon Sep 17 00:00:00 2001 From: "Endi S. Dewata" Date: Mon, 11 Nov 2024 10:08:39 -0600 Subject: [PATCH] Update Python API to support REST API v2 The PKIClient class has been added to replace PKIConnection as the main access point to PKI services. By default it will use REST API v2, then fall back to v1 if it's not available. Optionally, PKIClient can be configured to use a specific REST API version. The InfoClient, CertClient, AccountClient, and UserClient classes have been added/updated to construct the proper REST URL according to the REST API version in PKIClient. The pki-healthcheck has been updated to use PKIClient. Some simple Python scripts have also been added to demonstrate how to use PKIClient. New tests have been added to run these scripts against the current CA and KRA which support both REST API v1 and v2 and also against an older CA that only supports REST API v1. --- .../workflows/python-ca-rest-api-v1-test.yml | 646 ++++++++++++++++++ .github/workflows/python-ca-test.yml | 281 ++++++++ .github/workflows/python-kra-test.yml | 244 +++++++ .github/workflows/python-tests.yml | 15 + base/common/python/pki/account.py | 80 ++- base/common/python/pki/ca.py | 17 + base/common/python/pki/cert.py | 42 +- base/common/python/pki/client.py | 106 ++- base/common/python/pki/info.py | 48 +- base/common/python/pki/kra.py | 57 +- base/common/python/pki/user.py | 37 + .../server/healthcheck/meta/connectivity.py | 32 +- tests/bin/pki-info.py | 58 ++ tests/ca/bin/pki-ca-cert-find.py | 71 ++ tests/ca/bin/pki-ca-user-find.py | 88 +++ tests/kra/bin/pki-kra-user-find.py | 88 +++ 16 files changed, 1855 insertions(+), 55 deletions(-) create mode 100644 .github/workflows/python-ca-rest-api-v1-test.yml create mode 100644 .github/workflows/python-ca-test.yml create mode 100644 .github/workflows/python-kra-test.yml create mode 100644 base/common/python/pki/ca.py create mode 100644 base/common/python/pki/user.py create mode 100755 tests/bin/pki-info.py create mode 100755 tests/ca/bin/pki-ca-cert-find.py create mode 100755 tests/ca/bin/pki-ca-user-find.py create mode 100755 tests/kra/bin/pki-kra-user-find.py diff --git a/.github/workflows/python-ca-rest-api-v1-test.yml b/.github/workflows/python-ca-rest-api-v1-test.yml new file mode 100644 index 00000000000..ee4a3f9a026 --- /dev/null +++ b/.github/workflows/python-ca-rest-api-v1-test.yml @@ -0,0 +1,646 @@ +name: CA Python API with REST API v1 + +on: workflow_call + +env: + DS_IMAGE: ${{ vars.DS_IMAGE || 'quay.io/389ds/dirsrv' }} + +jobs: + # https://github.com/dogtagpki/pki/wiki/Deploying-CA-Container + test: + name: Test + runs-on: ubuntu-latest + env: + SHARED: /tmp/workdir/pki + steps: + - name: Clone repository + uses: actions/checkout@v4 + + - name: Retrieve PKI images + uses: actions/cache@v4 + with: + key: pki-images-${{ github.sha }} + path: pki-images.tar + + - name: Load PKI images + run: docker load --input pki-images.tar + + - name: Create network + run: docker network create example + + - name: Set up client container + run: | + tests/bin/runner-init.sh \ + --hostname=client.example.com \ + --network=example \ + client + + #################################################################################################### + # Create system certs + + - name: Create CA signing cert + run: | + mkdir certs + + docker exec client pki \ + nss-cert-request \ + --subject "CN=CA Signing Certificate" \ + --ext /usr/share/pki/server/certs/ca_signing.conf \ + --csr $SHARED/certs/ca_signing.csr + + docker exec client pki \ + nss-cert-issue \ + --csr $SHARED/certs/ca_signing.csr \ + --ext /usr/share/pki/server/certs/ca_signing.conf \ + --cert $SHARED/certs/ca_signing.crt + + docker exec client pki \ + nss-cert-import \ + --cert $SHARED/certs/ca_signing.crt \ + --trust CT,C,C \ + ca_signing + + docker exec client pki \ + nss-cert-show \ + ca_signing + + - name: Create OCSP signing cert + run: | + docker exec client pki \ + nss-cert-request \ + --subject "CN=OCSP Signing Certificate" \ + --ext /usr/share/pki/server/certs/ocsp_signing.conf \ + --csr $SHARED/certs/ocsp_signing.csr + + docker exec client pki \ + nss-cert-issue \ + --issuer ca_signing \ + --csr $SHARED/certs/ocsp_signing.csr \ + --ext /usr/share/pki/server/certs/ocsp_signing.conf \ + --cert $SHARED/certs/ocsp_signing.crt + + docker exec client pki \ + nss-cert-import \ + --cert $SHARED/certs/ocsp_signing.crt \ + ocsp_signing + + docker exec client pki \ + nss-cert-show \ + ocsp_signing + + - name: Create audit signing cert + run: | + docker exec client pki \ + nss-cert-request \ + --subject "CN=Audit Signing Certificate" \ + --ext /usr/share/pki/server/certs/audit_signing.conf \ + --csr $SHARED/certs/audit_signing.csr + + docker exec client pki \ + nss-cert-issue \ + --issuer ca_signing \ + --csr $SHARED/certs/audit_signing.csr \ + --ext /usr/share/pki/server/certs/audit_signing.conf \ + --cert $SHARED/certs/audit_signing.crt + + docker exec client pki \ + nss-cert-import \ + --cert $SHARED/certs/audit_signing.crt \ + --trust ,,P \ + audit_signing + + docker exec client pki \ + nss-cert-show \ + audit_signing + + - name: Create subsystem cert + run: | + docker exec client pki \ + nss-cert-request \ + --subject "CN=Subsystem Certificate" \ + --ext /usr/share/pki/server/certs/subsystem.conf \ + --csr $SHARED/certs/subsystem.csr + + docker exec client pki \ + nss-cert-issue \ + --issuer ca_signing \ + --csr $SHARED/certs/subsystem.csr \ + --ext /usr/share/pki/server/certs/subsystem.conf \ + --cert $SHARED/certs/subsystem.crt + + docker exec client pki \ + nss-cert-import \ + --cert $SHARED/certs/subsystem.crt \ + subsystem + + docker exec client pki \ + nss-cert-show \ + subsystem + + - name: Create SSL server cert + run: | + docker exec client pki \ + nss-cert-request \ + --subject "CN=ca.example.com" \ + --ext /usr/share/pki/server/certs/sslserver.conf \ + --csr $SHARED/certs/sslserver.csr + + docker exec client pki \ + nss-cert-issue \ + --issuer ca_signing \ + --csr $SHARED/certs/sslserver.csr \ + --ext /usr/share/pki/server/certs/sslserver.conf \ + --cert $SHARED/certs/sslserver.crt + + docker exec client pki \ + nss-cert-import \ + --cert $SHARED/certs/sslserver.crt \ + sslserver + + docker exec client pki \ + nss-cert-show \ + sslserver + + - name: Create admin cert + run: | + docker exec client pki \ + nss-cert-request \ + --subject "CN=Administrator" \ + --ext /usr/share/pki/server/certs/admin.conf \ + --csr $SHARED/certs/admin.csr + + docker exec client pki \ + nss-cert-issue \ + --issuer ca_signing \ + --csr $SHARED/certs/admin.csr \ + --ext /usr/share/pki/server/certs/admin.conf \ + --cert $SHARED/certs/admin.crt + + docker exec client pki \ + nss-cert-import \ + --cert $SHARED/certs/admin.crt \ + admin + + docker exec client pki \ + nss-cert-show \ + admin + + - name: "Export system certs and keys to PKCS #12 file" + run: | + docker exec client pki pkcs12-export \ + --pkcs12 $SHARED/certs/server.p12 \ + --password Secret.123 \ + ca_signing \ + ocsp_signing \ + audit_signing \ + subsystem \ + sslserver + + - name: "Export admin cert and key to PKCS #12 file" + run: | + docker exec client pki pkcs12-export \ + --pkcs12 $SHARED/certs/admin.p12 \ + --password Secret.123 \ + admin + + - name: "Export admin key to PEM file" + run: | + docker exec client openssl pkcs12 \ + -in $SHARED/certs/admin.p12 \ + -passin pass:Secret.123 \ + -out $SHARED/certs/admin.key \ + -nodes \ + -nocerts + + #################################################################################################### + # Set up CA database + + - name: Set up DS container + run: | + tests/bin/ds-create.sh \ + --image=${{ env.DS_IMAGE }} \ + --hostname=ds.example.com \ + --network=example \ + --network-alias=ds.example.com \ + --password=Secret.123 \ + ds + + # https://github.com/dogtagpki/pki/wiki/Setting-up-CA-Database + - name: Configure DS database + run: | + docker exec ds ldapadd \ + -H ldap://ds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -f $SHARED/base/server/database/ds/config.ldif + + - name: Add PKI schema + run: | + docker exec ds ldapmodify \ + -H ldap://ds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -f $SHARED/base/server/database/ds/schema.ldif + + - name: Add CA base entry + run: | + docker exec -i ds ldapadd \ + -H ldap://ds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 << EOF + dn: dc=ca,dc=pki,dc=example,dc=com + objectClass: dcObject + dc: ca + EOF + + - name: Add CA database entries + run: | + sed \ + -e 's/{rootSuffix}/dc=ca,dc=pki,dc=example,dc=com/g' \ + base/ca/database/ds/create.ldif \ + | tee create.ldif + docker exec ds ldapadd \ + -H ldap://ds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -f $SHARED/create.ldif + + - name: Add CA search indexes + run: | + sed \ + -e 's/{database}/userroot/g' \ + base/ca/database/ds/index.ldif \ + | tee index.ldif + docker exec ds ldapadd \ + -H ldap://ds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -f $SHARED/index.ldif + + - name: Rebuild CA search indexes + run: | + # start rebuild task + sed \ + -e 's/{database}/userroot/g' \ + base/ca/database/ds/indextasks.ldif \ + | tee indextasks.ldif + docker exec ds ldapadd \ + -H ldap://ds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -f $SHARED/indextasks.ldif + + # wait for task to complete + while true; do + sleep 1 + + docker exec ds ldapsearch \ + -H ldap://ds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -b "cn=index1160589770, cn=index, cn=tasks, cn=config" \ + -LLL \ + nsTaskExitCode \ + | tee output + + sed -n -e 's/nsTaskExitCode:\s*\(.*\)/\1/p' output > nsTaskExitCode + cat nsTaskExitCode + + if [ -s nsTaskExitCode ]; then + break + fi + done + + echo "0" > expected + diff expected nsTaskExitCode + + - name: Add CA ACL resources + run: | + sed \ + -e 's/{rootSuffix}/dc=ca,dc=pki,dc=example,dc=com/g' \ + base/ca/database/ds/acl.ldif \ + | tee acl.ldif + docker exec ds ldapadd \ + -H ldap://ds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -f $SHARED/acl.ldif + + - name: Add CA VLV indexes + run: | + sed \ + -e 's/{instanceId}/pki-tomcat/g' \ + -e 's/{database}/userroot/g' \ + -e 's/{rootSuffix}/dc=ca,dc=pki,dc=example,dc=com/g' \ + base/ca/database/ds/vlv.ldif \ + | tee vlv.ldif + docker exec ds ldapadd \ + -H ldap://ds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -f $SHARED/vlv.ldif + + - name: Rebuild CA VLV indexes + run: | + # start rebuild task + sed \ + -e 's/{database}/userroot/g' \ + -e 's/{instanceId}/pki-tomcat/g' \ + base/ca/database/ds/vlvtasks.ldif \ + | tee vlvtasks.ldif + docker exec ds ldapadd \ + -H ldap://ds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -f $SHARED/vlvtasks.ldif + + # wait for task to complete + while true; do + sleep 1 + + docker exec ds ldapsearch \ + -H ldap://ds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -b "cn=index1160589769, cn=index, cn=tasks, cn=config" \ + -LLL \ + nsTaskExitCode \ + | tee output + + sed -n -e 's/nsTaskExitCode:\s*\(.*\)/\1/p' output > nsTaskExitCode + cat nsTaskExitCode + + if [ -s nsTaskExitCode ]; then + break + fi + done + + echo "0" > expected + diff expected nsTaskExitCode + + #################################################################################################### + # Set up admin user + + # https://github.com/dogtagpki/pki/wiki/Setting-up-CA-Admin-User + - name: Add admin user + run: | + docker exec -i ds ldapadd \ + -H ldap://ds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 << EOF + dn: uid=admin,ou=people,dc=ca,dc=pki,dc=example,dc=com + objectClass: person + objectClass: organizationalPerson + objectClass: inetOrgPerson + objectClass: cmsuser + cn: admin + sn: admin + uid: admin + mail: admin@example.com + userPassword: Secret.123 + userState: 1 + userType: adminType + EOF + + - name: Assign admin cert to admin user + run: | + # convert cert from PEM to DER + openssl x509 -outform der -in certs/admin.crt -out certs/admin.der + + # get serial number + openssl x509 -text -noout -in certs/admin.crt | tee output + SERIAL=$(sed -En 'N; s/^ *Serial Number:\n *(.*)$/\1/p; D' output) + echo "SERIAL: $SERIAL" + HEX_SERIAL=$(echo "$SERIAL" | tr -d ':') + echo "HEX_SERIAL: $HEX_SERIAL" + DEC_SERIAL=$(python -c "print(int('$HEX_SERIAL', 16))") + echo "DEC_SERIAL: $DEC_SERIAL" + + docker exec -i ds ldapmodify \ + -H ldap://ds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 << EOF + dn: uid=admin,ou=people,dc=ca,dc=pki,dc=example,dc=com + changetype: modify + add: description + description: 2;$DEC_SERIAL;CN=CA Signing Certificate;CN=Administrator + - + add: userCertificate + userCertificate:< file:$SHARED/certs/admin.der + - + EOF + + - name: Add admin user into CA groups + run: | + docker exec -i ds ldapmodify \ + -H ldap://ds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 << EOF + dn: cn=Administrators,ou=groups,dc=ca,dc=pki,dc=example,dc=com + changetype: modify + add: uniqueMember + uniqueMember: uid=admin,ou=people,dc=ca,dc=pki,dc=example,dc=com + - + + dn: cn=Certificate Manager Agents,ou=groups,dc=ca,dc=pki,dc=example,dc=com + changetype: modify + add: uniqueMember + uniqueMember: uid=admin,ou=people,dc=ca,dc=pki,dc=example,dc=com + - + + dn: cn=Security Domain Administrators,ou=groups,dc=ca,dc=pki,dc=example,dc=com + changetype: modify + add: uniqueMember + uniqueMember: uid=admin,ou=people,dc=ca,dc=pki,dc=example,dc=com + - + + dn: cn=Enterprise CA Administrators,ou=groups,dc=ca,dc=pki,dc=example,dc=com + changetype: modify + add: uniqueMember + uniqueMember: uid=admin,ou=people,dc=ca,dc=pki,dc=example,dc=com + - + + dn: cn=Enterprise KRA Administrators,ou=groups,dc=ca,dc=pki,dc=example,dc=com + changetype: modify + add: uniqueMember + uniqueMember: uid=admin,ou=people,dc=ca,dc=pki,dc=example,dc=com + - + + dn: cn=Enterprise RA Administrators,ou=groups,dc=ca,dc=pki,dc=example,dc=com + changetype: modify + add: uniqueMember + uniqueMember: uid=admin,ou=people,dc=ca,dc=pki,dc=example,dc=com + - + + dn: cn=Enterprise TKS Administrators,ou=groups,dc=ca,dc=pki,dc=example,dc=com + changetype: modify + add: uniqueMember + uniqueMember: uid=admin,ou=people,dc=ca,dc=pki,dc=example,dc=com + - + + dn: cn=Enterprise OCSP Administrators,ou=groups,dc=ca,dc=pki,dc=example,dc=com + changetype: modify + add: uniqueMember + uniqueMember: uid=admin,ou=people,dc=ca,dc=pki,dc=example,dc=com + - + + dn: cn=Enterprise TPS Administrators,ou=groups,dc=ca,dc=pki,dc=example,dc=com + changetype: modify + add: uniqueMember + uniqueMember: uid=admin,ou=people,dc=ca,dc=pki,dc=example,dc=com + - + EOF + + #################################################################################################### + # Install CA that only supports REST API v1 + + - name: Create PKI CA 11.4 Dockerfile + run: | + # create a new Dockerfile to disable access log buffer + cat > Dockerfile-pki-ca-11.4 < expected << EOF + GET /pki/v2/info HTTP/1.1 404 - + GET /pki/rest/info HTTP/1.1 200 - + EOF + + diff expected output + + - name: Check CA certs + run: | + docker exec client python /usr/share/pki/tests/ca/bin/pki-ca-cert-find.py \ + -U https://ca.example.com:8443 \ + --ca-bundle $SHARED/certs/ca_signing.crt \ + -v + + sleep 1 + + # check HTTP methods, paths, protocols, status, and authenticated users + docker exec ca find /var/log/pki/pki-tomcat \ + -name "localhost_access_log.*" \ + -exec cat {} \; \ + | tail -3 \ + | sed -e 's/^.* .* \(.*\) \[.*\] "\(.*\)" \(.*\) .*$/\2 \3 \1/' \ + | tee output + + # Python API should fall back to REST API v1 + cat > expected << EOF + GET /pki/v2/info HTTP/1.1 404 - + GET /pki/rest/info HTTP/1.1 200 - + POST /ca/rest/certs/search HTTP/1.1 200 - + EOF + + - name: Check CA users + run: | + docker exec client python /usr/share/pki/tests/ca/bin/pki-ca-user-find.py \ + -U https://ca.example.com:8443 \ + --ca-bundle $SHARED/certs/ca_signing.crt \ + --client-cert $SHARED/certs/admin.crt \ + --client-key $SHARED/certs/admin.key \ + -v + + sleep 1 + + # check HTTP methods, paths, protocols, status, and authenticated users + docker exec ca find /var/log/pki/pki-tomcat \ + -name "localhost_access_log.*" \ + -exec cat {} \; \ + | tail -5 \ + | sed -e 's/^.* .* \(.*\) \[.*\] "\(.*\)" \(.*\) .*$/\2 \3 \1/' \ + | tee output + + # Python API should fall back to REST API v1 + cat > expected << EOF + GET /pki/v2/info HTTP/1.1 404 - + GET /pki/rest/info HTTP/1.1 200 - + GET /ca/rest/account/login HTTP/1.1 200 admin + GET /ca/rest/admin/users HTTP/1.1 200 admin + GET /ca/rest/account/logout HTTP/1.1 204 admin + EOF + + diff expected output + + - name: Check DS server systemd journal + if: always() + run: | + docker exec ds journalctl -x --no-pager -u dirsrv@localhost.service + + - name: Check DS container logs + if: always() + run: | + docker logs ds + + - name: Check PKI server access log + if: always() + run: | + docker exec ca find /var/log/pki/pki-tomcat -name "localhost_access_log.*" -exec cat {} \; + + - name: Check CA container logs + if: always() + run: | + docker logs ca diff --git a/.github/workflows/python-ca-test.yml b/.github/workflows/python-ca-test.yml new file mode 100644 index 00000000000..02cbdd59408 --- /dev/null +++ b/.github/workflows/python-ca-test.yml @@ -0,0 +1,281 @@ +name: CA Python API + +on: workflow_call + +env: + DS_IMAGE: ${{ vars.DS_IMAGE || 'quay.io/389ds/dirsrv' }} + +jobs: + # docs/installation/ca/Installing_CA.md + test: + name: Test + runs-on: ubuntu-latest + env: + SHARED: /tmp/workdir/pki + steps: + - name: Clone repository + uses: actions/checkout@v4 + + - name: Retrieve PKI images + uses: actions/cache@v4 + with: + key: pki-images-${{ github.sha }} + path: pki-images.tar + + - name: Load PKI images + run: docker load --input pki-images.tar + + - name: Create network + run: docker network create example + + #################################################################################################### + # Install CA that supports both REST API v1 and v2 + + - name: Set up DS container + run: | + tests/bin/ds-create.sh \ + --image=${{ env.DS_IMAGE }} \ + --hostname=ds.example.com \ + --network=example \ + --network-alias=ds.example.com \ + --password=Secret.123 \ + ds + + - name: Set up PKI container + run: | + tests/bin/runner-init.sh \ + --hostname=pki.example.com \ + --network=example \ + --network-alias=pki.example.com \ + pki + + - name: Install CA + run: | + docker exec pki pkispawn \ + -f /usr/share/pki/server/examples/installation/ca.cfg \ + -s CA \ + -D pki_ds_url=ldap://ds.example.com:3389 \ + -v + + - name: Update PKI server configuration + run: | + docker exec pki dnf install -y xmlstarlet + + # disable access log buffer + docker exec pki xmlstarlet edit --inplace \ + -u "//Valve[@className='org.apache.catalina.valves.AccessLogValve']/@buffered" \ + -v "false" \ + -i "//Valve[@className='org.apache.catalina.valves.AccessLogValve' and not(@buffered)]" \ + -t attr \ + -n "buffered" \ + -v "false" \ + /etc/pki/pki-tomcat/server.xml + + # restart PKI server + docker exec pki pki-server restart --wait + + - name: Set up client + run: | + # export CA signing cert + docker exec pki pki-server cert-export \ + --cert-file $SHARED/ca_signing.crt \ + ca_signing + + # export admin cert + docker exec pki openssl pkcs12 \ + -in /root/.dogtag/pki-tomcat/ca_admin_cert.p12 \ + -passin pass:Secret.123 \ + -out admin.crt \ + -clcerts \ + -nokeys + + # export admin key + docker exec pki openssl pkcs12 \ + -in /root/.dogtag/pki-tomcat/ca_admin_cert.p12 \ + -passin pass:Secret.123 \ + -out admin.key \ + -nodes \ + -nocerts + + #################################################################################################### + # Check Python API + + - name: Check PKI server info + run: | + docker exec pki python /usr/share/pki/tests/bin/pki-info.py \ + -U https://pki.example.com:8443 \ + --ca-bundle $SHARED/ca_signing.crt \ + -v + + sleep 1 + + # check HTTP methods, paths, protocols, status, and authenticated users + docker exec pki find /var/log/pki/pki-tomcat \ + -name "localhost_access_log.*" \ + -exec cat {} \; \ + | tail -1 \ + | sed -e 's/^.* .* \(.*\) \[.*\] "\(.*\)" \(.*\) .*$/\2 \3 \1/' \ + | tee output + + # Python API should use REST API v2 by default + cat > expected << EOF + GET /pki/v2/info HTTP/1.1 200 - + EOF + + diff expected output + + - name: Check PKI server info with REST API v1 + run: | + docker exec pki python /usr/share/pki/tests/bin/pki-info.py \ + -U https://pki.example.com:8443 \ + --ca-bundle $SHARED/ca_signing.crt \ + --api v1 \ + -v + + sleep 1 + + # check HTTP methods, paths, protocols, status, and authenticated users + docker exec pki find /var/log/pki/pki-tomcat \ + -name "localhost_access_log.*" \ + -exec cat {} \; \ + | tail -1 \ + | sed -e 's/^.* .* \(.*\) \[.*\] "\(.*\)" \(.*\) .*$/\2 \3 \1/' \ + | tee output + + # Python API should use REST API v1 as specified + cat > expected << EOF + GET /pki/v1/info HTTP/1.1 200 - + EOF + + - name: Check CA certs + run: | + docker exec pki python /usr/share/pki/tests/ca/bin/pki-ca-cert-find.py \ + -U https://pki.example.com:8443 \ + --ca-bundle $SHARED/ca_signing.crt \ + -v + + sleep 1 + + # check HTTP methods, paths, protocols, status, and authenticated users + docker exec pki find /var/log/pki/pki-tomcat \ + -name "localhost_access_log.*" \ + -exec cat {} \; \ + | tail -2 \ + | sed -e 's/^.* .* \(.*\) \[.*\] "\(.*\)" \(.*\) .*$/\2 \3 \1/' \ + | tee output + + # Python API should use REST API v2 by default + cat > expected << EOF + GET /pki/v2/info HTTP/1.1 200 - + POST /ca/v2/certs/search HTTP/1.1 200 - + EOF + + diff expected output + + - name: Check CA certs with REST API v1 + run: | + docker exec pki python /usr/share/pki/tests/ca/bin/pki-ca-cert-find.py \ + -U https://pki.example.com:8443 \ + --ca-bundle $SHARED/ca_signing.crt \ + --api v1 \ + -v + + sleep 1 + + # check HTTP methods, paths, protocols, status, and authenticated users + docker exec pki find /var/log/pki/pki-tomcat \ + -name "localhost_access_log.*" \ + -exec cat {} \; \ + | tail -1 \ + | sed -e 's/^.* .* \(.*\) \[.*\] "\(.*\)" \(.*\) .*$/\2 \3 \1/' \ + | tee output + + # Python API should use REST API v1 as specified + cat > expected << EOF + POST /ca/v1/certs/search HTTP/1.1 200 - + EOF + + diff expected output + + - name: Check CA users + run: | + docker exec pki python /usr/share/pki/tests/ca/bin/pki-ca-user-find.py \ + -U https://pki.example.com:8443 \ + --ca-bundle $SHARED/ca_signing.crt \ + --client-cert admin.crt \ + --client-key admin.key \ + -v + + sleep 1 + + # check HTTP methods, paths, protocols, status, and authenticated users + docker exec pki find /var/log/pki/pki-tomcat \ + -name "localhost_access_log.*" \ + -exec cat {} \; \ + | tail -4 \ + | sed -e 's/^.* .* \(.*\) \[.*\] "\(.*\)" \(.*\) .*$/\2 \3 \1/' \ + | tee output + + # Python API should use REST API v2 by default + cat > expected << EOF + GET /pki/v2/info HTTP/1.1 200 - + GET /ca/v2/account/login HTTP/1.1 200 caadmin + GET /ca/v2/admin/users HTTP/1.1 200 caadmin + GET /ca/v2/account/logout HTTP/1.1 204 caadmin + EOF + + diff expected output + + - name: Check CA users with REST API v1 + run: | + docker exec pki python /usr/share/pki/tests/ca/bin/pki-ca-user-find.py \ + -U https://pki.example.com:8443 \ + --ca-bundle $SHARED/ca_signing.crt \ + --client-cert admin.crt \ + --client-key admin.key \ + --api v1 \ + -v + + sleep 1 + + # check HTTP methods, paths, protocols, status, and authenticated users + docker exec pki find /var/log/pki/pki-tomcat \ + -name "localhost_access_log.*" \ + -exec cat {} \; \ + | tail -3 \ + | sed -e 's/^.* .* \(.*\) \[.*\] "\(.*\)" \(.*\) .*$/\2 \3 \1/' \ + | tee output + + # Python API should use REST API v1 as specified + cat > expected << EOF + GET /ca/v1/account/login HTTP/1.1 200 caadmin + GET /ca/v1/admin/users HTTP/1.1 200 caadmin + GET /ca/v1/account/logout HTTP/1.1 204 caadmin + EOF + + diff expected output + + - name: Check DS server systemd journal + if: always() + run: | + docker exec ds journalctl -x --no-pager -u dirsrv@localhost.service + + - name: Check DS container logs + if: always() + run: | + docker logs ds + + - name: Check PKI server systemd journal + if: always() + run: | + docker exec pki journalctl -x --no-pager -u pki-tomcatd@pki-tomcat.service + + - name: Check PKI server access log + if: always() + run: | + docker exec pki find /var/log/pki/pki-tomcat -name "localhost_access_log.*" -exec cat {} \; + + - name: Check CA debug log + if: always() + run: | + docker exec pki find /var/lib/pki/pki-tomcat/logs/ca -name "debug.*" -exec cat {} \; diff --git a/.github/workflows/python-kra-test.yml b/.github/workflows/python-kra-test.yml new file mode 100644 index 00000000000..0ffa609e438 --- /dev/null +++ b/.github/workflows/python-kra-test.yml @@ -0,0 +1,244 @@ +name: KRA Python API + +on: workflow_call + +env: + DS_IMAGE: ${{ vars.DS_IMAGE || 'quay.io/389ds/dirsrv' }} + +jobs: + # docs/installation/ca/Installing_CA.md + test: + name: Test + runs-on: ubuntu-latest + env: + SHARED: /tmp/workdir/pki + steps: + - name: Clone repository + uses: actions/checkout@v4 + + - name: Retrieve PKI images + uses: actions/cache@v4 + with: + key: pki-images-${{ github.sha }} + path: pki-images.tar + + - name: Load PKI images + run: docker load --input pki-images.tar + + - name: Create network + run: docker network create example + + #################################################################################################### + # Install KRA that supports both REST API v1 and v2 + + - name: Set up DS container + run: | + tests/bin/ds-create.sh \ + --image=${{ env.DS_IMAGE }} \ + --hostname=ds.example.com \ + --network=example \ + --network-alias=ds.example.com \ + --password=Secret.123 \ + ds + + - name: Set up PKI container + run: | + tests/bin/runner-init.sh \ + --hostname=pki.example.com \ + --network=example \ + --network-alias=pki.example.com \ + pki + + - name: Install CA + run: | + docker exec pki pkispawn \ + -f /usr/share/pki/server/examples/installation/ca.cfg \ + -s CA \ + -D pki_ds_url=ldap://ds.example.com:3389 \ + -v + + - name: Install KRA + run: | + docker exec pki pkispawn \ + -f /usr/share/pki/server/examples/installation/kra.cfg \ + -s KRA \ + -D pki_ds_url=ldap://ds.example.com:3389 \ + -v + + - name: Update PKI server configuration + run: | + docker exec pki dnf install -y xmlstarlet + + # disable access log buffer + docker exec pki xmlstarlet edit --inplace \ + -u "//Valve[@className='org.apache.catalina.valves.AccessLogValve']/@buffered" \ + -v "false" \ + -i "//Valve[@className='org.apache.catalina.valves.AccessLogValve' and not(@buffered)]" \ + -t attr \ + -n "buffered" \ + -v "false" \ + /etc/pki/pki-tomcat/server.xml + + # restart PKI server + docker exec pki pki-server restart --wait + + - name: Set up client + run: | + # export CA signing cert + docker exec pki pki-server cert-export \ + --cert-file $SHARED/ca_signing.crt \ + ca_signing + + # export admin cert + docker exec pki openssl pkcs12 \ + -in /root/.dogtag/pki-tomcat/ca_admin_cert.p12 \ + -passin pass:Secret.123 \ + -out admin.crt \ + -clcerts \ + -nokeys + + # export admin key + docker exec pki openssl pkcs12 \ + -in /root/.dogtag/pki-tomcat/ca_admin_cert.p12 \ + -passin pass:Secret.123 \ + -out admin.key \ + -nodes \ + -nocerts + + #################################################################################################### + # Check Python API + + - name: Check PKI server info + run: | + docker exec pki python /usr/share/pki/tests/bin/pki-info.py \ + -U https://pki.example.com:8443 \ + --ca-bundle $SHARED/ca_signing.crt \ + -v + + sleep 1 + + # check HTTP methods, paths, protocols, status, and authenticated users + docker exec pki find /var/log/pki/pki-tomcat \ + -name "localhost_access_log.*" \ + -exec cat {} \; \ + | tail -1 \ + | sed -e 's/^.* .* \(.*\) \[.*\] "\(.*\)" \(.*\) .*$/\2 \3 \1/' \ + | tee output + + # Python API should use REST API v2 by default + cat > expected << EOF + GET /pki/v2/info HTTP/1.1 200 - + EOF + + diff expected output + + - name: Check PKI server info with REST API v1 + run: | + docker exec pki python /usr/share/pki/tests/bin/pki-info.py \ + -U https://pki.example.com:8443 \ + --ca-bundle $SHARED/ca_signing.crt \ + --api v1 \ + -v + + sleep 1 + + # check HTTP methods, paths, protocols, status, and authenticated users + docker exec pki find /var/log/pki/pki-tomcat \ + -name "localhost_access_log.*" \ + -exec cat {} \; \ + | tail -1 \ + | sed -e 's/^.* .* \(.*\) \[.*\] "\(.*\)" \(.*\) .*$/\2 \3 \1/' \ + | tee output + + # Python API should use REST API v1 as specified + cat > expected << EOF + GET /pki/v1/info HTTP/1.1 200 - + EOF + + - name: Check KRA users + run: | + docker exec pki python /usr/share/pki/tests/kra/bin/pki-kra-user-find.py \ + -U https://pki.example.com:8443 \ + --ca-bundle $SHARED/ca_signing.crt \ + --client-cert admin.crt \ + --client-key admin.key \ + -v + + sleep 1 + + # check HTTP methods, paths, protocols, status, and authenticated users + docker exec pki find /var/log/pki/pki-tomcat \ + -name "localhost_access_log.*" \ + -exec cat {} \; \ + | tail -4 \ + | sed -e 's/^.* .* \(.*\) \[.*\] "\(.*\)" \(.*\) .*$/\2 \3 \1/' \ + | tee output + + # Python API should use REST API v2 by default + cat > expected << EOF + GET /pki/v2/info HTTP/1.1 200 - + GET /kra/v2/account/login HTTP/1.1 200 kraadmin + GET /kra/v2/admin/users HTTP/1.1 200 kraadmin + GET /kra/v2/account/logout HTTP/1.1 204 kraadmin + EOF + + diff expected output + + - name: Check KRA users with REST API v1 + run: | + docker exec pki python /usr/share/pki/tests/kra/bin/pki-kra-user-find.py \ + -U https://pki.example.com:8443 \ + --ca-bundle $SHARED/ca_signing.crt \ + --client-cert admin.crt \ + --client-key admin.key \ + --api v1 \ + -v + + sleep 1 + + # check HTTP methods, paths, protocols, status, and authenticated users + docker exec pki find /var/log/pki/pki-tomcat \ + -name "localhost_access_log.*" \ + -exec cat {} \; \ + | tail -3 \ + | sed -e 's/^.* .* \(.*\) \[.*\] "\(.*\)" \(.*\) .*$/\2 \3 \1/' \ + | tee output + + # Python API should use REST API v1 as specified + cat > expected << EOF + GET /kra/v1/account/login HTTP/1.1 200 kraadmin + GET /kra/v1/admin/users HTTP/1.1 200 kraadmin + GET /kra/v1/account/logout HTTP/1.1 204 kraadmin + EOF + + diff expected output + + - name: Check DS server systemd journal + if: always() + run: | + docker exec ds journalctl -x --no-pager -u dirsrv@localhost.service + + - name: Check DS container logs + if: always() + run: | + docker logs ds + + - name: Check PKI server systemd journal + if: always() + run: | + docker exec pki journalctl -x --no-pager -u pki-tomcatd@pki-tomcat.service + + - name: Check PKI server access log + if: always() + run: | + docker exec pki find /var/log/pki/pki-tomcat -name "localhost_access_log.*" -exec cat {} \; + + - name: Check CA debug log + if: always() + run: | + docker exec pki find /var/lib/pki/pki-tomcat/logs/ca -name "debug.*" -exec cat {} \; + + - name: Check KRA debug log + if: always() + run: | + docker exec pki find /var/lib/pki/pki-tomcat/logs/kra -name "debug.*" -exec cat {} \; diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index a01ea01593b..7ac797a495b 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -12,3 +12,18 @@ jobs: name: Python lint needs: build uses: ./.github/workflows/python-lint-test.yml + + python-ca-test: + name: CA Python API + needs: build + uses: ./.github/workflows/python-ca-test.yml + + python-ca-rest-api-v1-test: + name: CA Python API with REST API v1 + needs: build + uses: ./.github/workflows/python-ca-rest-api-v1-test.yml + + python-kra-test: + name: KRA Python API + needs: build + uses: ./.github/workflows/python-kra-test.yml diff --git a/base/common/python/pki/account.py b/base/common/python/pki/account.py index 86c8951bcad..3662a4caae9 100644 --- a/base/common/python/pki/account.py +++ b/base/common/python/pki/account.py @@ -18,9 +18,15 @@ # Copyright (C) 2013 Red Hat, Inc. # All rights reserved. # -from __future__ import absolute_import + +import inspect +import json +import logging + import pki +logger = logging.getLogger(__name__) + class AccountClient: """ @@ -35,27 +41,43 @@ class AccountClient: * call logout() to invalidate the session. """ - def __init__(self, connection, subsystem=None): + def __init__(self, parent, subsystem=None): """ Creates an AccountClient for the connection. - :param connection: connection to be associated with the AccountClient - :type connection: pki.PKIConnection + :param parent: PKIClient object + :type parent: pki.client.PKIClient :returns: AccountClient """ - self.connection = connection + if isinstance(parent, pki.client.PKIConnection): - self.login_url = '/rest/account/login' - self.logout_url = '/rest/account/logout' + logger.warning( + '%s:%s: The PKIConnection parameter in AccountClient.__init__() has been deprecated. ' + 'Provide PKIClient instead.', + inspect.stack()[1].filename, inspect.stack()[1].lineno) - if connection.subsystem is None: + self.subsystem_client = None + self.pki_client = None + self.connection = parent - if subsystem is None: + # in legacy code the subsystem name is specified in AccountClient + # in PKIConnection + if subsystem: + self.subsystem_name = subsystem + elif self.connection.subsystem: + self.subsystem_name = self.connection.subsystem + else: raise Exception('Missing subsystem for AccountClient') - self.login_url = '/' + subsystem + self.login_url - self.logout_url = '/' + subsystem + self.logout_url + else: + self.subsystem_client = parent + self.pki_client = self.subsystem_client.parent + self.connection = self.pki_client.connection + + # in newer code the subsystem name is specified in subsystem client + # (e.g. CAClient, KRAClient) + self.subsystem_name = self.subsystem_client.name @pki.handle_exceptions() def login(self): @@ -65,7 +87,26 @@ def login(self): :returns: None """ - self.connection.get(self.login_url) + + if self.pki_client: + api_path = self.pki_client.get_api_path() + else: + api_path = 'rest' + + path = '/%s/account/login' % api_path + + # in legacy code the PKIConnection object might already have the subsystem name + # in newer code the subsystem name needs to be included in the path + if not self.connection.subsystem: + path = '/' + self.subsystem_name + path + + response = self.connection.get(path) + + json_response = response.json() + logger.debug('Response:\n%s', json.dumps(json_response, indent=4)) + + # TODO: return Account object instead of JSON/XML + return json_response @pki.handle_exceptions() def logout(self): @@ -75,4 +116,17 @@ def logout(self): :returns: None """ - self.connection.get(self.logout_url) + + if self.pki_client: + api_path = self.pki_client.get_api_path() + else: + api_path = 'rest' + + path = '/%s/account/logout' % api_path + + # in legacy code the PKIConnection object might already have the subsystem name + # in newer code the subsystem name needs to be included in the path + if not self.connection.subsystem: + path = '/' + self.subsystem_name + path + + self.connection.get(path) diff --git a/base/common/python/pki/ca.py b/base/common/python/pki/ca.py new file mode 100644 index 00000000000..137f07ed03e --- /dev/null +++ b/base/common/python/pki/ca.py @@ -0,0 +1,17 @@ +# +# Copyright Red Hat, Inc. +# +# SPDX-License-Identifier: GPL-2.0-or-later +# + +import logging + +logger = logging.getLogger(__name__) + + +class CAClient: + + def __init__(self, parent): + + self.name = 'ca' + self.parent = parent diff --git a/base/common/python/pki/cert.py b/base/common/python/pki/cert.py index b0e0772e06a..33718e0c602 100644 --- a/base/common/python/pki/cert.py +++ b/base/common/python/pki/cert.py @@ -19,15 +19,15 @@ # Abhishek Koneru # Ade Lee -from __future__ import absolute_import -from __future__ import print_function import copy +import inspect import json import logging from six import iteritems import pki +import pki.ca import pki.client as client import pki.encoder as encoder import pki.profile as profile @@ -600,17 +600,31 @@ class CertClient(object): Java interface class defining the REST API for Certificate resources. """ - def __init__(self, connection): + def __init__(self, parent): """ Constructor """ - self.connection = connection + if isinstance(parent, pki.client.PKIConnection): + + logger.warning( + '%s:%s: The PKIConnection parameter in CertClient.__init__() has been deprecated. ' + 'Provide PKIClient instead.', + inspect.stack()[1].filename, inspect.stack()[1].lineno) + + self.ca_client = None + self.pki_client = None + self.connection = parent + + else: + self.ca_client = parent + self.pki_client = self.ca_client.parent + self.connection = self.pki_client.connection self.cert_url = '/rest/certs' self.agent_cert_url = '/rest/agent/certs' self.cert_requests_url = '/rest/certrequests' self.agent_cert_requests_url = '/rest/agent/certrequests' - if connection.subsystem is None: + if not self.connection.subsystem: self.cert_url = '/ca' + self.cert_url self.agent_cert_url = '/ca' + self.agent_cert_url self.cert_requests_url = '/ca' + self.cert_requests_url @@ -642,14 +656,28 @@ def list_certs(self, max_results=None, max_time=None, start=None, size=None, the certificates that satisfy the search criteria. If cert_search_request=None, returns all the certificates. """ - url = self.cert_url + '/search' + + if self.pki_client: + api_path = self.pki_client.get_api_path() + else: + api_path = 'rest' + + path = '/%s/certs/search' % api_path + + # in legacy code the PKIConnection object might already have the subsystem name + # in newer code the subsystem name needs to be included in the path + if not self.connection.subsystem: + path = '/ca' + path + + logger.info('Getting CA certs from %s', path) + query_params = {"maxResults": max_results, "maxTime": max_time, "start": start, "size": size} cert_search_request = CertSearchRequest(**cert_search_params) search_request = json.dumps(cert_search_request, cls=encoder.CustomTypeEncoder, sort_keys=True) - response = self.connection.post(url, search_request, self.headers, + response = self.connection.post(path, search_request, self.headers, query_params) json_response = response.json() diff --git a/base/common/python/pki/client.py b/base/common/python/pki/client.py index 64c72a01dd1..6e6ff31f59d 100644 --- a/base/common/python/pki/client.py +++ b/base/common/python/pki/client.py @@ -29,6 +29,7 @@ import os import ssl import warnings +import urllib import requests from requests import adapters @@ -38,6 +39,9 @@ except ImportError: from urllib3.exceptions import InsecureRequestWarning +import pki.info +import pki.util + logger = logging.getLogger(__name__) @@ -169,7 +173,10 @@ def __init__(self, protocol='http', hostname='localhost', port='8080', self.protocol = protocol self.hostname = hostname - self.port = port + self.port = str(port) + + # TODO: remove subsystem name from PKIConnection once all supported code + # has been changed to use PKIClient. self.subsystem = subsystem self.rootURI = self.protocol + '://' + self.hostname @@ -366,6 +373,103 @@ def delete(self, path, headers=None, use_root_uri=False): return r +class PKIClient: + + def __init__( + self, + url, + trust_env=None, + verify=True, + ca_bundle=None, + api_version=None): + + self.url = urllib.parse.urlparse(url) + + if self.url.port: + port = self.url.port + elif self.url.scheme == 'http': + port = 80 + elif self.url.scheme == 'https': + port = 443 + else: + raise Exception('Unsupported URL scheme: %s' % self.url.scheme) + + self.info = None + self.server_version = None + self.api_version = api_version + + if api_version == 'v1': + self.api_path = 'rest' + else: + self.api_path = api_version + + # TODO: do not hard-code message format + self.connection = PKIConnection( + protocol=self.url.scheme, + hostname=self.url.hostname, + port=port, + accept='application/json', + trust_env=trust_env, + verify=verify, + cert_paths=ca_bundle + ) + + self.info_client = pki.info.InfoClient(self) + + def set_client_auth(self, client_cert, client_key=None): + self.connection.set_authentication_cert(client_cert, client_key) + + def connect(self): + + logger.info('Connecting to %s', urllib.parse.urlunparse(self.url)) + + self.info = self.info_client.get_info() + + self.server_version = pki.util.Version(self.info.version) + logger.debug('- server version: %s', self.server_version) + + # if not specified, set REST API version and path based on server version + if not self.api_version: + if self.server_version >= pki.util.Version('11.6.0'): + # use REST API v2 for PKI 11.6.0 or later + self.api_version = 'v2' + self.api_path = 'v2' + else: + self.api_version = 'v1' + self.api_path = 'rest' + + logger.debug('- API version: %s', self.api_version) + logger.debug('- API path: %s', self.api_path) + + def get_info(self): + + if not self.info: + self.connect() + + return self.info + + def get_server_version(self): + + if not self.server_version: + self.connect() + + return self.server_version + + def get_api_version(self): + + if not self.api_version: + self.connect() + + return self.api_version + + def get_api_path(self): + + if not self.api_path: + self.connect() + + return self.api_path + + def main(): """ Test code for the PKIConnection class. diff --git a/base/common/python/pki/info.py b/base/common/python/pki/info.py index 03b7927ace8..fe69f9ec10d 100644 --- a/base/common/python/pki/info.py +++ b/base/common/python/pki/info.py @@ -21,11 +21,11 @@ """ Module containing the Python client classes for the InfoClient """ -from __future__ import absolute_import -from __future__ import print_function +import inspect import json import logging +import requests.exceptions from six import iteritems @@ -103,20 +103,56 @@ class InfoClient(object): server Info resources. """ - def __init__(self, connection): + def __init__(self, parent): """ Constructor """ - self.connection = connection + if isinstance(parent, pki.client.PKIConnection): - self.info_url = '/pki/v2/info' + logger.warning( + '%s:%s: The PKIConnection parameter in InfoClient.__init__() has been deprecated. ' + 'Provide PKIClient instead.', + inspect.stack()[1].filename, inspect.stack()[1].lineno) + + self.pki_client = None + self.connection = parent + + else: + self.pki_client = parent + self.connection = self.pki_client.connection @pki.handle_exceptions() def get_info(self): """ Return an Info object form a PKI server """ + if self.pki_client and self.pki_client.api_path: + # use REST API path specified in PKIClient + api_paths = [self.pki_client.api_path] + + else: + # try all possible REST API paths + api_paths = ['v2', 'rest'] + headers = {'Content-type': 'application/json', 'Accept': 'application/json'} - response = self.connection.get(self.info_url, headers) + + response = None + + for api_path in api_paths: + try: + path = '/pki/%s/info' % api_path + logger.info('Getting PKI server info from %s', path) + + response = self.connection.get(path, headers) + # REST API path available -> done + break + + except requests.exceptions.HTTPError as e: + if e.response.status_code != 404: + raise + # REST API path not available -> try another + + if not response: + raise Exception('Unable to get PKI server info') json_response = response.json() logger.debug('Response:\n%s', json.dumps(json_response, indent=4)) diff --git a/base/common/python/pki/kra.py b/base/common/python/pki/kra.py index 01d738342f3..701ffe83675 100644 --- a/base/common/python/pki/kra.py +++ b/base/common/python/pki/kra.py @@ -25,11 +25,15 @@ KeyRequestResource REST APIs. """ -from __future__ import absolute_import -from pki.info import InfoClient -import pki.key as key +import inspect +import logging -from pki.systemcert import SystemCertClient +import pki.client +import pki.info +import pki.key +import pki.systemcert + +logger = logging.getLogger(__name__) class KRAClient(object): @@ -38,7 +42,7 @@ class KRAClient(object): KeyRequest REST APIs. """ - def __init__(self, connection, crypto, transport_cert_nick=None): + def __init__(self, parent, crypto=None, transport_cert_nick=None): """ Constructor :param connection - PKIConnection object with DRM connection info. @@ -52,13 +56,36 @@ def __init__(self, connection, crypto, transport_cert_nick=None): Note that for NSS databases, the database must have been initialized beforehand. """ - self.connection = connection - self.crypto = crypto - self.info = InfoClient(connection) - self.keys = key.KeyClient( - connection, - crypto, - transport_cert_nick, - self.info - ) - self.system_certs = SystemCertClient(connection) + + self.name = 'kra' + self.parent = parent + + if isinstance(parent, pki.client.PKIConnection): + + logger.warning( + '%s:%s: The PKIConnection parameter in KRAClient.__init__() has been deprecated. ' + 'Provide PKIClient instead.', + inspect.stack()[1].filename, inspect.stack()[1].lineno) + + self.connection = parent + + self.crypto = crypto + self.info = pki.info.InfoClient(self.connection) + + self.keys = pki.key.KeyClient( + self.connection, + crypto, + transport_cert_nick, + self.info) + + self.system_certs = pki.systemcert.SystemCertClient(self.connection) + + else: + self.connection = parent.connection + + # do not automatically create these objects in KRAClient. + # client application should create them as needed. + self.crypto = None + self.info = None + self.keys = None + self.system_certs = None diff --git a/base/common/python/pki/user.py b/base/common/python/pki/user.py new file mode 100644 index 00000000000..19b791542c9 --- /dev/null +++ b/base/common/python/pki/user.py @@ -0,0 +1,37 @@ +# +# Copyright Red Hat, Inc. +# +# SPDX-License-Identifier: GPL-2.0-or-later +# + +import json +import logging + +logger = logging.getLogger(__name__) + + +class UserClient: + + def __init__(self, parent): + + self.subsystem_client = parent + self.pki_client = self.subsystem_client.parent + self.connection = self.pki_client.connection + + def find_users(self): + + api_path = self.pki_client.get_api_path() + + # the UserClient doesn't support legacy code so the subsystem name + # needs to be included in the path + path = '/%s/%s/admin/users' % (self.subsystem_client.name, api_path) + + logger.info('Getting %s users from %s', self.subsystem_client.name.upper(), path) + + response = self.connection.get(path) + + json_response = response.json() + logger.debug('Response:\n%s', json.dumps(json_response, indent=4)) + + # TODO: return UserCollection object instead of JSON/XML + return json_response diff --git a/base/server/healthcheck/pki/server/healthcheck/meta/connectivity.py b/base/server/healthcheck/pki/server/healthcheck/meta/connectivity.py index 00b561c09c4..d7c6d5e9fd7 100644 --- a/base/server/healthcheck/pki/server/healthcheck/meta/connectivity.py +++ b/base/server/healthcheck/pki/server/healthcheck/meta/connectivity.py @@ -1,11 +1,14 @@ import logging -from pki.server.healthcheck.meta.plugin import MetaPlugin, registry from ipahealthcheck.core.plugin import Result, duration from ipahealthcheck.core import constants -from pki.client import PKIConnection -from pki.cert import CertClient -from pki.systemcert import SystemCertClient + +import pki.ca +import pki.cert +import pki.client +import pki.systemcert + +from pki.server.healthcheck.meta.plugin import MetaPlugin, registry logger = logging.getLogger(__name__) @@ -43,12 +46,15 @@ def check(self): # Make a plain HTTPS GET to "find" one certificate, to test that # the server is up AND is able to respond back - connection = PKIConnection(protocol='https', - hostname='localhost', - port=https_port, - verify=False) + server_url = 'https://localhost:' + https_port + + pki_client = pki.client.PKIClient( + url=server_url, + verify=False) + + ca_client = pki.ca.CAClient(pki_client) - cert_client = CertClient(connection) + cert_client = pki.cert.CertClient(ca_client) cert = cert_client.list_certs(size=1) cert_info = cert.cert_data_info_list[0] if cert_info: @@ -63,13 +69,13 @@ def check(self): yield Result(self, constants.ERROR, msg="Unable to read serial number from retrieved cert", cert_info=cert_info, - serverURI=connection.serverURI, + serverURI=server_url, cert_url=cert_client.cert_url) else: logger.info("Request was made but none of the certs were retrieved") yield Result(self, constants.ERROR, msg="PKI server is up. But, unable to retrieve any certs", - serverURI=connection.serverURI, + serverURI=server_url, rest_path=cert_client.cert_url) else: @@ -118,12 +124,12 @@ def check(self): # Make a plain HTTPS GET to retrieve KRA transport cert, to test that # the server is up AND is able to respond back - connection = PKIConnection(protocol='https', + connection = pki.client.PKIConnection(protocol='https', hostname='localhost', port=https_port, verify=False) - system_cert_client = SystemCertClient(connection) + system_cert_client = pki.systemcert.SystemCertClient(connection) # This gets the KRA cert from CS.cfg via REST API. In future, the system # certs will be moved into LDAP. This means that even if LDAP is down diff --git a/tests/bin/pki-info.py b/tests/bin/pki-info.py new file mode 100755 index 00000000000..9cf308b830e --- /dev/null +++ b/tests/bin/pki-info.py @@ -0,0 +1,58 @@ +# +# Copyright Red Hat, Inc. +# +# SPDX-License-Identifier: GPL-2.0-or-later +# + +import argparse +import logging + +import pki.ca +import pki.cert +import pki.client + +logger = logging.getLogger(__name__) +logging.basicConfig(format='%(levelname)s: %(message)s') + +parser = argparse.ArgumentParser() +parser.add_argument( + '-U', + help='Server URL', + dest='url') +parser.add_argument( + '--ca-bundle', + help='Path to CA bundle', + dest='ca_bundle') +parser.add_argument( + '--api', + help='API version: v1, v2', + dest='api_version') +parser.add_argument( + '-v', + '--verbose', + help='Run in verbose mode.', + dest='verbose', + action='store_true') +parser.add_argument( + '--debug', + help='Run in debug mode.', + dest='debug', + action='store_true') + +args = parser.parse_args() + +if args.debug: + logging.getLogger().setLevel(logging.DEBUG) + +elif args.verbose: + logging.getLogger().setLevel(logging.INFO) + +pki_client = pki.client.PKIClient( + url=args.url, + ca_bundle=args.ca_bundle, + api_version=args.api_version) + +info = pki_client.get_info() + +print(' Server Version: %s' % pki_client.get_server_version()) +print(' API Version: %s' % pki_client.get_api_version()) diff --git a/tests/ca/bin/pki-ca-cert-find.py b/tests/ca/bin/pki-ca-cert-find.py new file mode 100755 index 00000000000..33df0cc58bb --- /dev/null +++ b/tests/ca/bin/pki-ca-cert-find.py @@ -0,0 +1,71 @@ +# +# Copyright Red Hat, Inc. +# +# SPDX-License-Identifier: GPL-2.0-or-later +# + +import argparse +import logging + +import pki.ca +import pki.cert +import pki.client + +logger = logging.getLogger(__name__) +logging.basicConfig(format='%(levelname)s: %(message)s') + +parser = argparse.ArgumentParser() +parser.add_argument( + '-U', + help='Server URL', + dest='url') +parser.add_argument( + '--ca-bundle', + help='Path to CA bundle', + dest='ca_bundle') +parser.add_argument( + '--api', + help='API version: v1, v2', + dest='api_version') +parser.add_argument( + '-v', + '--verbose', + help='Run in verbose mode.', + dest='verbose', + action='store_true') +parser.add_argument( + '--debug', + help='Run in debug mode.', + dest='debug', + action='store_true') + +args = parser.parse_args() + +if args.debug: + logging.getLogger().setLevel(logging.DEBUG) + +elif args.verbose: + logging.getLogger().setLevel(logging.INFO) + +pki_client = pki.client.PKIClient( + url=args.url, + ca_bundle=args.ca_bundle, + api_version=args.api_version) + +ca_client = pki.ca.CAClient(pki_client) +cert_client = pki.cert.CertClient(ca_client) + +certs = cert_client.list_certs() + +first = True + +for cert in certs: + + if first: + first = False + else: + print() + + print(' Serial Number: ' + cert.serial_number) + print(' Subject DN: ' + cert.subject_dn) + print(' Status: ' + cert.status) diff --git a/tests/ca/bin/pki-ca-user-find.py b/tests/ca/bin/pki-ca-user-find.py new file mode 100755 index 00000000000..122808a4a00 --- /dev/null +++ b/tests/ca/bin/pki-ca-user-find.py @@ -0,0 +1,88 @@ +# +# Copyright Red Hat, Inc. +# +# SPDX-License-Identifier: GPL-2.0-or-later +# + +import argparse +import logging + +import pki.ca +import pki.account +import pki.client +import pki.user + +logger = logging.getLogger(__name__) +logging.basicConfig(format='%(levelname)s: %(message)s') + +parser = argparse.ArgumentParser() +parser.add_argument( + '-U', + help='Server URL', + dest='url') +parser.add_argument( + '--ca-bundle', + help='Path to CA bundle', + dest='ca_bundle') +parser.add_argument( + '--client-cert', + help='Path to client certificate', + dest='client_cert') +parser.add_argument( + '--client-key', + help='Path to client key', + dest='client_key') +parser.add_argument( + '--api', + help='API version: v1, v2', + dest='api_version') +parser.add_argument( + '-v', + '--verbose', + help='Run in verbose mode.', + dest='verbose', + action='store_true') +parser.add_argument( + '--debug', + help='Run in debug mode.', + dest='debug', + action='store_true') + +args = parser.parse_args() + +if args.debug: + logging.getLogger().setLevel(logging.DEBUG) + +elif args.verbose: + logging.getLogger().setLevel(logging.INFO) + +pki_client = pki.client.PKIClient( + url=args.url, + ca_bundle=args.ca_bundle, + api_version=args.api_version) + +pki_client.set_client_auth( + client_cert=args.client_cert, + client_key=args.client_key) + +ca_client = pki.ca.CAClient(pki_client) + +account_client = pki.account.AccountClient(ca_client) +account_client.login() + +user_client = pki.user.UserClient(ca_client) +users = user_client.find_users() + +first = True + +for user in users['entries']: + + if first: + first = False + else: + print() + + print(' User ID: %s' % user['UserID']) + print(' Full name: %s' % user['FullName']) + +account_client.logout() diff --git a/tests/kra/bin/pki-kra-user-find.py b/tests/kra/bin/pki-kra-user-find.py new file mode 100755 index 00000000000..b918c04181c --- /dev/null +++ b/tests/kra/bin/pki-kra-user-find.py @@ -0,0 +1,88 @@ +# +# Copyright Red Hat, Inc. +# +# SPDX-License-Identifier: GPL-2.0-or-later +# + +import argparse +import logging + +import pki.kra +import pki.account +import pki.client +import pki.user + +logger = logging.getLogger(__name__) +logging.basicConfig(format='%(levelname)s: %(message)s') + +parser = argparse.ArgumentParser() +parser.add_argument( + '-U', + help='Server URL', + dest='url') +parser.add_argument( + '--ca-bundle', + help='Path to CA bundle', + dest='ca_bundle') +parser.add_argument( + '--client-cert', + help='Path to client certificate', + dest='client_cert') +parser.add_argument( + '--client-key', + help='Path to client key', + dest='client_key') +parser.add_argument( + '--api', + help='API version: v1, v2', + dest='api_version') +parser.add_argument( + '-v', + '--verbose', + help='Run in verbose mode.', + dest='verbose', + action='store_true') +parser.add_argument( + '--debug', + help='Run in debug mode.', + dest='debug', + action='store_true') + +args = parser.parse_args() + +if args.debug: + logging.getLogger().setLevel(logging.DEBUG) + +elif args.verbose: + logging.getLogger().setLevel(logging.INFO) + +pki_client = pki.client.PKIClient( + url=args.url, + ca_bundle=args.ca_bundle, + api_version=args.api_version) + +pki_client.set_client_auth( + client_cert=args.client_cert, + client_key=args.client_key) + +kra_client = pki.kra.KRAClient(pki_client) + +account_client = pki.account.AccountClient(kra_client) +account_client.login() + +user_client = pki.user.UserClient(kra_client) +users = user_client.find_users() + +first = True + +for user in users['entries']: + + if first: + first = False + else: + print() + + print(' User ID: %s' % user['UserID']) + print(' Full name: %s' % user['FullName']) + +account_client.logout()