New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support for Certificate Identity Mapping #398
Conversation
install/share/73certmap.ldif
Outdated
| @@ -0,0 +1,17 @@ | |||
| ## IPA Base OID: | |||
| ## | |||
| ## Attributes: 2.16.840.1.113730.3.8.22.x | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those OIDs are not registered, please register them
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, my plan was to wait for final agreement on the PR before registering the OIDs.
ipaserver/plugins/baseuser.py
Outdated
| @@ -18,15 +18,17 @@ | |||
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
|
|
|||
| import six | |||
| from ldap.dn import dn2str, str2dn | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be possible to use ipapython.dn.DN instead of importing ldap.dn ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Respectively implement reverse() to ipapython.dn.DN if it is not already there
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will implement reverse() in ipapython.dn.DN and add unit tests.
ipaserver/plugins/baseuser.py
Outdated
| times, or in conjunction with --subject --issuer. | ||
| """ | ||
| data = [] | ||
| if 'ipacertmapdata' in options: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be shorter:
for item in options.get('ipacertmapdata', ()):
data.append(item)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree
ipaserver/plugins/baseuser.py
Outdated
| subject = options.get('subject') | ||
| data.append(_build_mapdata(subject, issuer)) | ||
|
|
||
| if 'usercertificate' in options: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can use get(key, default) too here to save one level of indentation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree
ipaserver/plugins/baseuser.py
Outdated
| # --certificate | ||
| # --data | ||
| # Check that at least one of the 3 formats is used | ||
| if 'issuer' not in options and \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is ugly :)
more pythonic way is to use all()
# from top of my head untested
if all(key not in options for key in ('issuer', subject', ....)):
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree
ipaserver/plugins/baseuser.py
Outdated
|
|
||
| def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, | ||
| **options): | ||
| if 'issuer' not in options and \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all() can be use here too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree
| 'default_privileges': { | ||
| 'Certificate Identity Mapping Administrators'}, | ||
| }, | ||
| 'System: Read Certmap Rules': { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a question related to ACI, if System: Read Certmap Configuration is allowed for ldap://all, shouldn't be this System: Read Certmap Rules accessible to ldap://all? What/who is the consumer of this rules? SSSD?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right. SSSD will be the consumer of the rules and there's no point in hiding the rules.
| @@ -366,6 +368,13 @@ class user(baseuser): | |||
| }, | |||
| 'default_privileges': {'PassSync Service'}, | |||
| }, | |||
| 'System: Manage User Certificate Mappings': { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO the similar permission should be added to stageuser implementation, something like System: Manage Stage User Certificate Mappings
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The permissions for Users are finer-grained than the permissions for stage users. For instance, for users, there is "System: Modify Users" (which does not grant write on usercertificate) and "System: Manage User Certificates".
For stage user, there is only "System: Modify stage User" which applies to all the attributes.
I applied the same strategy (finer-grained for users) but if it is a mistake I can create a similar permission for cert mapping on stage users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok
|
I put some inline commets, @flo-renaud if you don't know where to register OIDs feel free to ping me |
ipaserver/plugins/baseuser.py
Outdated
| Converts a (ipa) DN into a string representation following X500 order | ||
| """ | ||
| if name: | ||
| name.reverse() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm always scared by in-place methods. Do you count with that the object where the original name reference was pointing to, changed also outside the _convert_to_x500 method?
maybe we should rather implement get_reverse() method that returns a new DN and keeps the original reference untouched
ipapython/dn.py
Outdated
| ''' | ||
| reverse_dn = DN(self) | ||
| reverse_dn.rdns.reverse() | ||
| return reverse_dn |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no such thing as reversed DN - it's only the the textural representation which might be in both directions, but the underlying DN is the same. I would prefer if you instead added two new methods ldap_text() and x500_text() which return textual representation of the DN in the LDAP and X.500 order, respectively, and use them as appropriate:
def ldap_text(self):
return dn2str([[self.to_openldap()]])
def x500_text(self):
return dn2str([[reversed(self.to_openldap()]])
def __str__(self):
return self.ldap_text()
ipaserver/plugins/baseuser.py
Outdated
| @@ -360,6 +362,13 @@ class baseuser(LDAPObject): | |||
| label=_('Certificate'), | |||
| doc=_('Base-64 encoded user certificate'), | |||
| ), | |||
| Str( | |||
| 'ipacertmapdata*', | |||
| cli_name='data', | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CLI name should be certmapdata, data is far too generic and disconnected from certificate mapping.
ipaserver/plugins/baseuser.py
Outdated
| subject = DN(cert.subject) | ||
| data.append(_build_mapdata(subject, issuer)) | ||
|
|
||
| return data |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of a bunch of global definitions used in both baseuser_add_certmap and baseuser_remove_certmap, create a base class (e.g. ModCertMapData) which baseuser_add_certmap and baseuser_remove_certmap inherit from and encapsulate the common stuff inside it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By "common stuff" I meant certmap_options and _convert_options_to_certmap as well.
ipaserver/plugins/baseuser.py
Outdated
| return data | ||
|
|
||
|
|
||
| class baseuser_add_certmap(LDAPAddAttributeViaOption): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be called baseuser_add_certmapdata, because the attribute is called certmapdata (actually it is called data now, but see my comment above). Ditto for baseuser_remove_certmap.
ipaserver/plugins/baseuser.py
Outdated
| # 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']: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think option.name == 'ipacertmapdata' would be preferrable.
ipaserver/plugins/baseuser.py
Outdated
| 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'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overwriting the objectclass is wrong and won't work, you should append ipacertmapobject to the existing value. Or, you could use baseldap.add_missing_object_class() which does all of this for you.
ipaserver/plugins/baseuser.py
Outdated
| if 'ipacertmapobject' not in objclasses_lc: | ||
| entry_attrs['objectclass'] = ['ipacertmapobject'] | ||
|
|
||
| entry_attrs[self.attribute] = _convert_options_to_certmap(options) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO it would be better to implement this using default_from on the ipacertmapdata, issuer and subject parameters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I initially considered using default_from, but it would be able to build ipacertmapdata only if both issuer and subject parameters are present (when one key is missing, it does not get called). The ipacertmapdata value can also be extracted from the certificate parameter, and I did not find any method in the framework allowing to take input either from (subject+issuer) or certificate. Would you have any suggestion?
ipaserver/plugins/certmap.py
Outdated
| ' authentication'), | ||
| ), | ||
| Str( | ||
| 'associateddomain*', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be a DNSNameParam.
5a6a88d
to
8e9eeb0
Compare
|
Hi @HonzaCholasta, |
|
@flo-renaud, nevermind the One more issue, I think the |
e8a0293
to
f26952b
Compare
|
Hi @HonzaCholasta |
| default:objectClass: groupofnames | ||
| default:objectClass: nestedgroup | ||
| default:cn: Certificate Identity Mapping Administrators | ||
| default:description: Certificate Identity Mapping Administrators |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that we usually put new privileges into 40-delegation.update. I'm not saying it's wrong to have it here, just curious why have you done it this way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @HonzaCholasta
I simply had a look at install/updates/60-trusts.update and did the same, i.e. define new containers and privileges in the same file.
I will keep your note for future reference though.
ipaserver/plugins/baseuser.py
Outdated
| cli_name='issuer', | ||
| label=_('Issuer'), | ||
| doc=_('Issuer of the certificate'), | ||
| flags=['virtual_attribute', 'no_create', 'no_update'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that the no_create and no_update flags are meaningless outside of object plugin params.
ipaserver/plugins/baseuser.py
Outdated
| return u'X509:{issuer}{subject}'.format( | ||
| issuer='<I>{}'.format(issuer.x500_text()) if issuer else '', | ||
| subject='<S>{}'.format(subject.x500_text()) if subject else '', | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just:
return u'X509:<I>{issuer}<S>{subject}'.format(issuer=issuer, subject=subject)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi,
issuer or subject can be None. If for instance issuer=None we should return X509:<S>cn=subject but not X509:<I>None<S>cn=subject
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When they can be None? I don't see when this can be useful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @HonzaCholasta
for instance if the user calls ipa user-add-certmapdata LOGIN --subject cn=subject then issuer=None.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get that, but what is the use case? The subject name is meaningless without context, and the issuer name provides that context. The only use case I can think of is that there is only a single CA (forever), so the issuer name is implied, but I believe that's a corner case which does not need to have special support outside of the generic ipa user-add-certmapdata LOGIN VALUE.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@HonzaCholasta
I'm afraid I disagree here. AD supports both (see Map a Certificate to a User Account) and it is easy for us to provide a function that supports only issuer / only subject / issuer+subject, so why make it more difficult for the administrator? It is more user-friendly to use the issuer or subject options rather than specify the format X509:....
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because it has additional costs for us (in maintenance, testing, ...) for very questionable benefit. Nobody requested it and I seriously doubt anybody would. What does it matter if it's user friendly when there's no user to use it that way?
AD does not support issuer-only AFAICT as it is non-sensical, but it supports other variants which this PR does not implement, so clearly supporting everything AD supports is not the goal here, and I don't think it ever was in the first place.
ipaserver/plugins/baseuser.py
Outdated
| # certificate | ||
| if len(keys) < 2: | ||
| valid_options = ('issuer', 'subject', 'usercertificate') | ||
| if all(key not in options for key in valid_options): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure just checking for presence of the options is not good enough, you should also check that none of the options are None. IMO the easiest way to achieve this would be to add this at the beginning of pre_callback:
try:
certmapdatas = args[1] or []
except IndexError:
certmapdatas = []
issuer = options.get('issuer')
subject = options.get('subject')
certificates = options.get('usercertificate') or []and work with these variables further in the code.
ipaserver/plugins/baseuser.py
Outdated
| if 'issuer' in options or 'subject' in options: | ||
| issuer = options.get('issuer') | ||
| subject = options.get('subject') | ||
| data.append(ModCertMapData._build_mapdata(subject, issuer)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO it would be better to declare _convert_options_to_certmap() as class method, so you can cls._build_mapdata() instead of ModCertMapData._build_mapdata().
ipaserver/plugins/baseuser.py
Outdated
|
|
||
| data = entry_attrs.get(self.attribute, list()) | ||
| entry_attrs[self.attribute] = self._convert_options_to_certmap( | ||
| data, options) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO it would be better to hide all this inside _convert_options_to_certmap() and have it called like this here:
self._convert_options_to_certmap(entry_attrs, options)
ipaserver/plugins/baseuser.py
Outdated
| ), | ||
| Bytes( | ||
| 'usercertificate*', validate_certificate, | ||
| cli_name='certificate', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, can we name the param certificate instead of usercertificate? It does not refer to the LDAP attribute after all. Speaking of that, it should have the virtual_attribute flag.
| __doc__ = _('Modify a Certificate Identity Mapping Rule.') | ||
|
|
||
| msg_summary = _('Modified Certificate Identity Mapping Rule "%(value)s"') | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should validate that associateddomain is either the IPA domain or one of the trusted domains in certmaprule_add and certmaprule_mod.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@HonzaCholasta
I discussed this point offline with Sumit and his answer was that any domain name should be accepted. If SSSD cannot make sense of one of the domains it will ignore it and log a message
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't say I agree, IMHO it does not make sense to allow rules for domains not related to IPA in any way. Also, with validation, we would be able to catch typos.
|
@HonzaCholasta |
|
PR updated with the check on domain in certmaprule-add/mod. |
install/updates/73-certmap.update
Outdated
| default:objectclass: ipaCertMapConfigObject | ||
| default:cn: certmap | ||
| default:ipaconfigstring: CertMapVersion 1 | ||
| default:ipacertmapversion: 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the point of the version? Why is it specified using both ipaConfigString and ipaCertMapVersion?
install/updates/73-certmap.update
Outdated
| default:objectclass: nsContainer | ||
| default:objectclass: ipaConfigObject | ||
| default:objectclass: ipaCertMapContainer | ||
| default:objectclass: ipaCertMapConfigObject |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can't have 2 structural object classes (nsContainer and ipaCertMapConfigObject) in an entry. RFC 4512, section 2.4.2 says:
An object or alias entry is characterized by precisely one structural object class superclass chain which has a single structural object class as the most subordinate object class. This structural object class is referred to as the structural object class of the entry.
I would define ipaCertMapConfigObject as auxiliary to fix this, because IMO all containers should have structural object class nsContainer.
ipaserver/plugins/baseuser.py
Outdated
| if not certmapdatas: | ||
| valid_options = (issuer, subject, certificates) | ||
| if all(not option for option in valid_options): | ||
| raise errors.RequirementError(name='CERTMAPDATA') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick, but IMO this would be a little bit more readable:
if not certmapdatas and not issuer and not subject and not certificates:
raise errors.RequirementError(name='ipacertmapdata')(BTW you should use the name of the param in RequirementError, like above.)
ipaserver/plugins/baseuser.py
Outdated
| if 'ipacertmapdata' not in attrs_list: | ||
| attrs_list.append('ipacertmapdata') | ||
|
|
||
| self._convert_options_to_certmap(entry_attrs, options) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick, but I think it would be better to pass certmapdatas, issuer, subject and certificates as arguments rather than options, so you can avoid the redundant options.get() etc. in _convert_options_to_certmap().
ipaserver/plugins/certmap.py
Outdated
| trust_suffix_namespace = set() | ||
| trust_suffix_namespace.add(api_inst.env.domain.lower()) | ||
|
|
||
| trust_objects = api_inst.Command.trust_find(u'', sizelimit=0)['result'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the empty string necessary? It shouldn't be.
ipaserver/plugins/certmap.py
Outdated
| def get_dn(self, *keys, **options): | ||
| rulename = keys[-1] | ||
| dn = super(certmaprule, self).get_dn(rulename, **options) | ||
| return dn |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this method does effectively nothing. What is its purpose?
| flags=['virtual_attribute'] | ||
| ), | ||
| Bytes( | ||
| 'certificate*', validate_certificate, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Outside of basic certificate validation, we should also validate that the certificate has non-empty subject, otherwise bad certmap data would be generated. This can be fixed later though.
install/updates/73-certmap.update
Outdated
| dn: cn=certmap,cn=ipa,cn=etc,$SUFFIX | ||
| default:objectclass: top | ||
| default:objectclass: nsContainer | ||
| default:objectclass: ipaConfigObject |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that the ipaConfigString attribute is gone, ipaConfigObject is not necessary anymore. This can be fixed later though.
ipaserver/plugins/baseuser.py
Outdated
|
|
||
| @classmethod | ||
| def _convert_options_to_certmap(cls, entry_attrs, issuer=None, | ||
| subject=None, certificates=[]): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please don't use mutable python types as default value in options. Yous should use certificates=()
I know that this is on many places in code but we are trying to get rid of it.
|
LGTM. @flo-renaud, don't forget to register the new OIDs. |
|
It looks like the ACis on the latest version do not allow hosts to access the rules. When I do 'kinit -k' on the IPA server or a client and call I do not get any results. When I call 'kinit admin' and use the same ldapsearch I get my rule returned. Can you confirm this or is my test system broken? |
|
Hi @sumit-bose , Valid starting Expires Service principal |
|
Ok, sorry for the noise, I tested on a fresh install again and now it is working as expected. I guess I shouldn't have tried to update from an older version of your patch to a newer one. |
|
Works for me. |
|
master:
|
See design http://www.freeipa.org/page/V4/Certificate_Identity_Mapping
https://fedorahosted.org/freeipa/ticket/6542