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
Client-side CSR autogeneration #10
Conversation
| @@ -503,6 +503,7 @@ Requires: python-custodia | |||
| Requires: python-dns >= 1.11.1 | |||
| Requires: python-netifaces >= 0.10.4 | |||
| Requires: pyusb | |||
| Requires: python-jinja2 | |||
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 about BuildRequires? pylint would be failing on the system where python-jinja2 is not installed.
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 had that before, but removed it when I couldn't get pylint to actually complain about the missing import. But maybe that was just because "import-error" is disabled (twice?) in pylintrc. Anyway, added as a fixup commit.
Edit: force pushed to fix conflicts with master
fa15aeb
to
207eb51
Compare
|
As discussed elsewhere, this script generation is a fairly low-level operation; you have to specify the helper and know how to run the script. Most users will probably want a command that just takes in a private key location and a profile and requests the cert for them. In case you'd like to look at it now, I have an implementation of that in a separate branch, which I'll create a pull request for after this one is complete: master...LiptonB:local-cert-build |
|
I've added a commit (Use data_sources option to define which fields are rendered) that simplifies the way we avoid rendering rules whose source data are missing, as discussed here: https://www.redhat.com/archives/freeipa-devel/2016-September/msg00051.html. I prefer this approach to the macros in the original implementation, but I'm leaving it as a separate commit in case you would like to compare them. |
|
Some tests for the CSR generation functionality have been added to the pull request. |
| ) | ||
| ) | ||
|
|
||
| def forward(self, *args, **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 should be execute, the command is not forwarded to the server.
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 used forward because the default run method only calls execute if you're running in the server context. Overriding forward is what other client-only commands like vault-add do. Should I override run instead?
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 forgot to mention that you should also inherit from Local. (Sorry.)
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.
Ah, I had tried that before but abandoned it because the rpc client wasn't connected automatically with Local. But now I see that vault-add connects it explicitly, so I've updated my code to do the same.
| stored mapping rules. | ||
| """) | ||
|
|
||
| CSR_DATA_DIR = '/usr/share/ipa/csr' |
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.
File paths should be defined in ipaplatform.paths.
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.
Done.
|
|
||
| from ipalib import errors | ||
| from ipalib.text import _ | ||
| from ipapython.ipa_log_manager import root_logger |
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.
Do not log into the root logger, use a module-specific logger:
from ipapython.ipa_log_manager import log_mgr
logger = log_mgr.get_logger(__name__)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.
Thank you! I thought this might not be right but I didn't know what the right way was.
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.
Right, this is not used often in our code, but we try to follow common best practices in new code, and the best practice for logging seems to be to use module-specific loggers.
| import collections | ||
| import jinja2 | ||
| import jinja2.ext | ||
| import jinja2.sandbox |
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 order of imports should be:
- standard
- 3rd party
- ipa
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.
Done. It seems to be a convention that six is the last import so I've left it that way. Is that right?
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.
No, six should be imported together with other 3rd party imports.
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, moved six to the proper place.
|
|
||
| try: | ||
| rule = [r for r in ruleset['rules'] | ||
| if r['helper'] == helper][0] |
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 [0] does not look right. Either use the complete list, or fail if there is more than one 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.
There should only be one, so I'll raise an error otherwise.
| except IOError: | ||
| raise errors.CertificateMappingError( | ||
| reason=_('Ruleset %(ruleset)s is configured in profile but' | ||
| ' does not exist.') % {'ruleset': rule_name}) |
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 looks rather like errors.NotFound.
Also, this method has no knowledge of profiles, so how do you know the ruleset is configured in profile?
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.
Good points.
| raise errors.CertificateMappingError( | ||
| reason=_('No transformation in "%(ruleset)s" rule supports' | ||
| ' helper "%(helper)s"') % | ||
| {'ruleset': rule_name, 'helper': helper}) |
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 looks rather like errors.EmptyResult.
| except IOError: | ||
| raise errors.CertificateMappingError( | ||
| reason=_('No certificate mappings are defined for profile' | ||
| ' %(profile_id)s') % {'profile_id': profile_id}) |
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 also looks rather like errors.NotFound.
| @@ -1697,6 +1697,15 @@ class CertificateFormatError(CertificateError): | |||
| format = _('Certificate format error: %(error)s') | |||
|
|
|||
|
|
|||
| class CertificateMappingError(CertificateError): | |||
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.
Could we do without adding yet another god exception? See also my other exception related comments.
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're right, the meaning of this exception drifted a lot. After making the changes you suggest above, the remaining cases are:
- A mapping rule contains more than one stanza that matches the given helper. This is very close to SingleMatchExpected, but the format string for that exception doesn't really make sense for this case.
- The jinja2 render failed (probably means the template was written incorrectly). I can't find any errors that seem appropriate for this at all.
- Nothing was generated for a rule that's marked as "required". RequirementError looks almost right, but the error message ("ERROR: 'syntaxSubject' is required") could be confusing. Admittedly, the real problem with this error message isn't just that it's generic but that it doesn't tell you what the actual missing data are, and that's trickier to solve.
Do you have any advice for what to do about these remaining cases?
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 subclass
SingleMatchExpectedwith your own format string. - I would use a new
ExecutionErrorsubclass for this. RequirementErroris not a good choice, as it is a validation error, but what you are describing is an execution error.
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 do you think of this version (0e999cf)? I subclassed SingleMatchExpected for the case of multiple matches for the helper, and merged the other two into CSRTemplateError with different descriptions.
|
In addition to my inline comments above:
|
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.
Thanks for the comments! I've fixed the simple ones and replied to the rest. Regarding your comments about file organization:
- I quite agree that certmapping isn't a good name for what this turned out to be. With the convention of naming modules after the objects they model, perhaps a good name would be
certrequestorcsr? The command could be renamed to something likecertrequest-get-data(orcertrequest-get-script). - Just to confirm, you're suggesting just moving these classes to the
ipaclient.plugins.<whatever we end up calling it>module? - Seems reasonable, I've moved it into the ipalib module for now. It will go wherever the contents of that module end up.
Logistical stuff:
- Now that this is under review I won't add any more content. Are you ok with the two commits about testing being part of this review or should I remove them?
- If you run rebase --autosquash with the latest commit it doesn't actually apply cleanly, but I'm trying not to change history while it's being reviewed, so I'll do the rebase later on if that's ok?
| ) | ||
| ) | ||
|
|
||
| def forward(self, *args, **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 used forward because the default run method only calls execute if you're running in the server context. Overriding forward is what other client-only commands like vault-add do. Should I override run instead?
| import collections | ||
| import jinja2 | ||
| import jinja2.ext | ||
| import jinja2.sandbox |
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.
Done. It seems to be a convention that six is the last import so I've left it that way. Is that right?
|
|
||
| from ipalib import errors | ||
| from ipalib.text import _ | ||
| from ipapython.ipa_log_manager import root_logger |
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.
Thank you! I thought this might not be right but I didn't know what the right way was.
| stored mapping rules. | ||
| """) | ||
|
|
||
| CSR_DATA_DIR = '/usr/share/ipa/csr' |
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.
Done.
| except IOError: | ||
| raise errors.CertificateMappingError( | ||
| reason=_('Ruleset %(ruleset)s is configured in profile but' | ||
| ' does not exist.') % {'ruleset': rule_name}) |
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.
Good points.
|
|
||
| try: | ||
| rule = [r for r in ruleset['rules'] | ||
| if r['helper'] == helper][0] |
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 should only be one, so I'll raise an error otherwise.
| @@ -1697,6 +1697,15 @@ class CertificateFormatError(CertificateError): | |||
| format = _('Certificate format error: %(error)s') | |||
|
|
|||
|
|
|||
| class CertificateMappingError(CertificateError): | |||
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're right, the meaning of this exception drifted a lot. After making the changes you suggest above, the remaining cases are:
- A mapping rule contains more than one stanza that matches the given helper. This is very close to SingleMatchExpected, but the format string for that exception doesn't really make sense for this case.
- The jinja2 render failed (probably means the template was written incorrectly). I can't find any errors that seem appropriate for this at all.
- Nothing was generated for a rule that's marked as "required". RequirementError looks almost right, but the error message ("ERROR: 'syntaxSubject' is required") could be confusing. Admittedly, the real problem with this error message isn't just that it's generic but that it doesn't tell you what the actual missing data are, and that's trickier to solve.
Do you have any advice for what to do about these remaining cases?
Logistical stuff:
|
05ce914
to
b1e0968
Compare
|
FYI: I force pushed a few minutes after adding these commits to fix a pep8 error. |
|
@jcholast, when you get a chance, could you take another look at this and let me know what else is needed? |
| %dir %{_usr}/share/ipa/csr/profiles | ||
| %{_usr}/share/ipa/csr/profiles/*.json | ||
| %dir %{_usr}/share/ipa/csr/rules | ||
| %{_usr}/share/ipa/csr/rules/*.json |
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 we call the feature csrgen, I guess we should put the files into /usr/share/ipa/csrgen rather than /usr/share/ipa/csr.
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.
Ah, true. Done.
|
@LiptonB, I have added some inline comments. Also, have you seen my latest reply in the design thread? |
046c6b5
to
f03a945
Compare
|
Thanks, I've updated the code based on your comments (force pushed to fix conflicts with master). And thanks for pointing out that email! I don't know how I missed it, but I will respond shortly. |
In the dogtag-ipa-ca-renew-agent-submit certmonger renewal helper, we currently use our hand-rolled PKCS freeipa#10 pyasn1 specification to parse the friendlyName out of CSRs generated by certmonger (it contains the NSSDB nickname of the cert). Use other information from the renewal helper process environment to determine the nickname and remove our PKCS freeipa#10 pyasn1 spec. Part of: https://fedorahosted.org/freeipa/ticket/6398
In the dogtag-ipa-ca-renew-agent-submit certmonger renewal helper, we currently use our hand-rolled PKCS freeipa#10 pyasn1 specification to parse the friendlyName out of CSRs generated by certmonger (it contains the NSSDB nickname of the cert). Use other information from the renewal helper process environment to determine the nickname and remove our PKCS freeipa#10 pyasn1 spec. Part of: https://fedorahosted.org/freeipa/ticket/6398
In the dogtag-ipa-ca-renew-agent-submit certmonger renewal helper, we currently use our hand-rolled PKCS freeipa#10 pyasn1 specification to parse the friendlyName out of CSRs generated by certmonger (it contains the NSSDB nickname of the cert). Use other information from the renewal helper process environment to determine the nickname and remove our PKCS freeipa#10 pyasn1 spec. Part of: https://fedorahosted.org/freeipa/ticket/6398
f03a945
to
054179c
Compare
|
Updated to fix conflicts with master again. I'm not sure what's up with Travis, it seems to be checking out PR #109 instead of this one for the pep8 check: |
In the dogtag-ipa-ca-renew-agent-submit certmonger renewal helper, we currently use our hand-rolled PKCS freeipa#10 pyasn1 specification to parse the friendlyName out of CSRs generated by certmonger (it contains the NSSDB nickname of the cert). Use other information from the renewal helper process environment to determine the nickname and remove our PKCS freeipa#10 pyasn1 spec. Part of: https://fedorahosted.org/freeipa/ticket/6398
In the dogtag-ipa-ca-renew-agent-submit certmonger renewal helper, we currently use our hand-rolled PKCS freeipa#10 pyasn1 specification to parse the friendlyName out of CSRs generated by certmonger (it contains the NSSDB nickname of the cert). Use other information from the renewal helper process environment to determine the nickname and remove our PKCS freeipa#10 pyasn1 spec. Part of: https://fedorahosted.org/freeipa/ticket/6398
In the dogtag-ipa-ca-renew-agent-submit certmonger renewal helper, we currently use our hand-rolled PKCS freeipa#10 pyasn1 specification to parse the friendlyName out of CSRs generated by certmonger (it contains the NSSDB nickname of the cert). Use other information from the renewal helper process environment to determine the nickname and remove our PKCS freeipa#10 pyasn1 spec. Part of: https://fedorahosted.org/freeipa/ticket/6398
In the dogtag-ipa-ca-renew-agent-submit certmonger renewal helper, we currently use our hand-rolled PKCS freeipa#10 pyasn1 specification to parse the friendlyName out of CSRs generated by certmonger (it contains the NSSDB nickname of the cert). Use other information from the renewal helper process environment to determine the nickname and remove our PKCS freeipa#10 pyasn1 spec. Part of: https://fedorahosted.org/freeipa/ticket/6398
In the dogtag-ipa-ca-renew-agent-submit certmonger renewal helper, we currently use our hand-rolled PKCS #10 pyasn1 specification to parse the friendlyName out of CSRs generated by certmonger (it contains the NSSDB nickname of the cert). Use other information from the renewal helper process environment to determine the nickname and remove our PKCS #10 pyasn1 spec. Part of: https://fedorahosted.org/freeipa/ticket/6398 Reviewed-By: Jan Cholasta <jcholast@redhat.com> Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
Adds a library that uses jinja2 to format a script that, when run, will build a CSR. Also adds a CLI command, 'cert-get-requestdata', that uses this library and builds the script for a given principal. The rules are read from json files in /usr/share/ipa/csr, but the rule provider is a separate class so that it can be replaced easily. https://fedorahosted.org/freeipa/ticket/4899
This removes the ipa.syntaxrule and ipa.datarule macros in favor of simple 'if' statements based on the data referenced in the rules. The 'if' statement for a syntax rule is generated based on the data rules it contains. The Subject DN should not be generated unless all data rules are in place, so the ability to override the logical operator that combines data_sources (from 'or' to 'and') is added.
Some tests are failing and will be fixed in the next commit. This patch also contains some code changes to make the code easier to test.
054179c
to
fd40f55
Compare
Adds a library that builds scripts that builds CSRs. Adds a CLI command, 'cert-get-requestdata', that uses this library and builds the script for a given principal. Adds rules for the caIPAserviceCert profile, as well as a new userCert profile, stored in json files in /usr/share/ipa/csr. The rule provider is a separate class so that it can be replaced easily if we ever want to move rules to the server side.
https://fedorahosted.org/freeipa/ticket/4899