-
Notifications
You must be signed in to change notification settings - Fork 317
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Per domain authorization for certificate issuance (#3889)
* Domain authorization - cert issuance * doc update * doc update * typo * docs syntax * typo * update return type * comments Co-authored-by: sayali <[email protected]>
- Loading branch information
Showing
10 changed files
with
261 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1025,6 +1025,23 @@ For more information about how to use social logins, see: `Satellizer <https://g | |
|
||
USER_MEMBERSHIP_PROVIDER = "<yourmembershippluginslug>" | ||
|
||
Authorization Providers | ||
~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
|
||
If you are not using a custom authorization provider you do not need to configure any of these options | ||
|
||
.. data:: USER_DOMAIN_AUTHORIZATION_PROVIDER | ||
:noindex: | ||
|
||
An optional plugin to perform domain level authorization during certificate issuance. Provide plugin slug here. | ||
Plugin is used to check if caller is authorized to issue a certificate for a given Common Name and Subject Alternative | ||
Name (SAN) of type DNSName. Plugin shall be an implementation of DomainAuthorizationPlugin. | ||
|
||
:: | ||
|
||
USER_DOMAIN_AUTHORIZATION_PROVIDER = "<yourauthorizationpluginslug>" | ||
|
||
Metric Providers | ||
~~~~~~~~~~~~~~~~ | ||
|
||
|
@@ -2015,6 +2032,8 @@ get it added. | |
Want to create your own extension? See :doc:`../developer/plugins/index` to get started. | ||
|
||
|
||
.. _iam_target: | ||
|
||
Identity and Access Management | ||
============================== | ||
|
||
|
@@ -2041,3 +2060,30 @@ These permissions are applied to the user upon login and refreshed on every requ | |
.. seealso:: | ||
|
||
`Flask-Principal <https://pythonhosted.org/Flask-Principal>`_ | ||
|
||
To allow integration with external access/membership management tools that may exist in your organization, lemur offers | ||
below plugins in addition to it's own RBAC implementation. | ||
|
||
Membership Plugin | ||
----------------- | ||
|
||
:Authors: | ||
Sayali Charhate <[email protected]> | ||
:Type: | ||
User Membership | ||
:Description: | ||
Adds support to learn and validate user membership details from an external service. User memberships are used to | ||
create user roles dynamically as described in :ref:`iam_target`. Configure this plugin slug as `USER_MEMBERSHIP_PROVIDER` | ||
|
||
Authorization Plugins | ||
--------------------- | ||
|
||
:Authors: | ||
Sayali Charhate <[email protected]> | ||
:Type: | ||
External Authorization | ||
:Description: | ||
Adds support to implement custom authorization logic that is best suited for your enterprise. Lemur offers `AuthorizationPlugin` | ||
and its extended version `DomainAuthorizationPlugin`. One can implement `DomainAuthorizationPlugin` and configure its | ||
slug as `USER_DOMAIN_AUTHORIZATION_PROVIDER` to check if caller is authorized to issue a certificate for a given Common | ||
Name and Subject Alternative Name (SAN) of type DNSName |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
"""Add column application_name to api_keys table. Null values are allowed. | ||
Revision ID: e2d406ada25c | ||
Revises: 189e5fda5bf8 | ||
Create Date: 2021-11-24 14:48:18.747487 | ||
""" | ||
|
||
# revision identifiers, used by Alembic. | ||
revision = 'e2d406ada25c' | ||
down_revision = '189e5fda5bf8' | ||
|
||
from alembic import op | ||
import sqlalchemy as sa | ||
|
||
|
||
def upgrade(): | ||
op.add_column("api_keys", sa.Column("application_name", sa.String(256), nullable=True)) | ||
|
||
|
||
def downgrade(): | ||
op.drop_column("api_keys", "application_name") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
""" | ||
.. module: lemur.plugins.bases.authorization | ||
:platform: Unix | ||
:copyright: (c) 2021 by Netflix Inc., see AUTHORS for more | ||
:license: Apache, see LICENSE for more details. | ||
.. moduleauthor:: Sayali Charhate <[email protected]> | ||
""" | ||
|
||
from lemur.exceptions import LemurException | ||
from lemur.plugins.base import Plugin | ||
|
||
|
||
class AuthorizationPlugin(Plugin): | ||
""" | ||
This is the base class for authorization providers. Check if the caller is authorized to access a resource. | ||
""" | ||
type = "authorization" | ||
|
||
def is_authorized(self, resource, caller): | ||
raise NotImplementedError | ||
|
||
|
||
class DomainAuthorizationPlugin(AuthorizationPlugin): | ||
""" | ||
This is the base class for domain authorization providers. Check if the caller can issue certificates for a domain. | ||
""" | ||
type = "domain-authorization" | ||
|
||
def is_authorized(self, domain, caller): | ||
raise NotImplementedError | ||
|
||
|
||
class UnauthorizedError(LemurException): | ||
""" | ||
Raised when user is unauthorized to perform an action on the resource | ||
""" | ||
def __init__(self, user, resource, action, details="no additional details"): | ||
self.user = user | ||
self.resource = resource | ||
self.action = action | ||
self.details = details | ||
|
||
def __str__(self): | ||
return repr(f"{self.user} is not authorized to perform {self.action} on {self.resource}: {self.details}") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -825,8 +825,8 @@ def test_create_basic_csr(client): | |
owner="[email protected]", | ||
key_type="RSA2048", | ||
extensions=dict( | ||
names=dict( | ||
sub_alt_names=x509.SubjectAlternativeName( | ||
sub_alt_names=dict( | ||
names=x509.SubjectAlternativeName( | ||
[ | ||
x509.DNSName("test.example.com"), | ||
x509.DNSName("test2.example.com"), | ||
|
@@ -1578,3 +1578,86 @@ def start_server(): | |
daemon.setDaemon(True) # Set as a daemon so it will be killed once the main thread is dead. | ||
daemon.start() | ||
return daemon | ||
|
||
|
||
def mocked_is_authorized_for_domain(name): | ||
domain_in_error = "fail.lemur.com" | ||
if name == domain_in_error: | ||
raise UnauthorizedError(user="dummy_user", resource=domain_in_error, action="issue_certificate", | ||
details="unit test, mocked failure") | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"common_name, extensions, expected_error, authz_check_count", | ||
[ | ||
("fail.lemur.com", None, True, 1), | ||
("fail.lemur.com", dict( | ||
sub_alt_names=dict( | ||
names=x509.SubjectAlternativeName( | ||
[ | ||
x509.DNSName("test.example.com"), | ||
x509.DNSName("test2.example.com"), | ||
] | ||
) | ||
) | ||
), True, 3), # CN is checked after SAN | ||
("test.example.com", dict( | ||
sub_alt_names=dict( | ||
names=x509.SubjectAlternativeName( | ||
[ | ||
x509.DNSName("fail.lemur.com"), | ||
x509.DNSName("test2.example.com"), | ||
] | ||
) | ||
) | ||
), True, 1), | ||
(None, dict( | ||
sub_alt_names=dict( | ||
names=x509.SubjectAlternativeName( | ||
[ | ||
x509.DNSName("fail.lemur.com"), | ||
x509.DNSName("test2.example.com"), | ||
] | ||
) | ||
) | ||
), True, 1), | ||
("pass.lemur.com", None, False, 1), | ||
("pass.lemur.com", dict( | ||
sub_alt_names=dict( | ||
names=x509.SubjectAlternativeName( | ||
[ | ||
x509.DNSName("test.example.com"), | ||
x509.DNSName("test2.example.com"), | ||
] | ||
) | ||
) | ||
), False, 3), | ||
("pass.lemur.com", dict( | ||
sub_alt_names=dict( | ||
names=x509.SubjectAlternativeName( | ||
[ | ||
x509.DNSName("test.example.com"), | ||
x509.DNSName("pass.lemur.com"), | ||
] | ||
) | ||
) | ||
), False, 2), # CN repeated in SAN | ||
], | ||
) | ||
def test_allowed_issuance_for_domain(common_name, extensions, expected_error, authz_check_count): | ||
from lemur.certificates.service import allowed_issuance_for_domain | ||
|
||
with patch( | ||
'lemur.certificates.service.is_authorized_for_domain', side_effect=mocked_is_authorized_for_domain | ||
) as wrapper: | ||
try: | ||
allowed_issuance_for_domain(common_name, extensions) | ||
if expected_error: | ||
assert False, f"UnauthorizedError did not occur, input: CN({common_name}), SAN({extensions})" | ||
except UnauthorizedError as e: | ||
if expected_error: | ||
pass | ||
else: | ||
assert False, f"UnauthorizedError occured, input: CN({common_name}), SAN({extensions})" | ||
|
||
assert wrapper.call_count == authz_check_count |