summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFraser Tweedale <ftweedal@redhat.com>2015-04-30 04:55:29 -0400
committerJan Cholasta <jcholast@redhat.com>2015-06-04 08:27:33 +0000
commit300b74fc7fb2a5ce540b2d21189794a5b2db88b1 (patch)
treecfa27d8e5ce6eec66e0c47e38ab6813fef6f6d43
parent35af0d6d66e623012755acca44bd77186067d156 (diff)
downloadfreeipa-300b74fc7fb2a5ce540b2d21189794a5b2db88b1.tar.gz
freeipa-300b74fc7fb2a5ce540b2d21189794a5b2db88b1.tar.xz
freeipa-300b74fc7fb2a5ce540b2d21189794a5b2db88b1.zip
Add certprofile plugin
Add the 'certprofile' plugin which defines the commands for managing certificate profiles and associated permissions. Also update Dogtag network code in 'ipapython.dogtag' to support headers and arbitrary request bodies, to facilitate use of the Dogtag profiles REST API. Part of: https://fedorahosted.org/freeipa/ticket/57 Reviewed-By: Martin Basti <mbasti@redhat.com>
-rw-r--r--ACI.txt8
-rw-r--r--API.txt62
-rw-r--r--install/updates/40-certprofile.update9
-rw-r--r--install/updates/40-delegation.update8
-rw-r--r--install/updates/Makefile.am1
-rw-r--r--ipalib/constants.py1
-rw-r--r--ipalib/plugins/certprofile.py253
-rw-r--r--ipapython/dogtag.py29
-rw-r--r--ipaserver/plugins/dogtag.py176
9 files changed, 534 insertions, 13 deletions
diff --git a/ACI.txt b/ACI.txt
index 1821696fd..543d8da69 100644
--- a/ACI.txt
+++ b/ACI.txt
@@ -22,6 +22,14 @@ dn: cn=automount,dc=ipa,dc=example
aci: (targetattr = "automountmapname || description")(targetfilter = "(objectclass=automountmap)")(version 3.0;acl "permission:System: Modify Automount Maps";allow (write) groupdn = "ldap:///cn=System: Modify Automount Maps,cn=permissions,cn=pbac,dc=ipa,dc=example";)
dn: cn=automount,dc=ipa,dc=example
aci: (targetfilter = "(objectclass=automountmap)")(version 3.0;acl "permission:System: Remove Automount Maps";allow (delete) groupdn = "ldap:///cn=System: Remove Automount Maps,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=certprofiles,cn=ca,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=ipacertprofile)")(version 3.0;acl "permission:System: Delete Certificate Profile";allow (delete) groupdn = "ldap:///cn=System: Delete Certificate Profile,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=certprofiles,cn=ca,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=ipacertprofile)")(version 3.0;acl "permission:System: Import Certificate Profile";allow (add) groupdn = "ldap:///cn=System: Import Certificate Profile,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=certprofiles,cn=ca,dc=ipa,dc=example
+aci: (targetattr = "cn || description || ipacertprofilestoreissued")(targetfilter = "(objectclass=ipacertprofile)")(version 3.0;acl "permission:System: Modify Certificate Profile";allow (write) groupdn = "ldap:///cn=System: Modify Certificate Profile,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=certprofiles,cn=ca,dc=ipa,dc=example
+aci: (targetattr = "cn || createtimestamp || description || entryusn || ipacertprofilestoreissued || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipacertprofile)")(version 3.0;acl "permission:System: Read Certificate Profiles";allow (compare,read,search) userdn = "ldap:///all";)
dn: cn=ipaconfig,cn=etc,dc=ipa,dc=example
aci: (targetattr = "cn || createtimestamp || entryusn || ipacertificatesubjectbase || ipaconfigstring || ipacustomfields || ipadefaultemaildomain || ipadefaultloginshell || ipadefaultprimarygroup || ipagroupobjectclasses || ipagroupsearchfields || ipahomesrootdir || ipakrbauthzdata || ipamaxusernamelength || ipamigrationenabled || ipapwdexpadvnotify || ipasearchrecordslimit || ipasearchtimelimit || ipaselinuxusermapdefault || ipaselinuxusermaporder || ipauserauthtype || ipauserobjectclasses || ipausersearchfields || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipaguiconfig)")(version 3.0;acl "permission:System: Read Global Configuration";allow (compare,read,search) userdn = "ldap:///all";)
dn: cn=costemplates,cn=accounts,dc=ipa,dc=example
diff --git a/API.txt b/API.txt
index 6520f2c42..81aca14af 100644
--- a/API.txt
+++ b/API.txt
@@ -509,6 +509,68 @@ args: 1,1,1
arg: Str('request_id')
option: Str('version?', exclude='webui')
output: Output('result', None, None)
+command: certprofile_del
+args: 1,2,3
+arg: Str('cn', attribute=True, cli_name='id', multivalue=True, primary_key=True, query=True, required=True)
+option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Str('version?', exclude='webui')
+output: Output('result', <type 'dict'>, None)
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: ListOfPrimaryKeys('value', None, None)
+command: certprofile_find
+args: 1,9,4
+arg: Str('criteria?', noextrawhitespace=False)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('cn', attribute=True, autofill=False, cli_name='id', multivalue=False, primary_key=True, query=True, required=False)
+option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, query=True, required=False)
+option: Bool('ipacertprofilestoreissued', attribute=True, autofill=False, cli_name='store', default=True, multivalue=False, query=True, required=False)
+option: Flag('pkey_only?', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Int('sizelimit?', autofill=False, minvalue=0)
+option: Int('timelimit?', autofill=False, minvalue=0)
+option: Str('version?', exclude='webui')
+output: Output('count', <type 'int'>, None)
+output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list of LDAP entries', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: Output('truncated', <type 'bool'>, None)
+command: certprofile_import
+args: 1,6,3
+arg: Str('cn', attribute=True, cli_name='id', multivalue=False, primary_key=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('description', attribute=True, cli_name='desc', multivalue=False, required=True)
+option: File('file', cli_name='file')
+option: Bool('ipacertprofilestoreissued', attribute=True, cli_name='store', default=True, multivalue=False, required=True)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: certprofile_mod
+args: 1,10,3
+arg: Str('cn', attribute=True, cli_name='id', multivalue=False, primary_key=True, query=True, required=True)
+option: Str('addattr*', cli_name='addattr', exclude='webui')
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('delattr*', cli_name='delattr', exclude='webui')
+option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, required=False)
+option: Bool('ipacertprofilestoreissued', attribute=True, autofill=False, cli_name='store', default=True, multivalue=False, required=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('rename', cli_name='rename', multivalue=False, primary_key=True, required=False)
+option: Flag('rights', autofill=True, default=False)
+option: Str('setattr*', cli_name='setattr', exclude='webui')
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: certprofile_show
+args: 1,4,3
+arg: Str('cn', attribute=True, cli_name='id', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
command: compat_is_enabled
args: 0,1,1
option: Str('version?', exclude='webui')
diff --git a/install/updates/40-certprofile.update b/install/updates/40-certprofile.update
new file mode 100644
index 000000000..6b0a81d0f
--- /dev/null
+++ b/install/updates/40-certprofile.update
@@ -0,0 +1,9 @@
+dn: cn=ca,$SUFFIX
+default: objectClass: nsContainer
+default: objectClass: top
+default: cn: ca
+
+dn: cn=certprofiles,cn=ca,$SUFFIX
+default: objectClass: nsContainer
+default: objectClass: top
+default: cn: certprofiles
diff --git a/install/updates/40-delegation.update b/install/updates/40-delegation.update
index 975929bd7..bc0736c5b 100644
--- a/install/updates/40-delegation.update
+++ b/install/updates/40-delegation.update
@@ -237,3 +237,11 @@ default:ipapermissiontype: SYSTEM
dn: cn=config
add:aci: (version 3.0;acl "permission:Add Configuration Sub-Entries";allow (add) groupdn = "ldap:///cn=Add Configuration Sub-Entries,cn=permissions,cn=pbac,$SUFFIX";)
+
+# CA Administrators
+dn: cn=CA Administrator,cn=privileges,cn=pbac,$SUFFIX
+default:objectClass: nestedgroup
+default:objectClass: groupofnames
+default:objectClass: top
+default:cn: CA Administrator
+default:description: CA Administrator
diff --git a/install/updates/Makefile.am b/install/updates/Makefile.am
index 4e2da05d6..fc6bd624e 100644
--- a/install/updates/Makefile.am
+++ b/install/updates/Makefile.am
@@ -32,6 +32,7 @@ app_DATA = \
40-replication.update \
40-dns.update \
40-automember.update \
+ 40-certprofile.update \
40-otp.update \
40-vault.update \
45-roles.update \
diff --git a/ipalib/constants.py b/ipalib/constants.py
index 95dec54a5..96396a236 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -118,6 +118,7 @@ DEFAULT_CONFIG = (
('container_radiusproxy', DN(('cn', 'radiusproxy'))),
('container_views', DN(('cn', 'views'), ('cn', 'accounts'))),
('container_masters', DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'))),
+ ('container_certprofile', DN(('cn', 'certprofiles'), ('cn', 'ca'))),
# Ports, hosts, and URIs:
('xmlrpc_uri', 'http://localhost:8888/ipa/xml'),
diff --git a/ipalib/plugins/certprofile.py b/ipalib/plugins/certprofile.py
new file mode 100644
index 000000000..1a2d14388
--- /dev/null
+++ b/ipalib/plugins/certprofile.py
@@ -0,0 +1,253 @@
+#
+# Copyright (C) 2015 FreeIPA Contributors see COPYING for license
+#
+
+import re
+
+from ipalib import api, Bool, File, Str
+from ipalib import output
+from ipalib.plugable import Registry
+from ipalib.plugins.virtual import VirtualCommand
+from ipalib.plugins.baseldap import (
+ LDAPObject, LDAPSearch, LDAPCreate,
+ LDAPDelete, LDAPUpdate, LDAPRetrieve)
+from ipalib import ngettext
+from ipalib.text import _
+
+from ipalib import errors
+
+
+__doc__ = _("""
+Manage Certificate Profiles
+
+Certificate Profiles are used by Certificate Authority (CA) in the signing of
+certificates to determine if a Certificate Signing Request (CSR) is acceptable,
+and if so what features and extensions will be present on the certificate.
+
+The Certificate Profile format is the property-list format understood by the
+Dogtag or Red Hat Certificate System CA.
+
+PROFILE ID SYNTAX:
+
+A Profile ID is a string without spaces or punctuation starting with a letter
+and followed by a sequence of letters, digits or underscore ("_").
+
+EXAMPLES:
+
+ Import a profile that will not store issued certificates:
+ ipa certprofile-import ShortLivedUserCert \\
+ --file UserCert.profile --summary "User Certificates" \\
+ --store=false
+
+ Delete a certificate profile:
+ ipa certprofile-del ShortLivedUserCert
+
+ Show information about a profile:
+ ipa certprofile-show ShortLivedUserCert
+
+ Search for profiles that do not store certificates:
+ ipa certprofile-find --store=false
+
+""")
+
+
+register = Registry()
+
+
+def ca_enabled_check():
+ """Raise NotFound if CA is not enabled.
+
+ This function is defined in multiple plugins to avoid circular imports
+ (cert depends on certprofile, so we cannot import cert here).
+
+ """
+ if not api.Command.ca_is_enabled()['result']:
+ raise errors.NotFound(reason=_('CA is not configured'))
+
+
+profile_id_pattern = re.compile('^[a-zA-Z]\w*$')
+
+
+def validate_profile_id(ugettext, value):
+ """Ensure profile ID matches form required by CA."""
+ if profile_id_pattern.match(value) is None:
+ return _('invalid Profile ID')
+ else:
+ return None
+
+
+@register()
+class certprofile(LDAPObject):
+ """
+ Certificate Profile object.
+ """
+ container_dn = api.env.container_certprofile
+ object_name = _('Certificate Profile')
+ object_name_plural = _('Certificate Profiles')
+ object_class = ['ipacertprofile']
+ default_attributes = [
+ 'cn', 'description', 'ipacertprofilestoreissued'
+ ]
+ search_attributes = [
+ 'cn', 'description', 'ipacertprofilestoreissued'
+ ]
+ rdn_is_primary_key = True
+ label = _('Certificate Profiles')
+ label_singular = _('Certificate Profile')
+
+ takes_params = (
+ Str('cn', validate_profile_id,
+ primary_key=True,
+ cli_name='id',
+ label=_('Profile ID'),
+ doc=_('Profile ID for referring to this profile'),
+ ),
+ Str('description',
+ required=True,
+ cli_name='desc',
+ label=_('Profile description'),
+ doc=_('Brief description of this profile'),
+ ),
+ Bool('ipacertprofilestoreissued',
+ default=True,
+ cli_name='store',
+ label=_('Store issued certificates'),
+ doc=_('Whether to store certs issued using this profile'),
+ ),
+ )
+
+ permission_filter_objectclasses = ['ipacertprofile']
+ managed_permissions = {
+ 'System: Read Certificate Profiles': {
+ 'replaces_global_anonymous_aci': True,
+ 'ipapermbindruletype': 'all',
+ 'ipapermright': {'read', 'search', 'compare'},
+ 'ipapermdefaultattr': {
+ 'cn',
+ 'description',
+ 'ipacertprofilestoreissued',
+ 'objectclass',
+ },
+ },
+ 'System: Import Certificate Profile': {
+ 'ipapermright': {'add'},
+ 'replaces': [
+ '(target = "ldap:///cn=*,cn=certprofiles,cn=ca,$SUFFIX")(version 3.0;acl "permission:Import Certificate Profile";allow (add) groupdn = "ldap:///cn=Import Certificate Profile,cn=permissions,cn=pbac,$SUFFIX";)',
+ ],
+ 'default_privileges': {'CA Administrator'},
+ },
+ 'System: Delete Certificate Profile': {
+ 'ipapermright': {'delete'},
+ 'replaces': [
+ '(target = "ldap:///cn=*,cn=certprofiles,cn=ca,$SUFFIX")(version 3.0;acl "permission:Delete Certificate Profile";allow (delete) groupdn = "ldap:///cn=Delete Certificate Profile,cn=permissions,cn=pbac,$SUFFIX";)',
+ ],
+ 'default_privileges': {'CA Administrator'},
+ },
+ 'System: Modify Certificate Profile': {
+ 'ipapermright': {'write'},
+ 'ipapermdefaultattr': {
+ 'cn',
+ 'description',
+ 'ipacertprofilestoreissued',
+ },
+ 'replaces': [
+ '(targetattr = "cn || description || ipacertprofilestoreissued")(target = "ldap:///cn=*,cn=certprofiles,cn=ca,$SUFFIX")(version 3.0;acl "permission:Modify Certificate Profile";allow (write) groupdn = "ldap:///cn=Modify Certificate Profile,cn=permissions,cn=pbac,$SUFFIX";)',
+ ],
+ 'default_privileges': {'CA Administrator'},
+ },
+ }
+
+
+
+@register()
+class certprofile_find(LDAPSearch):
+ __doc__ = _("Search for Certificate Profiles.")
+ msg_summary = ngettext(
+ '%(count)d profile matched', '%(count)d profiles matched', 0
+ )
+
+ def execute(self, *args, **kwargs):
+ ca_enabled_check()
+ return super(certprofile_find, self).execute(*args, **kwargs)
+
+
+@register()
+class certprofile_show(LDAPRetrieve):
+ __doc__ = _("Display the properties of a Certificate Profile.")
+
+ def execute(self, *args, **kwargs):
+ ca_enabled_check()
+ return super(certprofile_show, self).execute(*args, **kwargs)
+
+
+@register()
+class certprofile_import(LDAPCreate):
+ __doc__ = _("Import a Certificate Profile.")
+ msg_summary = _('Imported profile "%(value)s"')
+ takes_options = (
+ File('file',
+ label=_('Filename'),
+ cli_name='file',
+ flags=('virtual_attribute',),
+ ),
+ )
+
+ PROFILE_ID_PATTERN = re.compile('^profileId=([a-zA-Z]\w*)', re.MULTILINE)
+
+ def pre_callback(self, ldap, dn, entry, entry_attrs, *keys, **options):
+ ca_enabled_check()
+
+ match = self.PROFILE_ID_PATTERN.search(options['file'])
+ if match is None:
+ raise errors.ValidationError(name='file',
+ error=_("Profile ID is not present in profile data"))
+ elif keys[0] != match.group(1):
+ raise errors.ValidationError(name='file',
+ error=_("Profile ID '%(cli_value)s' does not match profile data '%(file_value)s'")
+ % {'cli_value': keys[0], 'file_value': match.group(1)}
+ )
+ return dn
+
+
+ def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+ """Import the profile into Dogtag and enable it.
+
+ If the operation succeeds, update the LDAP entry to 'enabled'.
+ If the operation fails, remove the LDAP entry.
+ """
+ try:
+ with self.api.Backend.ra_certprofile as profile_api:
+ profile_api.create_profile(options['file'])
+ profile_api.enable_profile(keys[0])
+ except:
+ # something went wrong ; delete entry
+ ldap.delete_entry(dn)
+ raise
+
+ return dn
+
+
+@register()
+class certprofile_del(LDAPDelete):
+ __doc__ = _("Delete a Certificate Profile.")
+ msg_summary = _('Deleted profile "%(value)s"')
+
+ def execute(self, *args, **kwargs):
+ ca_enabled_check()
+ return super(certprofile_del, self).execute(*args, **kwargs)
+
+ def post_callback(self, ldap, dn, *keys, **options):
+ with self.api.Backend.ra_certprofile as profile_api:
+ profile_api.disable_profile(keys[0])
+ profile_api.delete_profile(keys[0])
+ return dn
+
+
+@register()
+class certprofile_mod(LDAPUpdate):
+ __doc__ = _("Modify Certificate Profile configuration.")
+ msg_summary = _('Modified Certificate Profile "%(value)s')
+
+ def execute(self, *args, **kwargs):
+ ca_enabled_check()
+ return super(certprofile_mod, self).execute(*args, **kwargs)
diff --git a/ipapython/dogtag.py b/ipapython/dogtag.py
index c74b8736a..11311cf7b 100644
--- a/ipapython/dogtag.py
+++ b/ipapython/dogtag.py
@@ -233,9 +233,12 @@ def ca_status(ca_host=None, use_proxy=True):
return _parse_ca_status(body)
-def https_request(host, port, url, secdir, password, nickname, **kw):
+def https_request(host, port, url, secdir, password, nickname,
+ method='POST', headers=None, body=None, **kw):
"""
+ :param method: HTTP request method (defalut: 'POST')
:param url: The path (not complete URL!) to post to.
+ :param body: The request body (encodes kw if None)
:param kw: Keyword arguments to encode into POST body.
:return: (http_status, http_reason_phrase, http_headers, http_body)
as (integer, unicode, dict, str)
@@ -254,9 +257,11 @@ def https_request(host, port, url, secdir, password, nickname, **kw):
nickname, password, nss.get_default_certdb())
return conn
- body = urlencode(kw)
+ if body is None:
+ body = urlencode(kw)
return _httplib_request(
- 'https', host, port, url, connection_factory, body)
+ 'https', host, port, url, connection_factory, body,
+ method=method, headers=headers)
def http_request(host, port, url, **kw):
@@ -288,11 +293,13 @@ def unauthenticated_https_request(host, port, url, **kw):
def _httplib_request(
- protocol, host, port, path, connection_factory, request_body):
+ protocol, host, port, path, connection_factory, request_body,
+ method='POST', headers=None):
"""
:param request_body: Request body
:param connection_factory: Connection class to use. Will be called
with the host and port arguments.
+ :param method: HTTP request method (default: 'POST')
Perform a HTTP(s) request.
"""
@@ -301,13 +308,17 @@ def _httplib_request(
uri = '%s://%s%s' % (protocol, ipautil.format_netloc(host, port), path)
root_logger.debug('request %r', uri)
root_logger.debug('request body %r', request_body)
+
+ headers = headers or {}
+ if (
+ method == 'POST'
+ and 'content-type' not in (str(k).lower() for k in headers.viewkeys())
+ ):
+ headers['content-type'] = 'application/x-www-form-urlencoded'
+
try:
conn = connection_factory(host, port)
- conn.request(
- 'POST', uri,
- body=request_body,
- headers={'Content-type': 'application/x-www-form-urlencoded'},
- )
+ conn.request(method, uri, body=request_body, headers=headers)
res = conn.getresponse()
http_status = res.status
diff --git a/ipaserver/plugins/dogtag.py b/ipaserver/plugins/dogtag.py
index 52bdb0d42..9654123b1 100644
--- a/ipaserver/plugins/dogtag.py
+++ b/ipaserver/plugins/dogtag.py
@@ -4,8 +4,9 @@
# Jason Gerard DeRose <jderose@redhat.com>
# Rob Crittenden <rcritten@@redhat.com>
# John Dennis <jdennis@redhat.com>
+# Fraser Tweedale <ftweedal@redhat.com>
#
-# Copyright (C) 2014 Red Hat
+# Copyright (C) 2014, 2015 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
@@ -238,17 +239,21 @@ digits and nothing else follows.
'''
import datetime
+import json
from lxml import etree
+import os
import tempfile
import time
import urllib2
+import pki
from pki.client import PKIConnection
import pki.crypto as cryptoutil
from pki.kra import KRAClient
from ipalib import Backend
from ipapython.dn import DN
+import ipapython.cookie
import ipapython.dogtag
from ipapython import ipautil
from ipaserver.install.certs import CertDB
@@ -1262,13 +1267,12 @@ def select_any_master(ldap2, service='CA'):
#-------------------------------------------------------------------------------
-from ipalib import api, SkipPluginModule
+from ipalib import api, errors, SkipPluginModule
if api.env.ra_plugin != 'dogtag':
# In this case, abort loading this plugin module...
raise SkipPluginModule(reason='dogtag not selected as RA plugin')
import os, random
from ipaserver.plugins import rabase
-from ipalib.errors import CertificateOperationError
from ipalib.constants import TYPE_ERROR
from ipalib.util import cachedproperty
from ipapython import dogtag
@@ -1318,7 +1322,7 @@ class ra(rabase.rabase):
err_msg = u'%s (%s)' % (err_msg, detail)
self.error('%s.%s(): %s', self.fullname, func_name, err_msg)
- raise CertificateOperationError(error=err_msg)
+ raise errors.CertificateOperationError(error=err_msg)
@cachedproperty
def ca_host(self):
@@ -1923,3 +1927,167 @@ class kra(Backend):
return KRAClient(connection, crypto)
api.register(kra)
+
+
+class RestClient(Backend):
+ """Simple Dogtag REST client to be subclassed by other backends.
+
+ This class is a context manager. Authenticated calls must be
+ executed in a ``with`` suite::
+
+ class ra_certprofile(RestClient):
+ path = 'profile'
+ ...
+
+ api.register(ra_certprofile)
+
+ with api.Backend.ra_certprofile as profile_api:
+ # REST client is now logged in
+ profile_api.create_profile(...)
+
+ """
+ path = None
+
+ @staticmethod
+ def _parse_dogtag_error(body):
+ try:
+ return pki.PKIException.from_json(json.loads(body))
+ except:
+ return None
+
+ def __init__(self):
+ if api.env.in_tree:
+ self.sec_dir = api.env.dot_ipa + os.sep + 'alias'
+ self.pwd_file = self.sec_dir + os.sep + '.pwd'
+ else:
+ self.sec_dir = paths.HTTPD_ALIAS_DIR
+ self.pwd_file = paths.ALIAS_PWDFILE_TXT
+ self.noise_file = self.sec_dir + os.sep + '.noise'
+ self.ipa_key_size = "2048"
+ self.ipa_certificate_nickname = "ipaCert"
+ self.ca_certificate_nickname = "caCert"
+ try:
+ f = open(self.pwd_file, "r")
+ self.password = f.readline().strip()
+ f.close()
+ except IOError:
+ self.password = ''
+ super(RestClient, self).__init__()
+
+ # session cookie
+ self.cookie = None
+
+ @cachedproperty
+ def ca_host(self):
+ """
+ :return: host
+ as str
+
+ Select our CA host.
+ """
+ ldap2 = self.api.Backend.ldap2
+ if host_has_service(api.env.ca_host, ldap2, "CA"):
+ return api.env.ca_host
+ if api.env.host != api.env.ca_host:
+ if host_has_service(api.env.host, ldap2, "CA"):
+ return api.env.host
+ host = select_any_master(ldap2)
+ if host:
+ return host
+ else:
+ return api.env.ca_host
+
+ def __enter__(self):
+ """Log into the REST API"""
+ if self.cookie is not None:
+ return
+ status, status_text, resp_headers, resp_body = dogtag.https_request(
+ self.ca_host, self.env.ca_agent_port, '/ca/rest/account/login',
+ self.sec_dir, self.password, self.ipa_certificate_nickname,
+ method='GET'
+ )
+ cookies = ipapython.cookie.Cookie.parse(resp_headers.get('set-cookie', ''))
+ if status != 200 or len(cookies) == 0:
+ raise errors.RemoteRetrieveError(reason=_('Failed to authenticate to CA REST API'))
+ self.cookie = str(cookies[0])
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ """Log out of the REST API"""
+ dogtag.https_request(
+ self.ca_host, self.env.ca_agent_port, '/ca/rest/account/logout',
+ self.sec_dir, self.password, self.ipa_certificate_nickname,
+ method='GET'
+ )
+ self.cookie = None
+
+ def _ssldo(self, method, path, headers=None, body=None):
+ """
+ :param url: The URL to post to.
+ :param kw: Keyword arguments to encode into POST body.
+ :return: (http_status, http_reason_phrase, http_headers, http_body)
+ as (integer, unicode, dict, str)
+
+ Perform an HTTPS request
+ """
+ if self.cookie is None:
+ raise errors.RemoteRetrieveError(
+ reason=_("REST API is not logged in."))
+
+ headers = headers or {}
+ headers['Cookie'] = self.cookie
+
+ resource = os.path.join('/ca/rest', self.path, path)
+
+ # perform main request
+ status, status_text, resp_headers, resp_body = dogtag.https_request(
+ self.ca_host, self.env.ca_agent_port, resource,
+ self.sec_dir, self.password, self.ipa_certificate_nickname,
+ method=method, headers=headers, body=body
+ )
+ if status < 200 or status >= 300:
+ explanation = self._parse_dogtag_error(resp_body) or ''
+ raise errors.RemoteRetrieveError(
+ reason=_('Non-2xx response from CA REST API: %(status)d %(status_text)s. %(explanation)s')
+ % {'status': status, 'status_text': status_text, 'explanation': explanation}
+ )
+ return (status, status_text, resp_headers, resp_body)
+
+
+class ra_certprofile(RestClient):
+ """
+ Profile management backend plugin.
+ """
+ path = 'profiles'
+
+ def create_profile(self, profile_data):
+ """
+ Import the profile into Dogtag
+ """
+ self._ssldo('POST', 'raw',
+ headers={
+ 'Content-type': 'application/xml',
+ 'Accept': 'application/json',
+ },
+ body=profile_data
+ )
+
+ def enable_profile(self, profile_id):
+ """
+ Enable the profile in Dogtag
+ """
+ self._ssldo('POST', profile_id + '?action=enable')
+
+ def disable_profile(self, profile_id):
+ """
+ Enable the profile in Dogtag
+ """
+ self._ssldo('POST', profile_id + '?action=disable')
+
+ def delete_profile(self, profile_id):
+ """
+ Delete the profile from Dogtag
+ """
+ self._ssldo('DELETE', profile_id, headers={'Accept': 'application/json'})
+
+api.register(ra_certprofile)