URL: https://github.com/freeipa/freeipa/pull/399 Author: dkupka Title: #399: Certificate mapping test Action: synchronized
To pull the PR as Git branch: git remote add ghfreeipa https://github.com/freeipa/freeipa git fetch ghfreeipa pull/399/head:pr399 git checkout pr399
From cd72d4375c7a5b6d590b85cbe2ce0f049aa29d42 Mon Sep 17 00:00:00 2001 From: Florence Blanc-Renaud <f...@redhat.com> Date: Tue, 20 Dec 2016 16:21:58 +0100 Subject: [PATCH 1/9] Support for Certificate Identity Mapping See design http://www.freeipa.org/page/V4/Certificate_Identity_Mapping https://fedorahosted.org/freeipa/ticket/6542 --- ACI.txt | 16 +- API.txt | 184 +++++++++++++++++++ VERSION.m4 | 4 +- install/share/73certmap.ldif | 17 ++ install/share/Makefile.am | 1 + install/updates/73-certmap.update | 27 +++ install/updates/Makefile.am | 1 + ipalib/constants.py | 4 + ipapython/dn.py | 9 + ipaserver/install/dsinstance.py | 1 + ipaserver/plugins/baseuser.py | 174 +++++++++++++++++- ipaserver/plugins/certmap.py | 357 +++++++++++++++++++++++++++++++++++++ ipaserver/plugins/stageuser.py | 16 +- ipaserver/plugins/user.py | 23 ++- ipatests/test_ipapython/test_dn.py | 20 +++ 15 files changed, 843 insertions(+), 11 deletions(-) create mode 100644 install/share/73certmap.ldif create mode 100644 install/updates/73-certmap.update create mode 100644 ipaserver/plugins/certmap.py diff --git a/ACI.txt b/ACI.txt index 0b47489..a87fec1 100644 --- a/ACI.txt +++ b/ACI.txt @@ -40,6 +40,18 @@ dn: cn=caacls,cn=ca,dc=ipa,dc=example aci: (targetattr = "cn || description || ipaenabledflag")(targetfilter = "(objectclass=ipacaacl)")(version 3.0;acl "permission:System: Modify CA ACL";allow (write) groupdn = "ldap:///cn=System: Modify CA ACL,cn=permissions,cn=pbac,dc=ipa,dc=example";) dn: cn=caacls,cn=ca,dc=ipa,dc=example aci: (targetattr = "cn || createtimestamp || description || entryusn || hostcategory || ipacacategory || ipacertprofilecategory || ipaenabledflag || ipamemberca || ipamembercertprofile || ipauniqueid || member || memberhost || memberservice || memberuser || modifytimestamp || objectclass || servicecategory || usercategory")(targetfilter = "(objectclass=ipacaacl)")(version 3.0;acl "permission:System: Read CA ACLs";allow (compare,read,search) userdn = "ldap:///all";) +dn: cn=certmap,cn=ipa,cn=etc,dc=ipa,dc=example +aci: (targetattr = "ipacertmappromptusername")(targetfilter = "(objectclass=ipacertmapconfigobject)")(version 3.0;acl "permission:System: Modify Certmap Configuration";allow (write) groupdn = "ldap:///cn=System: Modify Certmap Configuration,cn=permissions,cn=pbac,dc=ipa,dc=example";) +dn: cn=certmap,cn=ipa,cn=etc,dc=ipa,dc=example +aci: (targetattr = "cn || ipacertmappromptusername")(targetfilter = "(objectclass=ipacertmapconfigobject)")(version 3.0;acl "permission:System: Read Certmap Configuration";allow (compare,read,search) userdn = "ldap:///all";) +dn: cn=certmaprules,cn=certmap,cn=ipa,cn=etc,dc=ipa,dc=example +aci: (targetfilter = "(objectclass=ipacertmaprule)")(version 3.0;acl "permission:System: Add Certmap Rules";allow (add) groupdn = "ldap:///cn=System: Add Certmap Rules,cn=permissions,cn=pbac,dc=ipa,dc=example";) +dn: cn=certmaprules,cn=certmap,cn=ipa,cn=etc,dc=ipa,dc=example +aci: (targetfilter = "(objectclass=ipacertmaprule)")(version 3.0;acl "permission:System: Delete Certmap Rules";allow (delete) groupdn = "ldap:///cn=System: Delete Certmap Rules,cn=permissions,cn=pbac,dc=ipa,dc=example";) +dn: cn=certmaprules,cn=certmap,cn=ipa,cn=etc,dc=ipa,dc=example +aci: (targetattr = "associateddomain || cn || description || ipacertmapissuer || ipacertmapmaprule || ipacertmapmatchrule || ipacertmappriority || ipaenabledflag || objectclass")(targetfilter = "(objectclass=ipacertmaprule)")(version 3.0;acl "permission:System: Modify Certmap Rules";allow (write) groupdn = "ldap:///cn=System: Modify Certmap Rules,cn=permissions,cn=pbac,dc=ipa,dc=example";) +dn: cn=certmaprules,cn=certmap,cn=ipa,cn=etc,dc=ipa,dc=example +aci: (targetattr = "associateddomain || cn || createtimestamp || description || entryusn || ipacertmapissuer || ipacertmapmaprule || ipacertmapmatchrule || ipacertmappriority || ipaenabledflag || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipacertmaprule)")(version 3.0;acl "permission:System: Read Certmap Rules";allow (compare,read,search) userdn = "ldap:///all";) 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 @@ -337,6 +349,8 @@ aci: (targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:S dn: cn=users,cn=accounts,dc=ipa,dc=example aci: (targetattr = "krbprincipalkey || passwordhistory || sambalmpassword || sambantpassword || userpassword")(targetfilter = "(&(!(memberOf=cn=admins,cn=groups,cn=accounts,dc=ipa,dc=example))(objectclass=posixaccount))")(version 3.0;acl "permission:System: Change User password";allow (write) groupdn = "ldap:///cn=System: Change User password,cn=permissions,cn=pbac,dc=ipa,dc=example";) dn: cn=users,cn=accounts,dc=ipa,dc=example +aci: (targetattr = "ipacertmapdata || objectclass")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Manage User Certificate Mappings";allow (write) groupdn = "ldap:///cn=System: Manage User Certificate Mappings,cn=permissions,cn=pbac,dc=ipa,dc=example";) +dn: cn=users,cn=accounts,dc=ipa,dc=example aci: (targetattr = "usercertificate")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Manage User Certificates";allow (write) groupdn = "ldap:///cn=System: Manage User Certificates,cn=permissions,cn=pbac,dc=ipa,dc=example";) dn: cn=users,cn=accounts,dc=ipa,dc=example aci: (targetattr = "krbcanonicalname || krbprincipalname")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Manage User Principals";allow (write) groupdn = "ldap:///cn=System: Manage User Principals,cn=permissions,cn=pbac,dc=ipa,dc=example";) @@ -347,7 +361,7 @@ aci: (targetattr = "businesscategory || carlicense || cn || departmentnumber || dn: cn=UPG Definition,cn=Definitions,cn=Managed Entries,cn=etc,dc=ipa,dc=example aci: (targetattr = "*")(target = "ldap:///cn=UPG Definition,cn=Definitions,cn=Managed Entries,cn=etc,dc=ipa,dc=example")(version 3.0;acl "permission:System: Read UPG Definition";allow (compare,read,search) groupdn = "ldap:///cn=System: Read UPG Definition,cn=permissions,cn=pbac,dc=ipa,dc=example";) dn: cn=users,cn=accounts,dc=ipa,dc=example -aci: (targetattr = "audio || businesscategory || carlicense || departmentnumber || destinationindicator || employeenumber || employeetype || facsimiletelephonenumber || homephone || homepostaladdress || inetuserhttpurl || inetuserstatus || internationalisdnnumber || jpegphoto || l || labeleduri || mail || mobile || o || ou || pager || photo || physicaldeliveryofficename || postaladdress || postalcode || postofficebox || preferreddeliverymethod || preferredlanguage || registeredaddress || roomnumber || secretary || seealso || st || street || telephonenumber || teletexterminalidentifier || telexnumber || usercertificate || usersmimecertificate || x121address || x500uniqueidentifier")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User Addressbook Attributes";allow (compare,read,search) userdn = "ldap:///all";) +aci: (targetattr = "audio || businesscategory || carlicense || departmentnumber || destinationindicator || employeenumber || employeetype || facsimiletelephonenumber || homephone || homepostaladdress || inetuserhttpurl || inetuserstatus || internationalisdnnumber || ipacertmapdata || jpegphoto || l || labeleduri || mail || mobile || o || ou || pager || photo || physicaldeliveryofficename || postaladdress || postalcode || postofficebox || preferreddeliverymethod || preferredlanguage || registeredaddress || roomnumber || secretary || seealso || st || street || telephonenumber || teletexterminalidentifier || telexnumber || usercertificate || usersmimecertificate || x121address || x500uniqueidentifier")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User Addressbook Attributes";allow (compare,read,search) userdn = "ldap:///all";) dn: dc=ipa,dc=example aci: (targetattr = "cn || createtimestamp || entryusn || gecos || gidnumber || homedirectory || loginshell || modifytimestamp || objectclass || uid || uidnumber")(target = "ldap:///cn=users,cn=compat,dc=ipa,dc=example")(version 3.0;acl "permission:System: Read User Compat Tree";allow (compare,read,search) userdn = "ldap:///anyone";) dn: cn=users,cn=accounts,dc=ipa,dc=example diff --git a/API.txt b/API.txt index 543cec5..adcb858 100644 --- a/API.txt +++ b/API.txt @@ -824,6 +824,119 @@ option: Str('version?') output: Entry('result') output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>]) output: PrimaryKey('value') +command: certmapconfig_mod/1 +args: 0,8,3 +option: Str('addattr*', cli_name='addattr') +option: Flag('all', autofill=True, cli_name='all', default=False) +option: Str('delattr*', cli_name='delattr') +option: Bool('ipacertmappromptusername?', autofill=False, cli_name='promptusername') +option: Flag('raw', autofill=True, cli_name='raw', default=False) +option: Flag('rights', autofill=True, default=False) +option: Str('setattr*', cli_name='setattr') +option: Str('version?') +output: Entry('result') +output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>]) +output: PrimaryKey('value') +command: certmapconfig_show/1 +args: 0,4,3 +option: Flag('all', autofill=True, cli_name='all', default=False) +option: Flag('raw', autofill=True, cli_name='raw', default=False) +option: Flag('rights', autofill=True, default=False) +option: Str('version?') +output: Entry('result') +output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>]) +output: PrimaryKey('value') +command: certmaprule_add/1 +args: 1,12,3 +arg: Str('cn', cli_name='rulename') +option: Str('addattr*', cli_name='addattr') +option: Flag('all', autofill=True, cli_name='all', default=False) +option: Str('associateddomain*', cli_name='domain') +option: Str('description?', cli_name='desc') +option: DNParam('ipacertmapissuer?', cli_name='issuer') +option: Str('ipacertmapmaprule?', cli_name='maprule') +option: Str('ipacertmapmatchrule?', cli_name='matchrule') +option: Int('ipacertmappriority?', cli_name='priority') +option: Flag('ipaenabledflag?', autofill=True, default=True) +option: Flag('raw', autofill=True, cli_name='raw', default=False) +option: Str('setattr*', cli_name='setattr') +option: Str('version?') +output: Entry('result') +output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>]) +output: PrimaryKey('value') +command: certmaprule_del/1 +args: 1,2,3 +arg: Str('cn+', cli_name='rulename') +option: Flag('continue', autofill=True, cli_name='continue', default=False) +option: Str('version?') +output: Output('result', type=[<type 'dict'>]) +output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>]) +output: ListOfPrimaryKeys('value') +command: certmaprule_disable/1 +args: 1,1,3 +arg: Str('cn', cli_name='rulename') +option: Str('version?') +output: Output('result', type=[<type 'bool'>]) +output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>]) +output: PrimaryKey('value') +command: certmaprule_enable/1 +args: 1,1,3 +arg: Str('cn', cli_name='rulename') +option: Str('version?') +output: Output('result', type=[<type 'bool'>]) +output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>]) +output: PrimaryKey('value') +command: certmaprule_find/1 +args: 1,14,4 +arg: Str('criteria?') +option: Flag('all', autofill=True, cli_name='all', default=False) +option: Str('associateddomain*', autofill=False, cli_name='domain') +option: Str('cn?', autofill=False, cli_name='rulename') +option: Str('description?', autofill=False, cli_name='desc') +option: DNParam('ipacertmapissuer?', autofill=False, cli_name='issuer') +option: Str('ipacertmapmaprule?', autofill=False, cli_name='maprule') +option: Str('ipacertmapmatchrule?', autofill=False, cli_name='matchrule') +option: Int('ipacertmappriority?', autofill=False, cli_name='priority') +option: Bool('ipaenabledflag?', autofill=False, default=True) +option: Flag('pkey_only?', autofill=True, default=False) +option: Flag('raw', autofill=True, cli_name='raw', default=False) +option: Int('sizelimit?', autofill=False) +option: Int('timelimit?', autofill=False) +option: Str('version?') +output: Output('count', type=[<type 'int'>]) +output: ListOfEntries('result') +output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>]) +output: Output('truncated', type=[<type 'bool'>]) +command: certmaprule_mod/1 +args: 1,14,3 +arg: Str('cn', cli_name='rulename') +option: Str('addattr*', cli_name='addattr') +option: Flag('all', autofill=True, cli_name='all', default=False) +option: Str('associateddomain*', autofill=False, cli_name='domain') +option: Str('delattr*', cli_name='delattr') +option: Str('description?', autofill=False, cli_name='desc') +option: DNParam('ipacertmapissuer?', autofill=False, cli_name='issuer') +option: Str('ipacertmapmaprule?', autofill=False, cli_name='maprule') +option: Str('ipacertmapmatchrule?', autofill=False, cli_name='matchrule') +option: Int('ipacertmappriority?', autofill=False, cli_name='priority') +option: Flag('ipaenabledflag?', autofill=True, default=True) +option: Flag('raw', autofill=True, cli_name='raw', default=False) +option: Flag('rights', autofill=True, default=False) +option: Str('setattr*', cli_name='setattr') +option: Str('version?') +output: Entry('result') +output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>]) +output: PrimaryKey('value') +command: certmaprule_show/1 +args: 1,4,3 +arg: Str('cn', cli_name='rulename') +option: Flag('all', autofill=True, cli_name='all', default=False) +option: Flag('raw', autofill=True, cli_name='raw', default=False) +option: Flag('rights', autofill=True, default=False) +option: Str('version?') +output: Entry('result') +output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>]) +output: PrimaryKey('value') command: certprofile_del/1 args: 1,2,3 arg: Str('cn+', cli_name='id') @@ -4751,6 +4864,20 @@ option: Str('version?') output: Entry('result') output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>]) output: PrimaryKey('value') +command: stageuser_add_certmap/1 +args: 1,8,3 +arg: Str('uid', cli_name='login') +option: Flag('all', autofill=True, cli_name='all', default=False) +option: Str('ipacertmapdata*', alwaysask=False, cli_name='data') +option: DNParam('issuer?', cli_name='issuer') +option: Flag('no_members', autofill=True, default=False) +option: Flag('raw', autofill=True, cli_name='raw', default=False) +option: DNParam('subject?', cli_name='subject') +option: Bytes('usercertificate*', cli_name='certificate') +option: Str('version?') +output: Entry('result') +output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>]) +output: PrimaryKey('value') command: stageuser_add_manager/1 args: 1,5,3 arg: Str('uid', cli_name='login') @@ -4882,6 +5009,20 @@ option: Str('version?') output: Entry('result') output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>]) output: PrimaryKey('value') +command: stageuser_remove_certmap/1 +args: 1,8,3 +arg: Str('uid', cli_name='login') +option: Flag('all', autofill=True, cli_name='all', default=False) +option: Str('ipacertmapdata*', alwaysask=False, cli_name='data') +option: DNParam('issuer?', cli_name='issuer') +option: Flag('no_members', autofill=True, default=False) +option: Flag('raw', autofill=True, cli_name='raw', default=False) +option: DNParam('subject?', cli_name='subject') +option: Bytes('usercertificate*', cli_name='certificate') +option: Str('version?') +output: Entry('result') +output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>]) +output: PrimaryKey('value') command: stageuser_remove_manager/1 args: 1,5,3 arg: Str('uid', cli_name='login') @@ -5752,6 +5893,20 @@ option: Str('version?') output: Entry('result') output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>]) output: PrimaryKey('value') +command: user_add_certmap/1 +args: 1,8,3 +arg: Str('uid', cli_name='login') +option: Flag('all', autofill=True, cli_name='all', default=False) +option: Str('ipacertmapdata*', alwaysask=False, cli_name='data') +option: DNParam('issuer?', cli_name='issuer') +option: Flag('no_members', autofill=True, default=False) +option: Flag('raw', autofill=True, cli_name='raw', default=False) +option: DNParam('subject?', cli_name='subject') +option: Bytes('usercertificate*', cli_name='certificate') +option: Str('version?') +output: Entry('result') +output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>]) +output: PrimaryKey('value') command: user_add_manager/1 args: 1,5,3 arg: Str('uid', cli_name='login') @@ -5924,6 +6079,20 @@ option: Str('version?') output: Entry('result') output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>]) output: PrimaryKey('value') +command: user_remove_certmap/1 +args: 1,8,3 +arg: Str('uid', cli_name='login') +option: Flag('all', autofill=True, cli_name='all', default=False) +option: Str('ipacertmapdata*', alwaysask=False, cli_name='data') +option: DNParam('issuer?', cli_name='issuer') +option: Flag('no_members', autofill=True, default=False) +option: Flag('raw', autofill=True, cli_name='raw', default=False) +option: DNParam('subject?', cli_name='subject') +option: Bytes('usercertificate*', cli_name='certificate') +option: Str('version?') +output: Entry('result') +output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>]) +output: PrimaryKey('value') command: user_remove_manager/1 args: 1,5,3 arg: Str('uid', cli_name='login') @@ -6307,6 +6476,17 @@ default: cert_request/1 default: cert_revoke/1 default: cert_show/1 default: cert_status/1 +default: certmapconfig/1 +default: certmapconfig_mod/1 +default: certmapconfig_show/1 +default: certmaprule/1 +default: certmaprule_add/1 +default: certmaprule_del/1 +default: certmaprule_disable/1 +default: certmaprule_enable/1 +default: certmaprule_find/1 +default: certmaprule_mod/1 +default: certmaprule_show/1 default: certprofile/1 default: certprofile_del/1 default: certprofile_find/1 @@ -6661,10 +6841,12 @@ default: sidgen_was_run/1 default: stageuser/1 default: stageuser_activate/1 default: stageuser_add/1 +default: stageuser_add_certmap/1 default: stageuser_add_manager/1 default: stageuser_del/1 default: stageuser_find/1 default: stageuser_mod/1 +default: stageuser_remove_certmap/1 default: stageuser_remove_manager/1 default: stageuser_show/1 default: sudocmd/1 @@ -6741,6 +6923,7 @@ default: trustdomain_mod/1 default: user/1 default: user_add/1 default: user_add_cert/1 +default: user_add_certmap/1 default: user_add_manager/1 default: user_add_principal/1 default: user_del/1 @@ -6749,6 +6932,7 @@ default: user_enable/1 default: user_find/1 default: user_mod/1 default: user_remove_cert/1 +default: user_remove_certmap/1 default: user_remove_manager/1 default: user_remove_principal/1 default: user_show/1 diff --git a/VERSION.m4 b/VERSION.m4 index 36929ee..187092c 100644 --- a/VERSION.m4 +++ b/VERSION.m4 @@ -73,8 +73,8 @@ define(IPA_DATA_VERSION, 20100614120000) # # ######################################################## define(IPA_API_VERSION_MAJOR, 2) -define(IPA_API_VERSION_MINOR, 217) -# Last change: Add options to write lightweight CA cert or chain to file +define(IPA_API_VERSION_MINOR, 218) +# Last change: Support for Certificate Identity Mapping ######################################################## diff --git a/install/share/73certmap.ldif b/install/share/73certmap.ldif new file mode 100644 index 0000000..fb70f88 --- /dev/null +++ b/install/share/73certmap.ldif @@ -0,0 +1,17 @@ +## IPA Base OID: +## +## Attributes: 2.16.840.1.113730.3.8.22.x +## ObjectClasses: 2.16.840.1.113730.3.8.23.y +## +dn: cn=schema +attributeTypes: (2.16.840.1.113730.3.8.22.1 NAME 'ipaCertMapVersion' DESC 'IPA Certificate Mapping version' EQUALITY integerMatch ORDERING integerOrderingMatch SINGLE-VALUE SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 X-ORIGIN 'IPA v4.5' ) +attributeTypes: (2.16.840.1.113730.3.8.22.2 NAME 'ipaCertMapPromptUsername' DESC 'Prompt for the username when multiple identities are mapped to a certificate' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'IPA v4.5' ) +attributeTypes: (2.16.840.1.113730.3.8.22.3 NAME 'ipaCertMapMapRule' DESC 'Certificate Mapping Rule' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'IPA v4.5' ) +attributeTypes: (2.16.840.1.113730.3.8.22.4 NAME 'ipaCertMapMatchRule' DESC 'Certificate Matching Rule' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'IPA v4.5' ) +attributeTypes: (2.16.840.1.113730.3.8.22.5 NAME 'ipaCertMapData' DESC 'Certificate Mapping Data' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.5' ) +attributeTypes: (2.16.840.1.113730.3.8.22.6 NAME 'ipaCertMapIssuer' DESC 'Certificate Issuer' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'IPA v4.5' ) +attributeTypes: (2.16.840.1.113730.3.8.22.7 NAME 'ipaCertMapPriority' DESC 'Rule priority' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.5' ) +objectClasses: (2.16.840.1.113730.3.8.23.1 NAME 'ipaCertMapContainer' DESC 'IPA Certificate Mapping container' AUXILIARY MUST ( ipaCertMapVersion ) X-ORIGIN 'IPA v4.5' ) +objectClasses: (2.16.840.1.113730.3.8.23.2 NAME 'ipaCertMapConfigObject' DESC 'IPA Certificate Mapping global config options' SUP top STRUCTURAL MAY ipaCertMapPromptUsername X-ORIGIN 'IPA v4.5' ) +objectClasses: (2.16.840.1.113730.3.8.23.3 NAME 'ipaCertMapRule' DESC 'IPA Certificate Mapping rule' SUP top STRUCTURAL MUST cn MAY ( description $ ipaCertMapIssuer $ ipaCertMapMapRule $ ipaCertMapMatchRule $ associatedDomain $ ipaCertMapPriority $ ipaEnabledFlag ) X-ORIGIN 'IPA v4.5' ) +objectClasses: (2.16.840.1.113730.3.8.23.4 NAME 'ipaCertMapObject' DESC 'IPA Object for Certificate Mapping' AUXILIARY MAY ipaCertMapData X-ORIGIN 'IPA v4.5' ) diff --git a/install/share/Makefile.am b/install/share/Makefile.am index 10de84d..6d07aec 100644 --- a/install/share/Makefile.am +++ b/install/share/Makefile.am @@ -26,6 +26,7 @@ dist_app_DATA = \ 70topology.ldif \ 71idviews.ldif \ 72domainlevels.ldif \ + 73certmap.ldif \ bootstrap-template.ldif \ ca-topology.uldif \ caJarSigningCert.cfg.template \ diff --git a/install/updates/73-certmap.update b/install/updates/73-certmap.update new file mode 100644 index 0000000..ede7d9b --- /dev/null +++ b/install/updates/73-certmap.update @@ -0,0 +1,27 @@ +# Configuration for Certificate Identity Mapping +dn: cn=certmap,cn=ipa,cn=etc,$SUFFIX +default:objectclass: top +default:objectclass: nsContainer +default:objectclass: ipaConfigObject +default:objectclass: ipaCertMapContainer +default:objectclass: ipaCertMapConfigObject +default:cn: certmap +default:ipaconfigstring: CertMapVersion 1 +default:ipacertmapversion: 1 +default:ipaCertMapPromptUsername: FALSE + +dn: cn=certmaprules,cn=certmap,cn=ipa,cn=etc,$SUFFIX +default:objectclass: top +default:objectclass: nsContainer +default:cn: certmaprules + +# Certificate Identity Mapping Administrators +dn: cn=Certificate Identity Mapping Administrators,cn=privileges,cn=pbac,$SUFFIX +default:objectClass: top +default:objectClass: groupofnames +default:objectClass: nestedgroup +default:cn: Certificate Identity Mapping Administrators +default:description: Certificate Identity Mapping Administrators + +dn: $SUFFIX +add:aci: (targetattr = "ipacertmapdata")(targattrfilters="add=objectclass:(objectclass=ipacertmapobject)")(version 3.0;acl "selfservice:Users can manage their own X.509 certificate identity mappings";allow (write) userdn = "ldap:///self";) diff --git a/install/updates/Makefile.am b/install/updates/Makefile.am index e8a55e1..0ff0edb 100644 --- a/install/updates/Makefile.am +++ b/install/updates/Makefile.am @@ -61,6 +61,7 @@ app_DATA = \ 72-domainlevels.update \ 73-custodia.update \ 73-winsync.update \ + 73-certmap.update \ 90-post_upgrade_plugins.update \ $(NULL) diff --git a/ipalib/constants.py b/ipalib/constants.py index 81643da..a66626a 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -122,6 +122,10 @@ ('container_dnsservers', DN(('cn', 'servers'), ('cn', 'dns'))), ('container_custodia', DN(('cn', 'custodia'), ('cn', 'ipa'), ('cn', 'etc'))), ('container_sysaccounts', DN(('cn', 'sysaccounts'), ('cn', 'etc'))), + ('container_certmap', DN(('cn', 'certmap'), ('cn', 'ipa'), ('cn', 'etc'))), + ('container_certmaprules', + DN(('cn', 'certmaprules'), ('cn', 'certmap'), ('cn', 'ipa'), + ('cn', 'etc'))), # Ports, hosts, and URIs: ('xmlrpc_uri', 'http://localhost:8888/ipa/xml'), diff --git a/ipapython/dn.py b/ipapython/dn.py index 2f7655d..7c09af9 100644 --- a/ipapython/dn.py +++ b/ipapython/dn.py @@ -1419,6 +1419,15 @@ def rindex(self, pattern, start=None, end=None): raise ValueError("pattern not found") return i + def get_reverse(self): + ''' + Return a reverse copy of the DN. For instance, the reverse of + uid=user,dc=example,dc=com is dc=com,dc=example,uid=user + ''' + reverse_dn = DN(self) + reverse_dn.rdns.reverse() + return reverse_dn + _ATTR_NAME_BY_OID = { cryptography.x509.oid.NameOID.COMMON_NAME: 'CN', diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py index ceb7bf3..d64232e 100644 --- a/ipaserver/install/dsinstance.py +++ b/ipaserver/install/dsinstance.py @@ -70,6 +70,7 @@ "70topology.ldif", "71idviews.ldif", "72domainlevels.ldif", + "73certmap.ldif", "15rfc2307bis.ldif", "15rfc4876.ldif") diff --git a/ipaserver/plugins/baseuser.py b/ipaserver/plugins/baseuser.py index 85ad417..44afef9 100644 --- a/ipaserver/plugins/baseuser.py +++ b/ipaserver/plugins/baseuser.py @@ -19,14 +19,15 @@ import six -from ipalib import api, errors -from ipalib import Flag, Int, Password, Str, Bool, StrEnum, DateTime, Bytes +from ipalib import api, errors, x509 +from ipalib import ( + Flag, Int, Password, Str, Bool, StrEnum, DateTime, Bytes, DNParam) from ipalib.parameters import Principal from ipalib.plugable import Registry from .baseldap import ( DN, LDAPObject, LDAPCreate, LDAPUpdate, LDAPSearch, LDAPDelete, LDAPRetrieve, LDAPAddAttribute, LDAPRemoveAttribute, LDAPAddMember, - LDAPRemoveMember) + LDAPRemoveMember, LDAPAddAttributeViaOption, LDAPRemoveAttributeViaOption) from ipaserver.plugins.service import ( validate_certificate, validate_realm, normalize_principal) from ipalib.request import context @@ -134,7 +135,7 @@ class baseuser(LDAPObject): object_class_config = 'ipauserobjectclasses' possible_objectclasses = [ 'meporiginentry', 'ipauserauthtypeclass', 'ipauser', - 'ipatokenradiusproxyuser' + 'ipatokenradiusproxyuser', 'ipacertmapobject' ] disallow_object_classes = ['krbticketpolicyaux'] permission_filter_objectclasses = ['posixaccount'] @@ -146,7 +147,8 @@ class baseuser(LDAPObject): 'memberofindirect', 'ipauserauthtype', 'userclass', 'ipatokenradiusconfiglink', 'ipatokenradiususername', 'krbprincipalexpiration', 'usercertificate;binary', - 'krbprincipalname', 'krbcanonicalname' + 'krbprincipalname', 'krbcanonicalname', + 'ipacertmapdata' ] search_display_attributes = [ 'uid', 'givenname', 'sn', 'homedirectory', 'krbcanonicalname', @@ -360,6 +362,13 @@ class baseuser(LDAPObject): label=_('Certificate'), doc=_('Base-64 encoded user certificate'), ), + Str( + 'ipacertmapdata*', + cli_name='data', + label=_('Certificate mapping data'), + doc=_('Certificate mapping data'), + flags=['no_create', 'no_update', 'no_search'], + ), ) def normalize_and_validate_email(self, email, config=None): @@ -694,3 +703,158 @@ class baseuser_remove_principal(LDAPRemoveAttribute): def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): ensure_last_krbprincipalname(ldap, entry_attrs, *keys) return dn + + +certmap_options = ( + DNParam( + 'issuer?', + cli_name='issuer', + label=_('Issuer'), + doc=_('Issuer of the certificate'), + flags=['virtual_attribute', 'no_create', 'no_update'] + ), + DNParam( + 'subject?', + cli_name='subject', + label=_('Subject'), + doc=_('Subject of the certificate'), + flags=['virtual_attribute', 'no_create', 'no_update'] + ), + Bytes( + 'usercertificate*', validate_certificate, + cli_name='certificate', + label=_('Certificate'), + doc=_('Base-64 encoded user certificate'), + ), +) + + +def _convert_to_x500(name): + """ + Converts a (ipa) DN into a string representation following X500 order + """ + if name: + return str(name.get_reverse()) + return name + + +def _build_mapdata(subject, issuer): + issuer = _convert_to_x500(issuer) + subject = _convert_to_x500(subject) + return u'X509:{issuer}{subject}'.format( + issuer='<I>{}'.format(issuer) if issuer else '', + subject='<S>{}'.format(subject) if subject else '', + ) + + +def _convert_options_to_certmap(options): + """ + Converts options to ipacertmapdata + + When --subject --issuer or --usercertificate options are used, + the value for ipacertmapdata is built from extracting subject and issuer, + converting their values to X500 ordering and using the format + X509:<I>issuer<S>subject + For instance: + X509:<I>O=DOMAIN,CN=Certificate Authority<S>O=DOMAIN,CN=user + A list of values can be returned if --usercertificate is used multiple + times, or in conjunction with --subject --issuer. + """ + data = [] + for item in options.get('ipacertmapdata', ()): + data.append(item) + + if 'issuer' in options or 'subject' in options: + issuer = options.get('issuer') + subject = options.get('subject') + data.append(_build_mapdata(subject, issuer)) + + for dercert in options.get('usercertificate', ()): + cert = x509.load_certificate(dercert, x509.DER) + issuer = DN(cert.issuer) + subject = DN(cert.subject) + data.append(_build_mapdata(subject, issuer)) + + return data + + +class baseuser_add_certmap(LDAPAddAttributeViaOption): + __doc__ = _("Add one or more certificate mappings to the user entry.") + msg_summary = _('Added certificate mappings to user "%(value)s"') + + attribute = 'ipacertmapdata' + takes_options = certmap_options + + def get_options(self): + # ipacertmapdata is not mandatory as it can be built + # from the values subject+issuer or from reading usercertificate + for option in super(baseuser_add_certmap, self).get_options(): + if option.name in ['ipacertmapdata']: + yield option.clone(required=False, alwaysask=False) + else: + yield option.clone() + + def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, + **options): + # The 3 valid calls are + # --subject xx --issuer yy + # --certificate + # --data + # Check that at least one of the 3 formats is used + valid_options = ( + 'issuer', 'subject', 'ipacertmapdata', 'usercertificate') + if all(key not in options for key in valid_options): + raise errors.RequirementError(name='data') + + # The objectclass ipacertmapobject may not be present on + # existing user entries. We need to add it if we define a new + # value for ipacertmapdata + if 'objectclass' not in entry_attrs: + entry_attrs_old = ldap.get_entry(dn, ['objectclass']) + objclasses_lc = [x.lower() for x in entry_attrs_old['objectclass']] + if 'ipacertmapobject' not in objclasses_lc: + entry_attrs['objectclass'] = ['ipacertmapobject'] + + entry_attrs[self.attribute] = _convert_options_to_certmap(options) + + # if the command is called with --subject --issuer or --certificate + # we need to add ipacertmapdata to the attrs_list in order to + # display the resulting value in the command output + if 'ipacertmapdata' not in attrs_list: + attrs_list.append('ipacertmapdata') + + return dn + + +class baseuser_remove_certmap(LDAPRemoveAttributeViaOption): + __doc__ = _("Remove one or more certificate mappings from the user entry.") + msg_summary = _('Removed certificate mappings from user "%(value)s"') + + attribute = 'ipacertmapdata' + takes_options = certmap_options + + def get_options(self): + # ipacertmapdata is not mandatory as it can be built + # from the values subject+issuer or from reading usercertificate + for option in super(baseuser_remove_certmap, self).get_options(): + if option.name in ['ipacertmapdata']: + yield option.clone(required=False, alwaysask=False) + else: + yield option.clone() + + def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, + **options): + valid_options = ( + 'issuer', 'subject', 'ipacertmapdata', 'usercertificate') + if all(key not in options for key in valid_options): + raise errors.RequirementError(name='data') + + entry_attrs[self.attribute] = _convert_options_to_certmap(options) + + # if the command is called with --subject --issuer or --certificate + # we need to add ipacertmapdata to the attrs_list in order to + # display the resulting value in the command output + if 'ipacertmapdata' not in attrs_list: + attrs_list.append('ipacertmapdata') + + return dn diff --git a/ipaserver/plugins/certmap.py b/ipaserver/plugins/certmap.py new file mode 100644 index 0000000..04032df --- /dev/null +++ b/ipaserver/plugins/certmap.py @@ -0,0 +1,357 @@ +import six + +from ipalib import api, errors +from ipalib.parameters import Bool, DNParam, Flag, Int, Str +from ipalib.plugable import Registry +from ipalib.util import validate_domain_name +from .baseldap import ( + LDAPCreate, + LDAPDelete, + LDAPObject, + LDAPQuery, + LDAPRetrieve, + LDAPSearch, + LDAPUpdate, + pkey_to_value) +from ipalib import _, ngettext +from ipalib import output + + +if six.PY3: + unicode = str + +__doc__ = _(""" +Certificate Identity Mapping +""") + _(""" +Manage Certificate Identity Mapping configuration and rules. +""") + _(""" +IPA supports the use of certificates for authentication. Certificates can +either be stored in the user entry (full certificate in the usercertificate +attribute), or simply linked to the user entry through a mapping. +This code enables the management of the rules allowing to link a +certificate to a user entry. +""") + _(""" +EXAMPLES: +""") + _(""" + Display the Certificate Identity Mapping global configuration: + ipa certmapconfig-show +""") + _(""" + Modify Certificate Identity Mapping global configuration: + ipa certmapconfig-mod --promptusername=TRUE +""") + _(""" + Create a new Certificate Identity Mapping Rule: + ipa certmaprule-add rule1 --desc="Link certificate with subject and issuer" +""") + _(""" + Modify a Certificate Identity Mapping Rule: + ipa certmaprule-mod rule1 --maprule="<ALT-SEC-ID-I-S:altSecurityIdentities>" +""") + _(""" + Disable a Certificate Identity Mapping Rule: + ipa certmaprule-disable rule1 +""") + _(""" + Enable a Certificate Identity Mapping Rule: + ipa certmaprule-enable rule1 +""") + _(""" + Display information about a Certificate Identity Mapping Rule: + ipa certmaprule-show rule1 +""") + _(""" + Find all Certificate Identity Mapping Rules with the specified domain: + ipa certmaprule-find --domain example.com +""") + _(""" + Delete a Certificate Identity Mapping Rule: + ipa certmaprule-del rule1 +""") + +register = Registry() + + +def _domain_name_validator(ugettext, value): + try: + validate_domain_name(value) + except ValueError as e: + return unicode(e) + + +def _domain_name_normalizer(d): + return d.lower().rstrip('.') + + +@register() +class certmapconfig(LDAPObject): + """ + Certificate Identity Mapping configuration object + """ + object_name = _('Certificate Identity Mapping configuration options') + default_attributes = ['ipacertmappromptusername'] + + container_dn = api.env.container_certmap + + label = _('Certificate Identity Mapping Global Configuration') + label_singular = _('Certificate Identity Mapping Global Configuration') + + takes_params = ( + Bool( + 'ipacertmappromptusername', + cli_name='promptusername', + label=_('Prompt for the username'), + doc=_('Prompt for the username when multiple identities' + ' are mapped to a certificate'), + ), + ) + + permission_filter_objectclasses = ['ipacertmapconfigobject'] + managed_permissions = { + 'System: Read Certmap Configuration': { + 'replaces_global_anonymous_aci': True, + 'ipapermbindruletype': 'all', + 'ipapermright': {'read', 'search', 'compare'}, + 'ipapermdefaultattr': { + 'ipacertmappromptusername', + 'cn', + }, + }, + 'System: Modify Certmap Configuration': { + 'replaces_global_anonymous_aci': True, + 'ipapermright': {'write'}, + 'ipapermdefaultattr': { + 'ipacertmappromptusername', + }, + 'default_privileges': { + 'Certificate Identity Mapping Administrators'}, + }, + } + + +@register() +class certmapconfig_mod(LDAPUpdate): + __doc__ = _('Modify Certificate Identity Mapping configuration.') + + +@register() +class certmapconfig_show(LDAPRetrieve): + __doc__ = _('Show the current Certificate Identity Mapping configuration.') + + +@register() +class certmaprule(LDAPObject): + """ + Certificate Identity Mapping Rules + """ + + label = _('Certificate Identity Mapping Rules') + label_singular = _('Certificate Identity Mapping Rule') + + object_name = _('Certificate Identity Mapping Rule') + object_name_plural = _('Certificate Identity Mapping Rules') + object_class = ['ipacertmaprule'] + + container_dn = api.env.container_certmaprules + default_attributes = [ + 'cn', 'description', 'ipacertmapissuer', + 'ipacertmapmaprule', + 'ipacertmapmatchrule', + 'associateddomain', + 'ipacertmappriority', + 'ipaenabledflag' + ] + search_attributes = [ + 'cn', 'description', 'ipacertmapissuer', + 'ipacertmapmaprule', + 'ipacertmapmatchrule', + 'associateddomain', + 'ipacertmappriority', + 'ipaenabledflag' + ] + + takes_params = ( + Str( + 'cn', + cli_name='rulename', + primary_key=True, + label=_('Rule name'), + doc=_('Certificate Identity Mapping Rule name'), + ), + Str( + 'description?', + cli_name='desc', + label=_('Description'), + doc=_('Certificate Identity Mapping Rule description'), + ), + DNParam( + 'ipacertmapissuer?', + cli_name='issuer', + label=_('Issuer'), + doc=_('LDAP DN of the certificate issuer' + ' (CN=Certificate Authority,O=DOMAIN.COM)'), + ), + Str( + 'ipacertmapmaprule?', + cli_name='maprule', + label=_('Mapping rule'), + doc=_('Rule used to map the certificate with a user entry'), + ), + Str( + 'ipacertmapmatchrule?', + cli_name='matchrule', + label=_('Matching rule'), + doc=_('Rule used to check if a certificate can be used for' + ' authentication'), + ), + Str( + 'associateddomain*', + _domain_name_validator, + normalizer=_domain_name_normalizer, + cli_name='domain', + label=_('Domain name'), + doc=_('Domain where the user entry will be searched'), + ), + Int( + 'ipacertmappriority?', + cli_name='priority', + label=_('Priority'), + doc=_('Priority of the rule (higher number means lower priority'), + minvalue=0, + ), + Flag( + 'ipaenabledflag?', + label=_('Enabled'), + flags=['no_option'], + default=True + ), + ) + + permission_filter_objectclasses = ['ipacertmaprule'] + managed_permissions = { + 'System: Add Certmap Rules': { + 'replaces_global_anonymous_aci': True, + 'ipapermright': {'add'}, + 'default_privileges': { + 'Certificate Identity Mapping Administrators'}, + }, + 'System: Read Certmap Rules': { + 'replaces_global_anonymous_aci': True, + 'ipapermbindruletype': 'all', + 'ipapermright': {'read', 'search', 'compare'}, + 'ipapermdefaultattr': { + 'objectclass', 'cn', 'description', 'ipacertmapissuer', + 'ipacertmapmaprule', 'ipacertmapmatchrule', 'associateddomain', + 'ipacertmappriority', 'ipaenabledflag', + }, + }, + 'System: Delete Certmap Rules': { + 'replaces_global_anonymous_aci': True, + 'ipapermright': {'delete'}, + 'default_privileges': { + 'Certificate Identity Mapping Administrators'}, + }, + 'System: Modify Certmap Rules': { + 'replaces_global_anonymous_aci': True, + 'ipapermright': {'write'}, + 'ipapermdefaultattr': { + 'objectclass', 'cn', 'description', 'ipacertmapissuer', + 'ipacertmapmaprule', 'ipacertmapmatchrule', 'associateddomain', + 'ipacertmappriority', 'ipaenabledflag', + }, + 'default_privileges': { + 'Certificate Identity Mapping Administrators'}, + }, + } + + def get_dn(self, *keys, **options): + rulename = keys[-1] + dn = super(certmaprule, self).get_dn(rulename, **options) + return dn + + +@register() +class certmaprule_add(LDAPCreate): + __doc__ = _('Create a new Certificate Identity Mapping Rule.') + + msg_summary = _('Added Certificate Identity Mapping Rule "%(value)s"') + + +@register() +class certmaprule_mod(LDAPUpdate): + __doc__ = _('Modify a Certificate Identity Mapping Rule.') + + msg_summary = _('Modified Certificate Identity Mapping Rule "%(value)s"') + + +@register() +class certmaprule_find(LDAPSearch): + __doc__ = _('Search for Certificate Identity Mapping Rules.') + + msg_summary = ngettext( + '%(count)d Certificate Identity Mapping Rule matched', + '%(count)d Certificate Identity Mapping Rules matched', 0 + ) + + +@register() +class certmaprule_show(LDAPRetrieve): + __doc__ = _('Display information about a Certificate Identity Mapping' + ' Rule.') + + +@register() +class certmaprule_del(LDAPDelete): + __doc__ = _('Delete a Certificate Identity Mapping Rule.') + + msg_summary = _('Deleted Certificate Identity Mapping Rule "%(value)s"') + + +@register() +class certmaprule_enable(LDAPQuery): + __doc__ = _('Enable a Certificate Identity Mapping Rule.') + + msg_summary = _('Enabled Certificate Identity Mapping Rule "%(value)s"') + has_output = output.standard_value + + def execute(self, cn, **options): + ldap = self.obj.backend + + dn = self.obj.get_dn(cn) + try: + entry_attrs = ldap.get_entry(dn, ['ipaenabledflag']) + except errors.NotFound: + self.obj.handle_not_found(cn) + + entry_attrs['ipaenabledflag'] = ['TRUE'] + + try: + ldap.update_entry(entry_attrs) + except errors.EmptyModlist: + pass + + return dict( + result=True, + value=pkey_to_value(cn, options), + ) + + +@register() +class certmaprule_disable(LDAPQuery): + __doc__ = _('Disable a Certificate Identity Mapping Rule.') + + msg_summary = _('Disabled Certificate Identity Mapping Rule "%(value)s"') + has_output = output.standard_value + + def execute(self, cn, **options): + ldap = self.obj.backend + + dn = self.obj.get_dn(cn) + try: + entry_attrs = ldap.get_entry(dn, ['ipaenabledflag']) + except errors.NotFound: + self.obj.handle_not_found(cn) + + entry_attrs['ipaenabledflag'] = ['FALSE'] + + try: + ldap.update_entry(entry_attrs) + except errors.EmptyModlist: + pass + + return dict( + result=True, + value=pkey_to_value(cn, options), + ) diff --git a/ipaserver/plugins/stageuser.py b/ipaserver/plugins/stageuser.py index afd402e..548f5ca 100644 --- a/ipaserver/plugins/stageuser.py +++ b/ipaserver/plugins/stageuser.py @@ -40,7 +40,9 @@ NO_UPG_MAGIC, baseuser_output_params, baseuser_add_manager, - baseuser_remove_manager) + baseuser_remove_manager, + baseuser_add_certmap, + baseuser_remove_certmap) from ipalib.request import context from ipalib.util import set_krbcanonicalname from ipalib import _, ngettext @@ -744,3 +746,15 @@ class stageuser_add_manager(baseuser_add_manager): @register() class stageuser_remove_manager(baseuser_remove_manager): __doc__ = _("Remove a manager to the stage user entry") + + +@register() +class stageuser_add_certmap(baseuser_add_certmap): + __doc__ = _("Add one or more certificate mappings to the stage user" + " entry.") + + +@register() +class stageuser_remove_certmap(baseuser_remove_certmap): + __doc__ = _("Remove one or more certificate mappings from the stage user" + " entry.") diff --git a/ipaserver/plugins/user.py b/ipaserver/plugins/user.py index 6440548..7a2c3b2 100644 --- a/ipaserver/plugins/user.py +++ b/ipaserver/plugins/user.py @@ -22,7 +22,6 @@ from time import gmtime, strftime import posixpath import os - import six from ipalib import api @@ -44,7 +43,9 @@ baseuser_add_manager, baseuser_remove_manager, baseuser_add_principal, - baseuser_remove_principal) + baseuser_remove_principal, + baseuser_add_certmap, + baseuser_remove_certmap) from .idviews import remove_ipaobject_overrides from ipalib.plugable import Registry from .baseldap import ( @@ -179,6 +180,7 @@ class user(baseuser): 'secretary', 'usercertificate', 'usersmimecertificate', 'x500uniqueidentifier', 'inetuserhttpurl', 'inetuserstatus', + 'ipacertmapdata', }, 'fixup_function': fix_addressbook_permission_bindrule, }, @@ -366,6 +368,13 @@ class user(baseuser): }, 'default_privileges': {'PassSync Service'}, }, + 'System: Manage User Certificate Mappings': { + 'ipapermright': {'write'}, + 'ipapermdefaultattr': {'ipacertmapdata', 'objectclass'}, + 'default_privileges': { + 'Certificate Identity Mapping Administrators' + }, + }, } takes_params = baseuser.takes_params + ( @@ -1201,6 +1210,16 @@ def post_callback(self, ldap, dn, entry_attrs, *keys, **options): @register() +class user_add_certmap(baseuser_add_certmap): + __doc__ = _("Add one or more certificate mappings to the user entry.") + + +@register() +class user_remove_certmap(baseuser_remove_certmap): + __doc__ = _("Remove one or more certificate mappings from the user entry.") + + +@register() class user_add_manager(baseuser_add_manager): __doc__ = _("Add a manager to the user entry") diff --git a/ipatests/test_ipapython/test_dn.py b/ipatests/test_ipapython/test_dn.py index 3ca3b57..d2fd94a 100644 --- a/ipatests/test_ipapython/test_dn.py +++ b/ipatests/test_ipapython/test_dn.py @@ -1184,6 +1184,26 @@ def test_hashing(self): self.assertFalse(dn3_a in s) self.assertFalse(dn3_b in s) + def test_reverse(self): + # reverse the null DN + nulldn = DN().get_reverse() + self.assertEqual(nulldn, DN()) + + # reverse a DN with a single RDN + dn1rev = DN(self.dn1).get_reverse() + self.assertEqual(dn1rev, self.dn1) + + # reverse a DN with 2 RDNs + dn3rev = DN(self.dn3).get_reverse() + self.assertEqual(dn3rev[0], self.dn3[1]) + self.assertEqual(dn3rev[1], self.dn3[0]) + + # reverse a longer DN + longdnrev = DN(self.base_container_dn).get_reverse() + l = len(self.base_container_dn) + for i in range(l): + self.assertEquals(longdnrev[i], self.base_container_dn[l-1-i]) + class TestEscapes(unittest.TestCase): def setUp(self): From f078d8873e022fceadaca3b0fa6fb8829367492a Mon Sep 17 00:00:00 2001 From: David Kupka <dku...@redhat.com> Date: Wed, 1 Feb 2017 11:36:32 +0100 Subject: [PATCH 2/9] tests: tracker: Split Tracker into one-purpose Trackers There are multiple types of entries and objects accessible in API and not all of them have the same set methods. Spliting Tracker into multiple trackers should reflect this better. --- ipatests/test_xmlrpc/tracker/base.py | 285 +++++++++++++++++++++-------------- 1 file changed, 172 insertions(+), 113 deletions(-) diff --git a/ipatests/test_xmlrpc/tracker/base.py b/ipatests/test_xmlrpc/tracker/base.py index aa88e6b..8b6e97e 100644 --- a/ipatests/test_xmlrpc/tracker/base.py +++ b/ipatests/test_xmlrpc/tracker/base.py @@ -15,61 +15,7 @@ from ipatests.util import Fuzzy -class Tracker(object): - """Wraps and tracks modifications to a plugin LDAP entry object - - Stores a copy of state of a plugin entry object and allows checking that - the state in the database is the same as expected. - This allows creating independent tests: the individual tests check - that the relevant changes have been made. At the same time - the entry doesn't need to be recreated and cleaned up for each test. - - Two attributes are used for tracking: ``exists`` (true if the entry is - supposed to exist) and ``attrs`` (a dict of LDAP attributes that are - expected to be returned from IPA commands). - - For commonly used operations, there is a helper method, e.g. - ``create``, ``update``, or ``find``, that does these steps: - - * ensure the entry exists (or does not exist, for "create") - * store the expected modifications - * get the IPA command to run, and run it - * check that the result matches the expected state - - Tests that require customization of these steps are expected to do them - manually, using lower-level methods. - Especially the first step (ensure the entry exists) is important for - achieving independent tests. - - The Tracker object also stores information about the entry, e.g. - ``dn``, ``rdn`` and ``name`` which is derived from DN property. - - To use this class, the programer must subclass it and provide the - implementation of following methods: - - * make_*_command -- implementing the API call for particular plugin - and operation (add, delete, ...) - These methods should use the make_command method - * check_* commands -- an assertion for a plugin command (CRUD) - * track_create -- to make an internal representation of the - entry - - Apart from overriding these methods, the subclass must provide the - distinguished name of the entry in `self.dn` property. - - It is also required to override the class variables defining the sets - of ldap attributes/keys for these operations specific to the plugin - being implemented. Take the host plugin test for an example. - - The implementation of these methods is not strictly enforced. - A missing method will cause a NotImplementedError during runtime - as a result. - """ - retrieve_keys = None - retrieve_all_keys = None - create_keys = None - update_keys = None - +class BaseTracker(object): _override_me_msg = "This method needs to be overridden in a subclass" def __init__(self, default_version=None): @@ -78,8 +24,6 @@ def __init__(self, default_version=None): self._dn = None self.attrs = {} - self.exists = False - @property def dn(self): """A property containing the distinguished name of the entry.""" @@ -138,53 +82,33 @@ def make_command(self, name, *args, **options): return functools.partial(self.run_command, name, *args, **options) def make_fixture(self, request): - """Make a pytest fixture for this tracker + """Make fixture for the tracker - The fixture ensures the plugin entry does not exist before - and after the tests that use it. + Don't do anything here. """ - del_command = self.make_delete_command() - try: - del_command() - except errors.NotFound: - pass - - def cleanup(): - existed = self.exists - try: - del_command() - except errors.NotFound: - if existed: - raise - self.exists = False - - request.addfinalizer(cleanup) - return self - def ensure_exists(self): - """If the entry does not exist (according to tracker state), create it - """ - if not self.exists: - self.create() - def ensure_missing(self): - """If the entry exists (according to tracker state), delete it - """ - if self.exists: - self.delete() +class RetrievableTracker(BaseTracker): + retrieve_keys = None + retrieve_all_keys = None - def make_create_command(self): - """Make function that creates the plugin entry object.""" + def make_retrieve_command(self, all=False, raw=False): + """Make function that retrieves the entry using ${CMD}_show""" raise NotImplementedError(self._override_me_msg) - def make_delete_command(self): - """Make function that deletes the plugin entry object.""" + def check_retrieve(self, result, all=False, raw=False): + """Check the plugin's `show` command result""" raise NotImplementedError(self._override_me_msg) - def make_retrieve_command(self, all=False, raw=False): - """Make function that retrieves the entry using ${CMD}_show""" - raise NotImplementedError(self._override_me_msg) + def retrieve(self, all=False, raw=False): + """Helper function to retrieve an entry and check the result""" + command = self.make_retrieve_command(all=all, raw=raw) + result = command() + self.check_retrieve(result, all=all, raw=raw) + + +class SearchableTracker(BaseTracker): def make_find_command(self, *args, **kwargs): """Make function that finds the entry using ${CMD}_find @@ -194,16 +118,62 @@ def make_find_command(self, *args, **kwargs): """ raise NotImplementedError(self._override_me_msg) + def check_find(self, result, all=False, raw=False): + """Check the plugin's `find` command result""" + raise NotImplementedError(self._override_me_msg) + + def find(self, all=False, raw=False): + """Helper function to search for this hosts and check the result""" + command = self.make_find_command(self.name, all=all, raw=raw) + result = command() + self.check_find(result, all=all, raw=raw) + + +class MutableTracker(BaseTracker): + update_keys = None + def make_update_command(self, updates): """Make function that modifies the entry using ${CMD}_mod""" raise NotImplementedError(self._override_me_msg) - def create(self): - """Helper function to create an entry and check the result""" - self.track_create() - command = self.make_create_command() + def check_update(self, result, extra_keys=()): + """Check the plugin's `mod` command result""" + raise NotImplementedError(self._override_me_msg) + + def update(self, updates, expected_updates=None): + """Helper function to update this hosts and check the result + + The ``updates`` are used as options to the *_mod command, + and the self.attrs is updated with this dict. + Additionally, self.attrs is updated with ``expected_updates``. + """ + if expected_updates is None: + expected_updates = {} + + command = self.make_update_command(updates) result = command() - self.check_create(result) + self.attrs.update(updates) + self.attrs.update(expected_updates) + for key, value in self.attrs.items(): + if value is None: + del self.attrs[key] + + self.check_update( + result, + extra_keys=set(updates.keys()) | set(expected_updates.keys()) + ) + + +class CreatableTracker(BaseTracker): + create_keys = None + + def __init__(self, default_version=None): + super(CreatableTracker, self).__init__(default_version=default_version) + self.exists = False + + def make_create_command(self): + """Make function that creates the plugin entry object.""" + raise NotImplementedError(self._override_me_msg) def track_create(self): """Update expected state for host creation @@ -225,12 +195,22 @@ def check_create(self, result): """Check plugin's add command result""" raise NotImplementedError(self._override_me_msg) - def delete(self): - """Helper function to delete a host and check the result""" - self.track_delete() - command = self.make_delete_command() + def create(self): + """Helper function to create an entry and check the result""" + self.track_create() + command = self.make_create_command() result = command() - self.check_delete(result) + self.check_create(result) + + def ensure_exists(self): + """If the entry does not exist (according to tracker state), create it + """ + if not self.exists: + self.create() + + def make_delete_command(self): + """Make function that deletes the plugin entry object.""" + raise NotImplementedError(self._override_me_msg) def track_delete(self): """Update expected state for host deletion""" @@ -241,14 +221,43 @@ def check_delete(self, result): """Check plugin's `del` command result""" raise NotImplementedError(self._override_me_msg) - def retrieve(self, all=False, raw=False): - """Helper function to retrieve an entry and check the result""" - command = self.make_retrieve_command(all=all, raw=raw) + def delete(self): + """Helper function to delete a host and check the result""" + self.track_delete() + command = self.make_delete_command() result = command() - self.check_retrieve(result, all=all, raw=raw) + self.check_delete(result) - def check_retrieve(self, result, all=False, raw=False): - """Check the plugin's `show` command result""" + def ensure_missing(self): + """If the entry exists (according to tracker state), delete it + """ + if self.exists: + self.delete() + + def make_fixture(self, request): + """Make a pytest fixture for this tracker + + The fixture ensures the plugin entry does not exist before + and after the tests that use it. + """ + del_command = self.make_delete_command() + try: + del_command() + except errors.NotFound: + pass + + def cleanup(): + existed = self.exists + try: + del_command() + except errors.NotFound: + if existed: + raise + self.exists = False + + request.addfinalizer(cleanup) + + return super(CreatableTracker, self).make_fixture(request) raise NotImplementedError(self._override_me_msg) def find(self, all=False, raw=False): @@ -282,6 +291,56 @@ def update(self, updates, expected_updates=None): self.check_update(result, extra_keys=set(updates.keys()) | set(expected_updates.keys())) - def check_update(self, result, extra_keys=()): - """Check the plugin's `mod` command result""" - raise NotImplementedError(self._override_me_msg) + +class Tracker(RetrievableTracker, SearchableTracker, MutableTracker, + CreatableTracker): + """Wraps and tracks modifications to a plugin LDAP entry object + + Stores a copy of state of a plugin entry object and allows checking that + the state in the database is the same as expected. + This allows creating independent tests: the individual tests check + that the relevant changes have been made. At the same time + the entry doesn't need to be recreated and cleaned up for each test. + + Two attributes are used for tracking: ``exists`` (true if the entry is + supposed to exist) and ``attrs`` (a dict of LDAP attributes that are + expected to be returned from IPA commands). + + For commonly used operations, there is a helper method, e.g. + ``create``, ``update``, or ``find``, that does these steps: + + * ensure the entry exists (or does not exist, for "create") + * store the expected modifications + * get the IPA command to run, and run it + * check that the result matches the expected state + + Tests that require customization of these steps are expected to do them + manually, using lower-level methods. + Especially the first step (ensure the entry exists) is important for + achieving independent tests. + + The Tracker object also stores information about the entry, e.g. + ``dn``, ``rdn`` and ``name`` which is derived from DN property. + + To use this class, the programer must subclass it and provide the + implementation of following methods: + + * make_*_command -- implementing the API call for particular plugin + and operation (add, delete, ...) + These methods should use the make_command method + * check_* commands -- an assertion for a plugin command (CRUD) + * track_create -- to make an internal representation of the + entry + + Apart from overriding these methods, the subclass must provide the + distinguished name of the entry in `self.dn` property. + + It is also required to override the class variables defining the sets + of ldap attributes/keys for these operations specific to the plugin + being implemented. Take the host plugin test for an example. + + The implementation of these methods is not strictly enforced. + A missing method will cause a NotImplementedError during runtime + as a result. + """ + pass From 1f5ca8f9b31064c5a51d3660a2f3a233a6ded907 Mon Sep 17 00:00:00 2001 From: David Kupka <dku...@redhat.com> Date: Wed, 1 Feb 2017 11:37:00 +0100 Subject: [PATCH 3/9] tests: tracker: Add EnableTracker to test *-{enable,disable} commands --- ipatests/test_xmlrpc/tracker/base.py | 65 ++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/ipatests/test_xmlrpc/tracker/base.py b/ipatests/test_xmlrpc/tracker/base.py index 8b6e97e..58a2891 100644 --- a/ipatests/test_xmlrpc/tracker/base.py +++ b/ipatests/test_xmlrpc/tracker/base.py @@ -258,38 +258,59 @@ def cleanup(): request.addfinalizer(cleanup) return super(CreatableTracker, self).make_fixture(request) + + +class EnableTracker(BaseTracker): + def __init__(self, default_version=None, enabled=True): + super(EnableTracker, self).__init__(default_version=default_version) + self.original_enabled = enabled + self.enabled = enabled + + def make_enable_command(self): + """Make function that enables the entry using ${CMD}_enable""" raise NotImplementedError(self._override_me_msg) - def find(self, all=False, raw=False): - """Helper function to search for this hosts and check the result""" - command = self.make_find_command(self.name, all=all, raw=raw) + def enable(self): + self.enabled = True + command = self.make_enable_command() result = command() - self.check_find(result, all=all, raw=raw) + self.check_enable(result) - def check_find(self, result, all=False, raw=False): - """Check the plugin's `find` command result""" + def check_enable(self, result): + """Check the plugin's `enable` command result""" raise NotImplementedError(self._override_me_msg) - def update(self, updates, expected_updates=None): - """Helper function to update this hosts and check the result + def make_disable_command(self): + """Make function that disables the entry using ${CMD}_disable""" + raise NotImplementedError(self._override_me_msg) - The ``updates`` are used as options to the *_mod command, - and the self.attrs is updated with this dict. - Additionally, self.attrs is updated with ``expected_updates``. + def disable(self): + self.enabled = False + command = self.make_disable_command() + result = command() + self.check_disable(result) + + def check_disable(self, result): + """Check the plugin's `disable` command result""" + raise NotImplementedError(self._override_me_msg) + + def make_fixture(self, request): + """Make a pytest fixture for this tracker + + The fixture ensures the plugin entry is in the same state + (enabled/disabled) after the test as it was before it. """ - if expected_updates is None: - expected_updates = {} + def cleanup(): + if self.original_enabled != self.enabled: + if self.original_enabled: + command = self.make_enable_command() + else: + command = self.make_disable_command() + command() - command = self.make_update_command(updates) - result = command() - self.attrs.update(updates) - self.attrs.update(expected_updates) - for key, value in self.attrs.items(): - if value is None: - del self.attrs[key] + request.addfinalizer(cleanup) - self.check_update(result, extra_keys=set(updates.keys()) | - set(expected_updates.keys())) + return super(EnableTracker, self).make_fixture(request) class Tracker(RetrievableTracker, SearchableTracker, MutableTracker, From cf4469326e43ff54e7d5f89e3f8d163a06481629 Mon Sep 17 00:00:00 2001 From: David Kupka <dku...@redhat.com> Date: Thu, 26 Jan 2017 17:11:16 +0100 Subject: [PATCH 4/9] tests: tracker: Add ConfigTracker to test *config-{mod,show} commands --- ipatests/test_xmlrpc/tracker/base.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/ipatests/test_xmlrpc/tracker/base.py b/ipatests/test_xmlrpc/tracker/base.py index 58a2891..4852ff6 100644 --- a/ipatests/test_xmlrpc/tracker/base.py +++ b/ipatests/test_xmlrpc/tracker/base.py @@ -131,6 +131,7 @@ def find(self, all=False, raw=False): class MutableTracker(BaseTracker): update_keys = None + singlevalue_keys = None def make_update_command(self, updates): """Make function that modifies the entry using ${CMD}_mod""" @@ -313,6 +314,33 @@ def cleanup(): return super(EnableTracker, self).make_fixture(request) +class ConfigTracker(RetrievableTracker, MutableTracker): + def make_fixture(self, request): + """Make a pytest fixture for this tracker + + Make sure that the state of entry in the end is the same + it was in the begining. + """ + retrieve = self.make_retrieve_command(all=True) + res = retrieve()['result'] + original_state = {} + for k, v in res.items(): + if k in self.update_keys: + original_state[k] = v[0] if k in self.singlevalue_keys else v + + def revert(): + update = self.make_update_command(original_state) + try: + update() + except errors.EmptyModlist: + # ignore no change + pass + + request.addfinalizer(revert) + + return super(ConfigTracker, self).make_fixture(request) + + class Tracker(RetrievableTracker, SearchableTracker, MutableTracker, CreatableTracker): """Wraps and tracks modifications to a plugin LDAP entry object From 90e42a623fa346ccfcd9d9e4feba5b3fd2608a22 Mon Sep 17 00:00:00 2001 From: David Kupka <dku...@redhat.com> Date: Wed, 1 Feb 2017 11:49:34 +0100 Subject: [PATCH 5/9] tests: tracker: Add CertmapTracker for testing certmap-* commands https://fedorahosted.org/freeipa/ticket/6542 --- ipatests/test_xmlrpc/objectclasses.py | 5 + ipatests/test_xmlrpc/tracker/certmap_plugin.py | 167 +++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 ipatests/test_xmlrpc/tracker/certmap_plugin.py diff --git a/ipatests/test_xmlrpc/objectclasses.py b/ipatests/test_xmlrpc/objectclasses.py index 1ea020b..0a15a21 100644 --- a/ipatests/test_xmlrpc/objectclasses.py +++ b/ipatests/test_xmlrpc/objectclasses.py @@ -227,3 +227,8 @@ u'top', u'ipaca', ] + +certmaprule = [ + u'top', + u'ipacertmaprule', +] diff --git a/ipatests/test_xmlrpc/tracker/certmap_plugin.py b/ipatests/test_xmlrpc/tracker/certmap_plugin.py new file mode 100644 index 0000000..d61be5d --- /dev/null +++ b/ipatests/test_xmlrpc/tracker/certmap_plugin.py @@ -0,0 +1,167 @@ +# +# Copyright (C) 2017 FreeIPA Contributors see COPYING for license +# + +from ipapython.dn import DN +from ipatests.test_xmlrpc.tracker.base import Tracker, EnableTracker +from ipatests.test_xmlrpc import objectclasses +from ipatests.util import assert_deepequal + + +class CertmapruleTracker(Tracker, EnableTracker): + """ Tracker for testin certmaprule plugin """ + retrieve_keys = { + u'dn', + u'cn', + u'description', + u'ipacertmapissuer', + u'ipacertmapmaprule', + u'ipacertmapmatchrule', + u'associateddomain', + u'ipacertmappriority', + u'ipaenabledflag' + } + retrieve_all_keys = retrieve_keys | {u'objectclass'} + create_keys = retrieve_keys | {u'objectclass'} + update_keys = retrieve_keys - {u'dn'} + + def __init__(self, cn, description, ipacertmapissuer, ipacertmapmaprule, + ipacertmapmatchrule, associateddomain, ipacertmappriority, + default_version=None): + super(CertmapruleTracker, self).__init__( + default_version=default_version) + + self.dn = DN((u'cn', cn,), + self.api.env.container_certmaprules, + self.api.env.basedn) + self.options = { + u'description': description, + u'ipacertmapissuer': ipacertmapissuer, + u'ipacertmapmaprule': ipacertmapmaprule, + u'ipacertmapmatchrule': ipacertmapmatchrule, + u'associateddomain': associateddomain, + u'ipacertmappriority': ipacertmappriority, + } + + def make_create_command(self, dont_fill=()): + kwargs = {k: v for k, v in self.options.items() if k not in dont_fill} + + return self.make_command('certmaprule_add', self.name, **kwargs) + + def track_create(self, dont_fill=()): + self.attrs = { + 'dn': self.dn, + 'cn': [self.name], + 'ipaenabledflag': [u'TRUE'], + 'objectclass': objectclasses.certmaprule, + } + self.attrs.update({ + k: [v] for k, v in self.options.items() if k not in dont_fill + }) + self.exists = True + + def check_create(self, result): + assert_deepequal(dict( + value=self.name, + summary=u'Added Certificate Identity Mapping Rule "{}"' + u''.format(self.name), + result=self.filter_attrs(self.create_keys), + ), result) + + def create(self, dont_fill=()): + self.track_create(dont_fill) + command = self.make_create_command(dont_fill) + result = command() + self.check_create(result) + + def make_delete_command(self): + return self.make_command('certmaprule_del', self.name) + + def check_delete(self, result): + assert_deepequal( + dict( + value=[self.name], + summary=u'Deleted Certificate Identity Mapping Rule "{}"' + ''.format(self.name), + result=dict(failed=[]), + ), + result + ) + + def make_retrieve_command(self, all=False, raw=False): + return self.make_command('certmaprule_show', self.name, all=all, + raw=raw) + + def check_retrieve(self, result, all=False, raw=False): + if all: + expected = self.filter_attrs(self.retrieve_all_keys) + else: + expected = self.filter_attrs(self.retrieve_keys) + assert_deepequal( + dict( + value=self.name, + summary=None, + result=expected, + ), + result + ) + + def make_find_command(self, *args, **kwargs): + return self.make_command('certmaprule_find', *args, **kwargs) + + def check_find(self, result, all=False, raw=False): + if all: + expected = self.filter_attrs(self.retrieve_all_keys) + else: + expected = self.filter_attrs(self.retrieve_keys) + assert_deepequal( + dict( + count=1, + truncated=False, + summary=u'1 Certificate Identity Mapping Rule matched', + result=[expected], + ), + result + ) + + def make_update_command(self, updates): + return self.make_command('certmaprule_mod', self.name, **updates) + + def check_update(self, result, extra_keys=()): + assert_deepequal( + dict( + value=self.name, + summary=u'Modified Certificate Identity Mapping Rule "{}"' + u''.format(self.name), + result=self.filter_attrs(self.update_keys | set(extra_keys)), + ), + result + ) + + def make_enable_command(self): + return self.make_command('certmaprule_enable', self.name) + + def check_enable(self, result): + assert_deepequal( + dict( + value=self.name, + summary=u'Enabled Certificate Identity Mapping Rule "{}"' + u''.format(self.name), + result=True, + ), + result + ) + + def make_disable_command(self): + return self.make_command('certmaprule_disable', self.name) + + def check_disable(self, result): + assert_deepequal( + dict( + value=self.name, + summary=u'Disabled Certificate Identity Mapping Rule "{}"' + u''.format(self.name), + result=True, + ), + result + ) From 311ae5c5d07ee7d6f3ad6f3c3e7124bab9fc608a Mon Sep 17 00:00:00 2001 From: David Kupka <dku...@redhat.com> Date: Wed, 1 Feb 2017 11:52:00 +0100 Subject: [PATCH 6/9] tests: certmap: Add basic tests for certmaprule commands https://fedorahosted.org/freeipa/ticket/6542 --- ipatests/test_xmlrpc/test_certmap_plugin.py | 107 ++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 ipatests/test_xmlrpc/test_certmap_plugin.py diff --git a/ipatests/test_xmlrpc/test_certmap_plugin.py b/ipatests/test_xmlrpc/test_certmap_plugin.py new file mode 100644 index 0000000..9343f9a --- /dev/null +++ b/ipatests/test_xmlrpc/test_certmap_plugin.py @@ -0,0 +1,107 @@ +# +# Copyright (C) 2017 FreeIPA Contributors see COPYING for license +# + +import itertools +import pytest + +from ipapython.dn import DN +from ipatests.test_xmlrpc.xmlrpc_test import XMLRPC_test +from ipatests.test_xmlrpc.tracker.certmap_plugin import CertmapruleTracker + +certmaprule_create_params = { + u'cn': u'test_rule', + u'description': u'Certificate mapping and matching rule for test ' + u'purposes', + u'ipacertmapissuer': DN('CN=CA,O=EXAMPLE.ORG'), + u'ipacertmapmaprule': u'arbitrary free-form mapping rule defined and ' + u'consumed by SSSD', + u'ipacertmapmatchrule': u'arbitrary free-form matching rule defined ' + u'and consumed by SSSD', + u'associateddomain': u'example.org', + u'ipacertmappriority': u'1', +} + +certmaprule_update_params = { + u'description': u'Changed description', + u'ipacertmapissuer': DN('CN=Changed CA,O=OTHER.ORG'), + u'ipacertmapmaprule': u'changed arbitrary mapping rule', + u'ipacertmapmatchrule': u'changed arbitrary maching rule', + u'associateddomain': u'changed.example.org', + u'ipacertmappriority': u'5', +} + +certmaprule_optional_params = ( + 'description', + 'ipacertmapissuer', + 'ipacertmapmaprule', + 'ipacertmapmatchrule', + 'ipaassociateddomain', + 'ipacertmappriority', +) + +def dontfill_idfn(dont_fill): + return u"dont_fill=({})".format(', '.join([ + u"{}".format(d) for d in dont_fill + ])) + + +def update_idfn(update): + return ', '.join(["{}: {}".format(k, v) for k, v in update.items()]) + + +@pytest.fixture(scope='class') +def certmap_rule(request): + tracker = CertmapruleTracker(**certmaprule_create_params) + return tracker.make_fixture(request) + + +class TestCRUD(XMLRPC_test): + @pytest.mark.parametrize( + 'dont_fill', + itertools.chain(*[ + itertools.combinations(certmaprule_optional_params, l) + for l in range(len(certmaprule_optional_params)+1) + ]), + ids=dontfill_idfn, + ) + def test_create(self, dont_fill, certmap_rule): + certmap_rule.ensure_missing() + try: + certmap_rule.create(dont_fill) + finally: + certmap_rule.ensure_missing() + + def test_retrieve(self, certmap_rule): + certmap_rule.ensure_exists() + certmap_rule.retrieve() + + def test_find(self, certmap_rule): + certmap_rule.ensure_exists() + certmap_rule.find() + + @pytest.mark.parametrize('update', [ + dict(u) for l in range(1, len(certmaprule_update_params)+1) + for u in itertools.combinations( + certmaprule_update_params.items(), l) + ], + ids=update_idfn, + ) + def test_update(self, update, certmap_rule): + certmap_rule.ensure_missing() + certmap_rule.ensure_exists() + certmap_rule.update(update, {o: [v] for o, v in update.items()}) + + def test_delete(self, certmap_rule): + certmap_rule.ensure_exists() + certmap_rule.delete() + + +class TestEnableDisable(XMLRPC_test): + def test_disable(self, certmap_rule): + certmap_rule.ensure_exists() + certmap_rule.disable() + + def test_enable(self, certmap_rule): + certmap_rule.ensure_exists() + certmap_rule.enable() From 56321a7b34af8c421d1b4e3d8d6ad1021b421380 Mon Sep 17 00:00:00 2001 From: David Kupka <dku...@redhat.com> Date: Tue, 24 Jan 2017 16:21:54 +0100 Subject: [PATCH 7/9] tests: certmap: Test permissions for certmap https://fedorahosted.org/freeipa/ticket/6542 --- ipatests/test_xmlrpc/test_certmap_plugin.py | 278 ++++++++++++++++++++++++++++ 1 file changed, 278 insertions(+) diff --git a/ipatests/test_xmlrpc/test_certmap_plugin.py b/ipatests/test_xmlrpc/test_certmap_plugin.py index 9343f9a..cdf667c 100644 --- a/ipatests/test_xmlrpc/test_certmap_plugin.py +++ b/ipatests/test_xmlrpc/test_certmap_plugin.py @@ -2,12 +2,18 @@ # Copyright (C) 2017 FreeIPA Contributors see COPYING for license # +from contextlib import contextmanager import itertools +from nose.tools import assert_raises import pytest +from ipalib import api, errors from ipapython.dn import DN from ipatests.test_xmlrpc.xmlrpc_test import XMLRPC_test from ipatests.test_xmlrpc.tracker.certmap_plugin import CertmapruleTracker +from ipatests.util import assert_deepequal +from ipatests.util import change_principal, unlock_principal_password + certmaprule_create_params = { u'cn': u'test_rule', @@ -40,6 +46,22 @@ 'ipacertmappriority', ) +CREATE_PERM = u'System: Add Certmap Rules' +READ_PERM = u'System: Read Certmap Rules' +UPDATE_PERM = u'System: Modify Certmap Rules' +DELETE_PERM = u'System: Delete Certmap Rules' + +certmaprule_permissions = { + u'C': CREATE_PERM, + u'R': READ_PERM, + u'U': UPDATE_PERM, + u'D': DELETE_PERM, +} + +CERTMAP_USER = u'cuser' +CERTMAP_PASSWD = 'Secret123' + + def dontfill_idfn(dont_fill): return u"dont_fill=({})".format(', '.join([ u"{}".format(d) for d in dont_fill @@ -105,3 +127,259 @@ def test_disable(self, certmap_rule): def test_enable(self, certmap_rule): certmap_rule.ensure_exists() certmap_rule.enable() + + +@contextmanager +def execute_with_expected(user, password, perms, exps, ok_expected=None): + """ + Run command as specified user. Check exception or return value + according provided rules. + + @param user Change to this user before calling the command + @param password User to change user + @param perms User has those permissions + @param exps Iterable containing tuple + (permission, exception_class, expected_result,) + If permission is missing command must raise exception of + exception_class. If exception class is None command must + raise Result(expected_result) + @param ok_expected When no permission is missing command must raise + Result(ok_expected) + """ + for perm, exception, expected in exps: + if perm not in perms: + break + else: + exception = None + expected = ok_expected + + with change_principal(user, password): + if exception: + with assert_raises(exception): + yield + else: + got = yield + if expected: + if got: + assert_deepequal(expected, got) + else: + assert("Command didn't returned") + + +def permissions_idfn(perms): + i = [] + for short_name, long_name in certmaprule_permissions.items(): + if long_name in perms: + i.append(short_name) + else: + i.append('-') + return ''.join(i) + + +def change_permissions_bindtype(perm, bindtype): + orig = api.Command.permission_show(perm)['result']['ipapermbindruletype'] + if orig != (bindtype,): + api.Command.permission_mod(perm, ipapermbindruletype=bindtype) + + return orig + + +@pytest.fixture(scope='class') +def bindtype_permission(request): + orig_bindtype = {} + # set bindtype to permission to actually test the permission + for perm_name in certmaprule_permissions.values(): + orig_bindtype[perm_name] = change_permissions_bindtype( + perm_name, u'permission') + + def finalize(): + for perm_name, bindtype in orig_bindtype.items(): + change_permissions_bindtype(perm_name, bindtype[0]) + + request.addfinalizer(finalize) + + +@pytest.fixture( + scope='class', + params=itertools.chain(*[ + itertools.combinations(certmaprule_permissions.values(), l) + for l in range(len(certmaprule_permissions.values())+1) + ]), + ids=permissions_idfn, +) +def certmap_user_permissions(request, bindtype_permission): + tmp_password = u'Initial123' + + priv_name = u'test_certmap_privilege' + role_name = u'test_certmap_role' + + api.Command.user_add(CERTMAP_USER, givenname=u'Certmap', sn=u'User', + userpassword=tmp_password) + unlock_principal_password(CERTMAP_USER, tmp_password, + CERTMAP_PASSWD) + + api.Command.privilege_add(priv_name) + for perm_name in request.param: + # add to privilege for user + api.Command.privilege_add_permission(priv_name, permission=perm_name) + api.Command.role_add(role_name) + api.Command.role_add_privilege(role_name, privilege=priv_name) + api.Command.role_add_member(role_name, user=CERTMAP_USER) + + def finalize(): + try: + api.Command.user_del(CERTMAP_USER) + except Exception: + pass + try: + api.Command.role_del(role_name) + except Exception: + pass + try: + api.Command.privilege_del(priv_name) + except Exception: + pass + + request.addfinalizer(finalize) + + return request.param + + +class TestPermission(XMLRPC_test): + def test_create(self, certmap_rule, certmap_user_permissions): + certmap_rule.ensure_missing() + + with execute_with_expected( + CERTMAP_USER, + CERTMAP_PASSWD, + certmap_user_permissions, + [ + (CREATE_PERM, errors.ACIError, None,), + (READ_PERM, errors.NotFound, None,), + ], + ): + certmap_rule.create() + + # Tracker sets 'exists' to True even when the create does not + # succeed so ensure_missing wouldn't be reliable here + try: + certmap_rule.delete() + except Exception: + pass + + def test_retrieve(self, certmap_rule, certmap_user_permissions): + certmap_rule.ensure_exists() + + with execute_with_expected( + CERTMAP_USER, + CERTMAP_PASSWD, + certmap_user_permissions, + [ + (READ_PERM, errors.NotFound, None,), + ], + ): + certmap_rule.retrieve() + + def test_find(self, certmap_rule, certmap_user_permissions): + certmap_rule.ensure_exists() + + expected_without_read = { + u'count': 0, + u'result': (), + u'summary': u'0 Certificate Identity Mapping Rules matched', + u'truncated': False, + } + expected_ok = { + u'count': 1, + u'result': [{ + k: (v,) for k, v in certmaprule_create_params.items() + }], + u'summary': u'1 Certificate Identity Mapping Rule matched', + u'truncated': False, + } + expected_ok[u'result'][0][u'dn'] = DN( + (u'cn', expected_ok[u'result'][0][u'cn'][0]), + api.env.container_certmaprules, + api.env.basedn, + ) + expected_ok[u'result'][0][u'ipaenabledflag'] = (u'TRUE',) + with execute_with_expected( + CERTMAP_USER, + CERTMAP_PASSWD, + certmap_user_permissions, + [ + (READ_PERM, None, expected_without_read,), + ], + expected_ok, + ): + find = certmap_rule.make_find_command() + find(**{k: v for k, v in certmaprule_create_params.items() + if k is not u'dn'}) + + def test_update(self, certmap_rule, certmap_user_permissions): + certmap_rule.ensure_missing() + certmap_rule.ensure_exists() + + with execute_with_expected( + CERTMAP_USER, + CERTMAP_PASSWD, + certmap_user_permissions, + [ + (READ_PERM, errors.NotFound, None,), + (UPDATE_PERM, errors.ACIError, None,), + ], + ): + certmap_rule.update( + certmaprule_update_params, + {o: [v] for o, v in certmaprule_update_params.items()}, + ) + + def test_delete(self, certmap_rule, certmap_user_permissions): + certmap_rule.ensure_exists() + + with execute_with_expected( + CERTMAP_USER, + CERTMAP_PASSWD, + certmap_user_permissions, + [ + (DELETE_PERM, errors.ACIError, None,), + ], + ): + certmap_rule.delete() + + # Tracker sets 'exists' to False even when the delete does not + # succeed so ensure_missing wouldn't be reliable here + try: + certmap_rule.delete() + except Exception: + pass + + def test_enable(self, certmap_rule, certmap_user_permissions): + certmap_rule.ensure_exists() + certmap_rule.disable() + + with execute_with_expected( + CERTMAP_USER, + CERTMAP_PASSWD, + certmap_user_permissions, + [ + (READ_PERM, errors.NotFound, None,), + (UPDATE_PERM, errors.ACIError, None,), + ], + ): + certmap_rule.enable() + + def test_disable(self, certmap_rule, certmap_user_permissions): + certmap_rule.ensure_exists() + certmap_rule.enable() + + with execute_with_expected( + CERTMAP_USER, + CERTMAP_PASSWD, + certmap_user_permissions, + [ + (READ_PERM, errors.NotFound, None,), + (UPDATE_PERM, errors.ACIError, None,), + ], + ): + certmap_rule.disable() From 9d8477a48357ca6417aa80efdbeaed822f5250c3 Mon Sep 17 00:00:00 2001 From: David Kupka <dku...@redhat.com> Date: Thu, 26 Jan 2017 17:20:23 +0100 Subject: [PATCH 8/9] tests: tracker: Add CertmapconfigTracker to tests certmapconfig-* commands https://fedorahosted.org/freeipa/ticket/6542 --- ipatests/test_xmlrpc/objectclasses.py | 8 ++++ ipatests/test_xmlrpc/tracker/certmap_plugin.py | 66 +++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/ipatests/test_xmlrpc/objectclasses.py b/ipatests/test_xmlrpc/objectclasses.py index 0a15a21..c9ae724 100644 --- a/ipatests/test_xmlrpc/objectclasses.py +++ b/ipatests/test_xmlrpc/objectclasses.py @@ -232,3 +232,11 @@ u'top', u'ipacertmaprule', ] + +certmapconfig = [ + u'top', + u'ipaConfigObject', + u'nsContainer', + u'ipaCertMapContainer', + u'ipaCertMapConfigObject', +] diff --git a/ipatests/test_xmlrpc/tracker/certmap_plugin.py b/ipatests/test_xmlrpc/tracker/certmap_plugin.py index d61be5d..8991e69 100644 --- a/ipatests/test_xmlrpc/tracker/certmap_plugin.py +++ b/ipatests/test_xmlrpc/tracker/certmap_plugin.py @@ -3,8 +3,10 @@ # from ipapython.dn import DN -from ipatests.test_xmlrpc.tracker.base import Tracker, EnableTracker +from ipatests.test_xmlrpc.tracker.base import Tracker +from ipatests.test_xmlrpc.tracker.base import ConfigTracker, EnableTracker from ipatests.test_xmlrpc import objectclasses +from ipatests.test_xmlrpc.xmlrpc_test import fuzzy_string from ipatests.util import assert_deepequal @@ -165,3 +167,65 @@ def check_disable(self, result): ), result ) + + +class CertmapconfigTracker(ConfigTracker): + retrieve_keys = { + u'dn', + u'ipacertmappromptusername', + } + + retrieve_all_keys = retrieve_keys | { + u'cn', + u'objectclass', + u'aci', + u'ipacertmapversion', + u'ipaconfigstring' + } + update_keys = retrieve_keys - {u'dn'} + singlevalue_keys = {u'ipacertmappromptusername'} + + def __init__(self, default_version=None): + super(CertmapconfigTracker, self).__init__( + default_version=default_version) + + self.attrs = { + u'dn': DN(self.api.env.container_certmap, self.api.env.basedn), + u'cn': [self.api.env.container_certmap[0].value], + u'objectclass': objectclasses.certmapconfig, + u'ipacertmapversion': [u'1'], + u'ipaconfigstring': [u'CertMapVersion 1'], + u'aci': [fuzzy_string], + u'ipacertmappromptusername': self.api.Command.certmapconfig_show( + )[u'result'][u'ipacertmappromptusername'] + } + + def make_update_command(self, updates): + return self.make_command('certmapconfig_mod', **updates) + + def check_update(self, result, extra_keys=()): + assert_deepequal( + dict( + value=None, + summary=None, + result=self.filter_attrs(self.update_keys | set(extra_keys)), + ), + result + ) + + def make_retrieve_command(self, all=False, raw=False): + return self.make_command('certmapconfig_show', all=all, raw=raw) + + def check_retrieve(self, result, all=False, raw=False): + if all: + expected = self.filter_attrs(self.retrieve_all_keys) + else: + expected = self.filter_attrs(self.retrieve_keys) + assert_deepequal( + dict( + value=None, + summary=None, + result=expected, + ), + result + ) From a10ef1e14dab5d077522ba7800402aa5829fc225 Mon Sep 17 00:00:00 2001 From: David Kupka <dku...@redhat.com> Date: Thu, 26 Jan 2017 17:40:42 +0100 Subject: [PATCH 9/9] tests: certmap: Add test for certmapconfig-{mod,show} https://fedorahosted.org/freeipa/ticket/6542 --- ipatests/test_xmlrpc/test_certmap_plugin.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/ipatests/test_xmlrpc/test_certmap_plugin.py b/ipatests/test_xmlrpc/test_certmap_plugin.py index cdf667c..7756415 100644 --- a/ipatests/test_xmlrpc/test_certmap_plugin.py +++ b/ipatests/test_xmlrpc/test_certmap_plugin.py @@ -10,7 +10,8 @@ from ipalib import api, errors from ipapython.dn import DN from ipatests.test_xmlrpc.xmlrpc_test import XMLRPC_test -from ipatests.test_xmlrpc.tracker.certmap_plugin import CertmapruleTracker +from ipatests.test_xmlrpc.tracker.certmap_plugin import (CertmapruleTracker, + CertmapconfigTracker) from ipatests.util import assert_deepequal from ipatests.util import change_principal, unlock_principal_password @@ -46,6 +47,8 @@ 'ipacertmappriority', ) +certmapconfig_update_params = {u'ipacertmappromptusername': u'TRUE'} + CREATE_PERM = u'System: Add Certmap Rules' READ_PERM = u'System: Read Certmap Rules' UPDATE_PERM = u'System: Modify Certmap Rules' @@ -78,6 +81,12 @@ def certmap_rule(request): return tracker.make_fixture(request) +@pytest.fixture(scope='class') +def certmap_config(request): + tracker = CertmapconfigTracker() + return tracker.make_fixture(request) + + class TestCRUD(XMLRPC_test): @pytest.mark.parametrize( 'dont_fill', @@ -129,6 +138,17 @@ def test_enable(self, certmap_rule): certmap_rule.enable() +class TestConfig(XMLRPC_test): + def test_config_mod(self, certmap_config): + certmap_config.update( + certmapconfig_update_params, + {k: [v] for k, v in certmapconfig_update_params.items()} + ) + + def test_config_show(self, certmap_config): + certmap_config.retrieve() + + @contextmanager def execute_with_expected(user, password, perms, exps, ok_expected=None): """
-- Manage your subscription for the Freeipa-devel mailing list: https://www.redhat.com/mailman/listinfo/freeipa-devel Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code