From 395d714685b649695e652643905a2c3a110487c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Tue, 9 Aug 2016 15:10:49 +0200 Subject: [PATCH 01/62] Possum authentication route --- README.md | 4 ++-- conjur/api.py | 5 +++-- conjur/config.py | 28 +++++++--------------------- tests/api_test.py | 8 ++++---- 4 files changed, 16 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 87c078e..9065cb5 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ of all classes and methods. from conjur.config import config # Set the conjur appliance url. This can also be provided -# by the CONJUR_APPLIANCE_URL environment variable. -config.appliance_url = 'https://conjur.example.com/api' +# by the POSSUM_URL environment variable. +config.url = 'https://possum.example' # Set the (PEM) certificate file. This is also configurable with the # CONJUR_CERT_FILE environment variable. diff --git a/conjur/api.py b/conjur/api.py index 2d479fe..4cb1fe2 100644 --- a/conjur/api.py +++ b/conjur/api.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2014 Conjur Inc +# Copyright (C) 2014-2016 Conjur Inc # # Permission is hereby granted, free of charge, to any person obtaining a copy of # this software and associated documentation files (the "Software"), to deal in @@ -84,7 +84,8 @@ def authenticate(self, cached=True): raise ConjurException( "API created without credentials can't authenticate") - url = "%s/users/%s/authenticate" % (self.config.authn_url, + url = "%s/authn/%s/%s/authenticate" % (self.config.url, + self.config.account, urlescape(self.login)) self.token = self._request('post', url, self.api_key).text diff --git a/conjur/config.py b/conjur/config.py index 24f4860..69ebd14 100644 --- a/conjur/config.py +++ b/conjur/config.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2014 Conjur Inc +# Copyright (C) 2014-2016 Conjur Inc # # Permission is hereby granted, free of charge, to any person obtaining a copy of # this software and associated documentation files (the "Software"), to deal in @@ -25,28 +25,14 @@ `conjur.config.config`. This object is used when a `conjur.api.API` instance is created without a config. -Example: - ```python - from conjur.config import config +The most important settings are `url`, `account`, and `cert_file`. - # Set multiple keys using the `update + * `url` points the client to your Possum instance. -The most important settings are `appliance_url`, `account`, and `cert_file`. + * `account` is an organizational name you want to use. This value is required. - * `appliance_url` points the client to your Conjur appliance. Suppose the hostname of - the appliance is `conjur.example.com`. Then the appliance url is `https://conjur.example.com/api`. - Note that the `scheme` **must** be `https`, and the `/api` path is required. - - * `account` is an organizational name chosen when you deploy your conjur appliance. The CLI command - `conjur authn whoami` will show you this value. This value is required. - - * `cert_file` is the path to a `pem` formated certificate used to make a secure connection to the - Conjur appliance. The CLI command `conjur init -h ` is the standard way to install - such a certificate. This option is **required** if you are using a self-signed certificate (which is - the most common use case). - -While endpoints for specific services can also be configured, this is not normally needed -outside of development work. + * `cert_file` is the path to a `pem` formated certificate used to make a secure connection to + Possum. This option is **required** if you are using https with a self-signed certificate. """ import os @@ -136,7 +122,7 @@ def set(self, key, value): account = _setting('account', 'conjur', 'Conjur account identifier') - appliance_url = _setting('appliance_url', None, 'URL for Conjur appliance') + url = _setting('url', None, 'URL for Possum') @property def verify(self): diff --git a/tests/api_test.py b/tests/api_test.py index e827973..0921358 100644 --- a/tests/api_test.py +++ b/tests/api_test.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2014 Conjur Inc +# Copyright (C) 2014-2016 Conjur Inc # # Permission is hereby granted, free of charge, to any person obtaining a copy of # this software and associated documentation files (the "Software"), to deal in @@ -28,14 +28,14 @@ @patch.object(requests, 'post') def test_authenticate(mock_post): - api = conjur.new_from_key("login", "api-key") - api.config.authn_url = "https://example.com" + api = conjur.new_from_key("alice", "api-key") + api.config.url = "http://possum.test" mock_post.return_value = mock_response = Mock() mock_response.status_code = 200 mock_response.text = "token token token" token = api.authenticate() assert token == "token token token" - mock_post.assert_called_with("https://example.com/users/login/authenticate", + mock_post.assert_called_with("http://possum.test/authn/conjur/alice/authenticate", "api-key", verify=api.config.verify) From 028e1ad1a2bea4a015df66b16c2fb1a7becb7438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Wed, 10 Aug 2016 18:09:35 +0200 Subject: [PATCH 02/62] Remove special classes for group, host, layer, user --- conjur/__init__.py | 8 ++--- conjur/api.py | 6 +--- conjur/group.py | 88 --------------------------------------------- conjur/host.py | 85 ------------------------------------------- conjur/layer.py | 66 ---------------------------------- conjur/user.py | 49 ------------------------- tests/group_test.py | 52 --------------------------- tests/user_test.py | 70 ------------------------------------ 8 files changed, 3 insertions(+), 421 deletions(-) delete mode 100644 conjur/group.py delete mode 100644 conjur/host.py delete mode 100644 conjur/layer.py delete mode 100644 conjur/user.py delete mode 100644 tests/group_test.py delete mode 100644 tests/user_test.py diff --git a/conjur/__init__.py b/conjur/__init__.py index 750cd8a..688ddf5 100644 --- a/conjur/__init__.py +++ b/conjur/__init__.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2014 Conjur Inc +# Copyright (C) 2014-2016 Conjur Inc # # Permission is hereby granted, free of charge, to any person obtaining a copy of # this software and associated documentation files (the "Software"), to deal in @@ -21,10 +21,6 @@ from config import Config from api import API -from group import Group -from user import User -from host import Host -from layer import Layer from resource import Resource from role import Role from variable import Variable @@ -117,4 +113,4 @@ def new_from_token(token, configuration=None): __all__ = ( 'config', 'Config', 'Group', 'API', 'User', 'Host', 'Layer', 'Resource', 'Role', 'Variable', 'new_from_key', 'new_from_netrc', 'new_from_token', 'configure', 'ConjurException' -) \ No newline at end of file +) diff --git a/conjur/api.py b/conjur/api.py index 4cb1fe2..b98f6dc 100644 --- a/conjur/api.py +++ b/conjur/api.py @@ -23,11 +23,7 @@ import requests from conjur.variable import Variable -from conjur.user import User from conjur.role import Role -from conjur.group import Group -from conjur.layer import Layer -from conjur.host import Host from conjur.resource import Resource from conjur.util import urlescape from conjur.exceptions import ConjurException @@ -317,7 +313,7 @@ def user(self, login): The user is *not* created by this method, and may in fact not exist. """ - return User(self, login) + return Role(self, 'user', login) def create_user(self, login, password=None): """ diff --git a/conjur/group.py b/conjur/group.py deleted file mode 100644 index 27d3d42..0000000 --- a/conjur/group.py +++ /dev/null @@ -1,88 +0,0 @@ -# -# Copyright (C) 2014 Conjur Inc -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -class Group(object): - """ - Represents a Conjur [group](https://developer.conjur.net/reference/services/directory/group). - - Generally you won't create instances of this class, but use the `conjur.API.group(id)` method. - - A group is a role that contains other roles, typically users and other groups. A `conjur.Group` - object can list its members with the `members` method, and also manage them with the `add_member` - and `remove_member` methods. - """ - def __init__(self, api, id): - self.api = api - """ - Instance of `conjur.API` used to implement Conjur operations. - """ - - self.id = id - """ - Identifier (unqualified) of the group. - """ - - self.role = api.role('group', id) - """ - Represents the `conjur.Role` associated with this group. - """ - - def members(self): - """ - Return a list of members of this group. Members are returned as `dict`s - with the following keys: - - * `'member'` the fully qualified identifier of the group - * `'role'` the fully qualified identifier of the group (redundant) - * `'grantor'` the role that granted the membership - * `'admin_option'` whether this member can grant membership in the group to other roles. - - Example: print member ids (fully qualified) and whether they are admins of the group. - >>> group = api.group('security_admin') - >>> for member in group.members(): - ... print('{} is a member of security_admin ({} admin option)'.format( - ... member['member'], - ... 'with' if member['admin_option'] else 'without' - ... )) - """ - return self.role.members() - - def add_member(self, member, admin=False): - """ - Add a member to this group. - - `member` is the member we want to add to the group, and should be a qualified Conjur id, - or an object with a `role` attribute or a `roleid` method. Examples of such objects - include `conjur.User`, `conjur.Role`, and `conjur.Group`. - - If `admin` is True, the member will be allowed to add other members to this group. - """ - self.role.grant_to(member, admin) - - def remove_member(self, member): - """ - Remove a member from the group. - - `member` is the member to remove, and should be a qualified Conjur id, - or an object with a `role` attribute or a `roleid` method. Examples of such objects - include `conjur.User`, `conjur.Role`, and `conjur.Group`. - """ - self.role.revoke_from(member) diff --git a/conjur/host.py b/conjur/host.py deleted file mode 100644 index 51e308d..0000000 --- a/conjur/host.py +++ /dev/null @@ -1,85 +0,0 @@ -# -# Copyright (C) 2014 Conjur Inc -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -from conjur.util import urlescape -from conjur.exceptions import ConjurException - - -class Host(object): - """ - A Conjur `Host` is a role corresponding to a machine or machine identity. - - The `Host` class provides the ability to check for existence and read attributes of the - host. - - Attributes (such as the `ownerid`) are fetched lazily. - - Newly created hosts, as returned by `conjur.API.create_host`, have an `api_key` attribute, - but existing hosts retrieved with `conjur.API.host` or the constructor of this class *do not* - have one. - - Example: - - >>> # Create a host and save it's api key to a file. - >>> host = api.create_host('jenkins') - >>> api_key = host.api_key - >>> with open('/etc/conjur.identity') as f: - ... f.write(api_key) - - Example: - - >>> # See if a host named `jenkins` exists: - >>> if api.host('jenkins').exists(): - ... print("Host 'jenkins' exists") - ... else: - ... print("Host 'jenkins' does not exist") - - """ - def __init__(self, api, id, attrs=None): - self.api = api - self.id = id - self._attrs = attrs - self.role = self.api.role('host', self.id) - - def exists(self): - """ - Return `True` if this host exists. - """ - status = self.api.get(self._url(), check_errors=False).status_code - if status == 200: - return True - if status == 404: - return False - raise ConjurException("Request Failed: {0}".format(status)) - - def _fetch(self): - self._attrs = self.api.get(self._url()).json() - - def _url(self): - return "{0}/hosts/{1}".format(self.api.config.core_url, - urlescape(self.id)) - - def __getattr__(self, item): - if self._attrs is None: - self._fetch() - try: - return self._attrs[item] - except KeyError: - raise AttributeError(item) diff --git a/conjur/layer.py b/conjur/layer.py deleted file mode 100644 index 4e962e2..0000000 --- a/conjur/layer.py +++ /dev/null @@ -1,66 +0,0 @@ -# -# Copyright (C) 2014 Conjur Inc -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -from conjur.util import urlescape, authzid -from conjur.exceptions import ConjurException - - -class Layer(object): - def __init__(self, api, id, attrs=None): - self.api = api - self.id = id - self._attrs = {} if attrs is None else attrs - - def add_host(self, host): - hostid = authzid(host, 'role', with_account=False) - self.api.post(self._hosts_url(), data={'hostid': hostid}) - - def remove_host(self, host): - hostid = authzid(host, 'role') - self.api.delete(self._host_url(hostid)) - - def exists(self): - resp = self.api.get(self._url(), check_errors=False) - if resp.status_code == 200: - return True - if resp.status_code == 404: - return False - raise ConjurException("Request Failed: {0}".format(resp.status_code)) - - def _url(self): - return "{0}/layers/{1}".format(self.api.config.core_url, - urlescape(self.id)) - - def _hosts_url(self): - return "{0}/hosts".format(self._url()) - - def _host_url(self, host_id): - return "{0}/{1}".format(self._hosts_url(), urlescape(host_id)) - - def _fetch(self): - self._attrs = self.api.get(self._url()).json() - - def __getattr__(self, item): - if self._attrs is None: - self._fetch() - try: - return self._attrs[item] - except KeyError: - raise AttributeError(item) diff --git a/conjur/user.py b/conjur/user.py deleted file mode 100644 index b0cdb42..0000000 --- a/conjur/user.py +++ /dev/null @@ -1,49 +0,0 @@ -# -# Copyright (C) 2014 Conjur Inc -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -from conjur.util import urlescape - - -class User(object): - def __init__(self, api, login, attrs=None): - self.api = api - self.login = login - # support as_role - self.role = api.role('user', login) - self._attrs = attrs - - def exists(self): - resp = self.api.get(self.url(), check_errors=False) - return resp.status_code != 404 - - def __getattr__(self, item): - if self._attrs is None: - self._fetch() - try: - return self._attrs[item] - except KeyError: - raise AttributeError(item) - - def _fetch(self): - self._attrs = self.api.get(self.url()).json() - - def url(self): - return "{0}/users/{1}".format(self.api.config.core_url, - urlescape(self.login)) diff --git a/tests/group_test.py b/tests/group_test.py deleted file mode 100644 index 5f9e1b4..0000000 --- a/tests/group_test.py +++ /dev/null @@ -1,52 +0,0 @@ -# -# Copyright (C) 2014 Conjur Inc -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -from mock import patch -import conjur - -api = conjur.new_from_key('foo', 'bar') - -group = api.group('v1/admins') - - -def test_group(): - assert group.role.kind == 'group' - assert group.role.identifier == 'v1/admins' - assert group.role.roleid == api.config.account + ':group:v1/admins' - - -@patch.object(group.role, 'grant_to') -def test_add_member(mock_grant_to): - member = api.user('foo') - group.add_member(member) - mock_grant_to.assert_called_with(member, False) - - -@patch.object(group.role, 'grant_to') -def test_add_member_admin(mock_grant_to): - member = api.role('something', 'else') - group.add_member(member, True) - mock_grant_to.assert_called_with(member, True) - - -@patch.object(group.role, 'revoke_from') -def test_remove_member(mock_revoke_from): - member = api.user('foo') - group.remove_member(member) - mock_revoke_from.assert_called_with(member) diff --git a/tests/user_test.py b/tests/user_test.py deleted file mode 100644 index 72cd63c..0000000 --- a/tests/user_test.py +++ /dev/null @@ -1,70 +0,0 @@ -# -# Copyright (C) 2014 Conjur Inc -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -from mock import patch, Mock -import requests - -import conjur - - -@patch.object(requests, 'post') -def test_create_user(mock_post): - api = conjur.new_from_token('token') - mock_post.return_value = resp = Mock() - resp.status_code = 200 - resp.json = lambda: {'login': 'foo', 'api_key': 'apikey'} - - user_no_pass = api.create_user('foo') - assert user_no_pass.login == 'foo' - assert user_no_pass.api_key == 'apikey' - mock_post.assert_called_with( - '{0}/users'.format(api.config.core_url), - data={'login': 'foo'}, - headers={'Authorization': api.auth_header()}, - verify=api.config.verify - ) - - api.create_user('foo', 'bar') - mock_post.assert_called_with( - '{0}/users'.format(api.config.core_url), - data={'login': 'foo', 'password': 'bar'}, - headers={'Authorization': api.auth_header()}, - verify=api.config.verify - ) - - -@patch.object(requests, 'get') -def test_user(mock_get): - api = conjur.new_from_token('token') - mock_get.return_value = Mock(status_code=200, json=lambda: {'foo': 'bar'}) - user = api.user('login') - assert user.foo == 'bar' - mock_get.assert_called_with( - '{0}/users/login'.format(api.config.core_url), - headers={'Authorization': api.auth_header()}, - verify=api.config.verify - ) - - -def test_user_role(): - user = conjur.new_from_key('foo', 'bar').user('someone') - role = user.role - assert role.kind == 'user' - assert role.identifier == 'someone' From 3cebc17dffa127a620f7e8df4b538da8773b951b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Wed, 10 Aug 2016 19:03:07 +0200 Subject: [PATCH 03/62] Move secret handling methods to Resource class --- conjur/__init__.py | 1 - conjur/api.py | 1 - conjur/resource.py | 49 +++++++++++++++++++++- conjur/variable.py | 94 ------------------------------------------ tests/resource_test.py | 20 +++++++++ tests/variable_test.py | 71 ------------------------------- 6 files changed, 67 insertions(+), 169 deletions(-) delete mode 100644 conjur/variable.py delete mode 100644 tests/variable_test.py diff --git a/conjur/__init__.py b/conjur/__init__.py index 688ddf5..816e0b6 100644 --- a/conjur/__init__.py +++ b/conjur/__init__.py @@ -23,7 +23,6 @@ from api import API from resource import Resource from role import Role -from variable import Variable from exceptions import ConjurException from config import config diff --git a/conjur/api.py b/conjur/api.py index b98f6dc..d3b88a1 100644 --- a/conjur/api.py +++ b/conjur/api.py @@ -22,7 +22,6 @@ import requests -from conjur.variable import Variable from conjur.role import Role from conjur.resource import Resource from conjur.util import urlescape diff --git a/conjur/resource.py b/conjur/resource.py index da612d8..b2e1138 100644 --- a/conjur/resource.py +++ b/conjur/resource.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2014 Conjur Inc +# Copyright (C) 2014-2016 Conjur Inc # # Permission is hereby granted, free of charge, to any person obtaining a copy of # this software and associated documentation files (the "Software"), to deal inre @@ -28,8 +28,16 @@ class Resource(object): A `Resource` represents an object on which `Role`s can be permitted to perform certain actions. - Generally you will not construct these directly, but call the `conjur.API.role` method + Generally you will not construct these directly, but call the `conjur.API.resource` method to do so. + + Resources can be used to represent secrets; conventionally resources with + `variable` kind are used for this purpose, but this is not enforced. + + In particular, resources of (kind, id) in the form of ('public_key', + 'username/key_id') are customarily used to represent public keys of a + given user, with the keys themselves attached as secrets. + (Note public keys aren't secrets per se, but are stored as such for consistency.) """ def __init__(self, api, kind, identifier): self.api = api @@ -140,3 +148,40 @@ def url(self): self.kind, self.identifier ]) + + def secret(self, version=None): + """ + Retrieve the secret attached to this resource. + + `version` is a *one based* index of the version to be retrieved. + + If no such version exists, a 404 error is raised. + + Returns the value of the secret as a string. + """ + url = self.secret_url() + if version is not None: + url = "%s?version=%s" % (url, version) + return self.api.get(url).text + + def add_secret(self, value): + """ + Stores a new version of the secret in this resource. + + `value` is a string giving the new value to store. + """ + self._attrs = None + data = value + self.api.post(self.secret_url(), data=data) + + def secret_url(self): + """ + Internal method to return a url for the secrets of this object as a string. + """ + return "/".join([ + self.api.config.url, + 'secrets', + self.api.config.account, + self.kind, + self.identifier + ]) diff --git a/conjur/variable.py b/conjur/variable.py deleted file mode 100644 index e45a168..0000000 --- a/conjur/variable.py +++ /dev/null @@ -1,94 +0,0 @@ -# -# Copyright (C) 2014 Conjur Inc -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -from conjur.util import urlescape - - -class Variable(object): - """ - A `Variable` represents a versioned secret stored in Conjur. - - Generally you will get an instance of this class by calling `conjur.API.create_variable` - or `conjur.API.variable`. - - Instances of this class allow you to fetch values of the variable, and store new ones. - - Example: - - >>> # Print the current value of the variable `mysql-password` - >>> variable = api.variable('mysql-password') - >>> print("mysql-password is {}".format(variable.value())) - - Example: - - >>> # Print all versions of the same variable - >>> variable = api.variable('mysql-password') - >>> for i in range(1, variable.version_count + 1): # version numbers are 1 based - ... print("version {} of 'mysql-password' is {}".format(i, variable.value(i))) - - """ - def __init__(self, api, id, attrs=None): - self.id = id - self.api = api - self._attrs = attrs - - def value(self, version=None): - """ - Retrieve the secret stored in a variable. - - `version` is a *one based* index of the version to be retrieved. - - If no such version exists, a 404 error is raised. - - Returns the value of the variable as a string. - """ - url = "%s/variables/%s/value" % (self.api.config.core_url, - urlescape(self.id)) - if version is not None: - url = "%s?version=%s" % (url, version) - return self.api.get(url).text - - def add_value(self, value): - """ - Stores a new version of the secret in this variable. - - `value` is a string giving the new value to store. - - This increments the variable's `version_count` member by one. - """ - self._attrs = None - data = {'value': value} - url = "%s/variables/%s/values" % (self.api.config.core_url, - urlescape(self.id)) - self.api.post(url, data=data) - - def __getattr__(self, item): - if self._attrs is None: - self._fetch() - try: - return self._attrs[item] - except KeyError: - raise AttributeError(item) - - def _fetch(self): - self._attrs = self.api.get( - "{0}/variables/{1}".format(self.api.config.core_url, - urlescape(self.id)) - ).json() diff --git a/tests/resource_test.py b/tests/resource_test.py index 038cb16..affc5dc 100644 --- a/tests/resource_test.py +++ b/tests/resource_test.py @@ -79,3 +79,23 @@ def test_permitted_error_with_role(mock_get): mock_get.return_value = Mock(status_code=401) with pytest.raises(conjur.ConjurException): resource.permitted('fry', bob) + +@patch.object(api, 'get') +def test_get_secret_value(mock_get): + mock_get.return_value = resp = Mock() + resp.status_code = 200 + resp.text = 'teh value' + assert resource.secret() == 'teh value' + mock_get.assert_called_with( + '%s/secrets/conjur/food/bacon' % api.config.url + ) + +@patch.object(api, 'post') +def test_add_secret_value(mock_post): + mock_post.return_value = resp = Mock() + resp.status_code = 201 + resource.add_secret('boo') + mock_post.assert_called_with( + '%s/secrets/conjur/food/bacon' % api.config.url, + data='boo', + ) diff --git a/tests/variable_test.py b/tests/variable_test.py deleted file mode 100644 index 50abd30..0000000 --- a/tests/variable_test.py +++ /dev/null @@ -1,71 +0,0 @@ -# -# Copyright (C) 2014 Conjur Inc -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -from mock import patch, Mock -import requests - -import conjur - - -@patch.object(requests, 'post') -def test_create_variable(mock_post): - mock_post.return_value = mock_response = Mock() - mock_response.status_code = 201 - mock_response.json = lambda: {'id': 'foobar'} # No attribute support now - api = conjur.new_from_token('token') - v = api.create_variable(mime_type='mimey', kind='something') - assert v.id == 'foobar' - mock_post.assert_called_with( - '%s/variables' % api.config.core_url, - data={'mime_type': 'mimey', 'kind': 'something'}, - headers={'Authorization': api.auth_header()}, - verify=api.config.verify - ) - - -@patch.object(requests, 'get') -def test_get_variable_value(mock_get): - mock_get.return_value = resp = Mock() - resp.status_code = 200 - resp.text = 'teh value' - api = conjur.new_from_token('token') - v = api.variable('my-id') - assert v.value() == 'teh value' - mock_get.assert_called_with( - '%s/variables/my-id/value' % api.config.core_url, - headers={'Authorization': api.auth_header()}, - verify=api.config.verify - ) - - -@patch.object(requests, 'post') -def test_add_variable_value(mock_post): - mock_post.return_value = resp = Mock() - resp.status_code = 201 - api = conjur.new_from_token('token') - v = api.variable('var') - v.add_value('boo') - mock_post.assert_called_with( - '%s/variables/var/values' % api.config.core_url, - headers={'Authorization': api.auth_header()}, - data={'value': 'boo'}, - verify=api.config.verify - ) From 094f68313374cf1d50a454bc04ccd425d7e61e56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Thu, 11 Aug 2016 16:27:47 +0200 Subject: [PATCH 04/62] Add password authentication --- conjur/__init__.py | 22 +++++++++++++++++++++- tests/api_test.py | 21 +++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/conjur/__init__.py b/conjur/__init__.py index 816e0b6..6436e51 100644 --- a/conjur/__init__.py +++ b/conjur/__init__.py @@ -18,6 +18,7 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import os +import requests from config import Config from api import API @@ -85,7 +86,7 @@ def new_from_key(login, api_key, configuration=None): `login` is the identity of the Conjur user or host to authenticate as. - `api_key` is the api key *or* password to use when authenticating. + `api_key` is the api key to use when authenticating. `configuration` is a `conjur.Config` instance for the api. If not given the global `Config` instance (`conjur.config`) will be used. @@ -93,6 +94,25 @@ def new_from_key(login, api_key, configuration=None): return API(credentials=(login, api_key), config=_config(configuration)) +def new_from_password(login, password, configuration=None): + """ + Create a `conjur.API` instance that will authenticate immediately (to + exchange the password for the API key) as the identity given by `login` + and `api_key`. + + `login` is the identity of the Conjur user or host to authenticate as. + + `password` is the password to use when authenticating. + + `configuration` is a `conjur.Config` instance for the api. If not given the global + `Config` instance (`conjur.config`) will be used. Note it needs to be + set up correctly before using this function. + """ + configuration = _config(configuration) + url = "%s/authn/%s/login" % (configuration.url, configuration.account) + response = requests.get(url, auth=(login, password), verify=configuration.verify) + api_key = response.text + return new_from_key(login, api_key, configuration) def new_from_token(token, configuration=None): """ diff --git a/tests/api_test.py b/tests/api_test.py index 0921358..ce8dcb1 100644 --- a/tests/api_test.py +++ b/tests/api_test.py @@ -38,6 +38,27 @@ def test_authenticate(mock_post): mock_post.assert_called_with("http://possum.test/authn/conjur/alice/authenticate", "api-key", verify=api.config.verify) +@patch.object(requests, 'post') +@patch.object(requests, 'get') +def test_authenticate_password(mock_get, mock_post): + mock_get.return_value = mock_response = Mock() + mock_response.status_code = 200 + mock_response.text = "api-key" + + mock_post.return_value = mock_response = Mock() + mock_response.status_code = 200 + mock_response.text = "token token token" + + conjur.config.url = "http://possum.test" + api = conjur.new_from_password("alice", "secret password") + token = api.authenticate() + assert token == "token token token" + + mock_get.assert_called_with("http://possum.test/authn/conjur/login", + auth=("alice", "secret password"), + verify=api.config.verify) + mock_post.assert_called_with("http://possum.test/authn/conjur/alice/authenticate", + "api-key", verify=api.config.verify) @patch.object(requests, 'post') def test_authenticate_with_cached_token(mock_post): From f944c0659a6f916596db207f91a1b76e524672f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Thu, 11 Aug 2016 16:30:53 +0200 Subject: [PATCH 05/62] Remove new_from_netrc --- conjur/__init__.py | 26 -------------------------- conjur/api.py | 2 +- 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/conjur/__init__.py b/conjur/__init__.py index 6436e51..023a1b0 100644 --- a/conjur/__init__.py +++ b/conjur/__init__.py @@ -53,32 +53,6 @@ def configure(**kwargs): config.update(**kwargs) return config - -def new_from_netrc(netrc_file=None, configuration=None): - """ - Create a `conjur.API` instance using an identity loaded from netrc. This method - uses the identity stored for the host `config.authn_url`. - - `netrc_file` is an alternative path to the netrc formatted file. Defaults - to ~/.netrc on unixy systems. - - `configuration` is a `conjur.Config` instance used to determine the host - in the netrc file, and also passed to the `conjur.new_from_key` method to - create the API instance using the identity. - """ - import netrc - - configuration = _config(configuration) - auth = netrc.netrc(netrc_file).authenticators(configuration.authn_url) - if auth is None: - raise ValueError("No authenticators found for authn_url '%s' in %s" % ( - configuration.authn_url, - (netrc_file or '~/.netrc') - )) - login, _, api_key = auth - return new_from_key(login, api_key, configuration) - - def new_from_key(login, api_key, configuration=None): """ Create a `conjur.API` instance that will authenticate on demand as the identity given diff --git a/conjur/api.py b/conjur/api.py index d3b88a1..4193b0f 100644 --- a/conjur/api.py +++ b/conjur/api.py @@ -33,7 +33,7 @@ def __init__(self, credentials=None, token=None, config=None): Creates an API instance configured with the given credentials or token and config. - Generally you should use `conjur.new_from_key`, `conjur.new_from_netrc`, + Generally you should use `conjur.new_from_key`, `conjur.new_from_password`, or `conjur.new_from_token` to get an API instance instead of calling this constructor directly. From a9a606d3fa9db2857bca237c868be7355cc0c5cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Thu, 11 Aug 2016 19:19:07 +0200 Subject: [PATCH 06/62] Drop functions referring to removed resources --- conjur/api.py | 108 ----------------------------------------- tests/resource_test.py | 2 +- tests/role_test.py | 2 +- 3 files changed, 2 insertions(+), 110 deletions(-) diff --git a/conjur/api.py b/conjur/api.py index 4193b0f..b656449 100644 --- a/conjur/api.py +++ b/conjur/api.py @@ -221,114 +221,6 @@ def resource(self, kind, identifier): """ return Resource(self, kind, identifier) - def group(self, id): - """ - Return a `conjur.Group` object with the given id. - - This method neither creates nor checks for the groups's existence. - - `id` is the *unqualified* id of the group, and does not include the account or kind. - """ - return Group(self, id) - - def create_group(self, id): - """ - Creates a Conjur Group and returns a `conjur.Group` object representing it. - - `id` is the identifier of the group to create. - """ - - self.post('{0}/groups'.format(self.config.core_url), data={'id': id}) - return Group(self, id) - - def variable(self, id): - """ - Return a `conjur.Variable` object with the given `id`. - - This method neither creates nor checks for the variable's existence. - """ - return Variable(self, id) - - def create_variable(self, id=None, mime_type='text/plain', kind='secret', - value=None): - """ - Creates a Conjur variable. - - Returns a `conjur.Variable` object. - - `id` is an identifier for the new variable. If not given, a unique id will - be generated. - - `mime_type` is a string like `text/plain` indicating the content type stored by the - variable. This determines the Content-Type header of responses returning the variable's value. - - `kind` is a string indicating a user defined role for the variable. - Ignored by Conjur, but useful for making a variable's - purpose. - - `value` is a string assigning an initial value for the variable. - """ - data = {'mime_type': mime_type, 'kind': kind} - if id is not None: - data['id'] = id - if value is not None: - data['value'] = value - - attrs = self.post("%s/variables" % self.config.core_url, data=data).json() - id = id or attrs['id'] - return Variable(self, id, attrs) - - def layer(self, layer_id): - """ - Return a `conjur.Layer` object with the given `layer_id`. - - This method neither creates nor checks for the layer's existence. - """ - return Layer(self, layer_id) - - def host(self, host_id): - """ - Return a `conjur.Host` object with the given `host_id`. - - This method neither creates nor checks for the host's existence. - """ - return Host(self, host_id) - - def create_host(self, host_id): - """ - Creates a Conjur Host and returns a `conjur.Host` object that represents it. - - `host_id` is the id of the Host to be created. The `conjur.Host` object returned by - this method will have an `api_key` attribute, but when the Host is fetched in the future this attribute - is not available. - """ - attrs = self.post("{0}/hosts".format(self.config.core_url), - data={'id': host_id}).json() - return Host(self, host_id, attrs) - - def user(self, login): - """ - Returns an object representing a Conjur user with the given login. - - The user is *not* created by this method, and may in fact not exist. - """ - return Role(self, 'user', login) - - def create_user(self, login, password=None): - """ - Create a Conjur user with the given `login` and password, and returns a `conjur.User` object - representing it. - - If `password` is not given, the user will only be able to authenticate using the generated api_key - attribute of the returned User instance. Note that this `api_key` will not be available when the User - is fetched in the future. - """ - data = {'login': login} - if password is not None: - data['password'] = password - url = "{0}/users".format(self.config.core_url) - return User(self, login, self.post(url, data=data).json()) - def _public_key_url(self, *args): return '/'.join([self.config.pubkeys_url] + [urlescape(arg) for arg in args]) diff --git a/tests/resource_test.py b/tests/resource_test.py index affc5dc..7a3b47f 100644 --- a/tests/resource_test.py +++ b/tests/resource_test.py @@ -24,7 +24,7 @@ api = conjur.new_from_key('admin', 'secret') resource = api.resource('food', 'bacon') -bob = api.user('bob') +bob = api.role('user', 'bob') def test_resource_id(): diff --git a/tests/role_test.py b/tests/role_test.py index 0e1529f..55c2a45 100644 --- a/tests/role_test.py +++ b/tests/role_test.py @@ -96,7 +96,7 @@ def test_role_members(mock_get): @patch.object(api, 'put') def test_role_grant_to_user(mock_put): role = api.role('somekind', 'admins') - user = api.user('somebody') + user = api.role('user', 'somebody') role.grant_to(user) mock_put.assert_called_with( '{0}/the-account/roles/somekind/admins?members&member={1}'.format( From bb3605463d81ccc549dcfeb286ecdf5a13e56a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Thu, 11 Aug 2016 19:30:13 +0200 Subject: [PATCH 07/62] Move public keys function to Role --- conjur/api.py | 62 ------------------------------------ conjur/role.py | 15 +++++++++ tests/pubkeys_test.py | 74 ------------------------------------------- tests/role_test.py | 11 +++++++ 4 files changed, 26 insertions(+), 136 deletions(-) delete mode 100644 tests/pubkeys_test.py diff --git a/conjur/api.py b/conjur/api.py index b656449..8d7d31d 100644 --- a/conjur/api.py +++ b/conjur/api.py @@ -220,65 +220,3 @@ def resource(self, kind, identifier): `api.resource('variable', 'db_password')`. """ return Resource(self, kind, identifier) - - def _public_key_url(self, *args): - return '/'.join([self.config.pubkeys_url] + - [urlescape(arg) for arg in args]) - - def add_public_key(self, username, key): - """ - Upload an openssh formatted public key to be made available for the user - given by `username`. - - The key should be formatted like `ssh-rsa bob@example.com`. - """ - self.post(self._public_key_url(username), data=key) - - def remove_public_key(self, username, keyname): - """ - Remove a specific public key for the user identified by `username`. - The `keyname` argument refers to the name field in the openssh formatted key - to be deleted. - - For example, if they key contents are `ssh-rsa bob@example.com`, - the `keyname` should be `bob@example.com` - """ - self.delete(self._public_key_url(username, keyname)) - - def remove_public_keys(self, username): - """ - Remove all public keys for the user represented by `username`. - """ - for keyname in self.public_key_names(username): - self.remove_public_key(username, keyname) - - def public_keys(self, username): - """ - Returns all keys for the user given by `username`, as a newline delimited string. - - The odd format is chosen to support the Conjur SSH login implementation. - """ - return self.get(self._public_key_url(username)).text - - def public_key(self, username, keyname): - """ - Return the contents of a specific public key given by `keyname`, - for the user given by `username` as a string. - - The name of the key is based on the name entry of the openssh formatted key that was uploaded. - - For example, if they key contents are `ssh-rsa bob@example.com`, - the `keyname` should be `bob@example.com` - """ - return self.get(self._public_key_url(username, keyname)).text - - def public_key_names(self, username): - """ - Return the names of public keys for the user given by `username`. - - The names of the keys are based on the name entry of the openssh formatted key that was uploaded. - - For example, if they key contents are `ssh-rsa bob@example.com`, - the `keyname` should be `bob@example.com` - """ - return [k.split(' ')[-1] for k in self.public_keys(username).split('\n')] diff --git a/conjur/role.py b/conjur/role.py index afec968..e1716a3 100644 --- a/conjur/role.py +++ b/conjur/role.py @@ -178,3 +178,18 @@ def _url(self, *args): 'roles', self.kind, self.identifier] + list(args)) + + def _public_keys_url(self): + return '/'.join([ + self.api.config.url, + 'public_keys', + self.api.config.account, + self.kind, + self.identifier + ]) + + def public_keys(self): + """ + Returns all SSH public keys for this role, if any, as a newline delimited string. + """ + return self.api.get(self._public_keys_url()).text diff --git a/tests/pubkeys_test.py b/tests/pubkeys_test.py deleted file mode 100644 index faabd7f..0000000 --- a/tests/pubkeys_test.py +++ /dev/null @@ -1,74 +0,0 @@ -# -# Copyright (C) 2014 Conjur Inc -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -from mock import patch, Mock, call -import conjur - -api = conjur.new_from_key('fakeid', 'fakepass') - - -@patch.object(api, 'get') -def test_public_keys(mock_get): - response = "a b key1\na b key2" - mock_get.return_value = Mock(text=response) - assert api.public_keys('foo bar') == response - mock_get.assert_called_with( - '{0}/foo%20bar'.format(api.config.pubkeys_url) - ) - - -@patch.object(api, 'get') -def test_public_key(mock_get): - mock_get.return_value = Mock(text="a b c") - assert api.public_key('foo bar', 'keyname') == 'a b c' - mock_get.assert_called_with( - '{0}/foo%20bar/keyname'.format(api.config.pubkeys_url)) - - -@patch.object(api, 'get') -def test_public_key_names(mock_get): - response = "a b key1\na b key2" - mock_get.return_value = Mock(text=response) - assert list(api.public_key_names('foo bar')) == ['key1', 'key2'] - mock_get.assert_called_with( - '{0}/foo%20bar'.format(api.config.pubkeys_url) - ) - - -@patch.object(api, 'post') -def test_add_public_key(mock_post): - api.add_public_key('foo', 'a b c') - mock_post.assert_called_with(api.config.pubkeys_url + '/foo', data='a b c') - - -@patch.object(api, 'delete') -def test_remove_public_key(mock_del): - api.remove_public_key('foo', 'bar') - mock_del.assert_called_with(api.config.pubkeys_url + '/foo/bar') - - -@patch.object(api, 'delete') -@patch.object(api, 'get') -def test_remove_public_keys(mock_get, mock_del): - mock_get.return_value = Mock(text="a b key1\na b key2") - api.remove_public_keys('foo') - mock_del.assert_has_calls([ - call(api.config.pubkeys_url + '/foo/key1'), - call(api.config.pubkeys_url + '/foo/key2') - ]) diff --git a/tests/role_test.py b/tests/role_test.py index 55c2a45..19fd27a 100644 --- a/tests/role_test.py +++ b/tests/role_test.py @@ -24,6 +24,7 @@ api = new_from_key('login', 'pass', config) config.account = 'the-account' config.appliance_url = 'https://example.com/api' +config.url = 'http://possum.test' def test_roleid(): @@ -105,3 +106,13 @@ def test_role_grant_to_user(mock_put): ), data={} ) + +@patch.object(api, 'get') +def test_public_keys(mock_get): + response = "a b key1\na b key2" + mock_get.return_value = Mock(text=response) + user = api.role('user', 'somebody') + assert user.public_keys() == response + mock_get.assert_called_with( + 'http://possum.test/public_keys/the-account/user/somebody' + ) From d0e3972266bef4f5e43059f9eba2ec1ddb421e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Thu, 11 Aug 2016 19:36:32 +0200 Subject: [PATCH 08/62] Remove _service_url --- conjur/config.py | 31 ------------------- conjur/resource.py | 4 +-- conjur/role.py | 4 +-- tests/resource_test.py | 4 +-- tests/role_test.py | 69 +----------------------------------------- 5 files changed, 7 insertions(+), 105 deletions(-) diff --git a/conjur/config.py b/conjur/config.py index 69ebd14..b854025 100644 --- a/conjur/config.py +++ b/conjur/config.py @@ -53,16 +53,6 @@ def fset(self, value): return property(fget, fset, doc=doc) -def _service_url(name, per_account=True, doc=''): - def fget(self): - return self.service_url(name, per_account) - - def fset(self, value): - self.set(name + '_url', value) - - return property(fget=fget, fset=fset, doc=doc) - - class Config(object): def __init__(self, **kwargs): self._config = {} @@ -80,18 +70,6 @@ def update(self, *dicts, **kwargs): for d in dicts + (kwargs, ): self._config.update(d) - def service_url(self, service, per_account=True): - key = '%s_url' % service - if key in self._config: - return self._config[key] - if self.appliance_url is not None: - url_parts = [self.appliance_url] - if service != "core": - url_parts.append(service) - return "/".join(url_parts) - else: - raise ConfigException('Missing appliance_url') - def get(self, key, default=_DEFAULT): if key in self._config: return self._config[key] @@ -107,15 +85,6 @@ def get(self, key, default=_DEFAULT): def set(self, key, value): self._config[key] = value - authn_url = _service_url('authn', doc='URL for the authn service') - core_url = _service_url('core', doc='URL for the core service') - authz_url = _service_url('authz', - per_account=False, - doc='URL for the authz service') - - pubkeys_url = _service_url('pubkeys', doc='URL for the pubkeys service') - - cert_file = _setting('cert_file', None, "Path to certificate to verify ssl requests \ to appliance") diff --git a/conjur/resource.py b/conjur/resource.py index b2e1138..7b27c94 100644 --- a/conjur/resource.py +++ b/conjur/resource.py @@ -142,9 +142,9 @@ def url(self): Internal method to return a url for this object as a string. """ return "/".join([ - self.api.config.authz_url, - self.api.config.account, + self.api.config.url, 'resources', + self.api.config.account, self.kind, self.identifier ]) diff --git a/conjur/role.py b/conjur/role.py index e1716a3..24766a0 100644 --- a/conjur/role.py +++ b/conjur/role.py @@ -173,9 +173,9 @@ def _membership_url(self, member=None): return url def _url(self, *args): - return "/".join([self.api.config.authz_url, - self.api.config.account, + return "/".join([self.api.config.url, 'roles', + self.api.config.account, self.kind, self.identifier] + list(args)) diff --git a/tests/resource_test.py b/tests/resource_test.py index 7a3b47f..d41ccc6 100644 --- a/tests/resource_test.py +++ b/tests/resource_test.py @@ -37,7 +37,7 @@ def test_permitted_with_role(mock_get): assert resource.permitted('fry', bob) mock_get.assert_called_with( - 'https://example.com/api/authz/conjur/roles/user/bob', + 'http://possum.test/roles/conjur/user/bob', params={'privilege': 'fry', 'check': 'true', 'resource_id': 'conjur:food:bacon'}, check_errors=False @@ -49,7 +49,7 @@ def test_permitted_self_role(mock_get): mock_get.return_value = Mock(status_code=204) assert resource.permitted('fry') mock_get.assert_called_with( - 'https://example.com/api/authz/conjur/resources/food/bacon', # noqa E501 (line too long) + 'http://possum.test/resources/conjur/food/bacon', params={'privilege': 'fry', 'check': 'true'}, check_errors=False ) diff --git a/tests/role_test.py b/tests/role_test.py index 19fd27a..6e5d350 100644 --- a/tests/role_test.py +++ b/tests/role_test.py @@ -31,58 +31,6 @@ def test_roleid(): role = api.role('some-kind', 'the-id') assert role.roleid == 'the-account:some-kind:the-id' - -@patch.object(api, 'put') -def test_role_grant_to_without_admin(mock_put): - role = api.role('some-kind', 'the-id') - role.grant_to('some-other-role') - mock_put.assert_called_with( - '{0}/the-account/roles/some-kind/the-id?members&member={1}'.format( - config.authz_url, - 'some-other-role' - ), - data={} - ) - - -@patch.object(api, 'put') -def test_role_grant_to_with_admin_true(mock_put): - role = api.role('some-kind', 'the-id') - role.grant_to('some-other-role', True) - mock_put.assert_called_with( - '{0}/the-account/roles/some-kind/the-id?members&member={1}'.format( - config.authz_url, - 'some-other-role' - ), - data={'admin': 'true'} - ) - - -@patch.object(api, 'put') -def test_role_grant_to_with_admin_false(mock_put): - role = api.role('some-kind', 'the-id') - role.grant_to('some-other-role', False) - mock_put.assert_called_with( - '{0}/the-account/roles/some-kind/the-id?members&member={1}'.format( - config.authz_url, - 'some-other-role' - ), - data={'admin': 'false'} - ) - - -@patch.object(api, 'delete') -def test_role_revoke_from(mock_del): - role = api.role('some-kind', 'the-id') - role.revoke_from('some-other-role') - mock_del.assert_called_with( - '{0}/the-account/roles/some-kind/the-id?members&member={1}'.format( - config.authz_url, - 'some-other-role' - ) - ) - - @patch.object(api, 'get') def test_role_members(mock_get): members = ['foo', 'bar'] @@ -90,23 +38,8 @@ def test_role_members(mock_get): role = api.role('blah', 'boo') assert role.members() == members mock_get.assert_called_with( - '{0}/the-account/roles/blah/boo?members'.format(config.authz_url) + 'http://possum.test/roles/the-account/blah/boo?members' ) - - -@patch.object(api, 'put') -def test_role_grant_to_user(mock_put): - role = api.role('somekind', 'admins') - user = api.role('user', 'somebody') - role.grant_to(user) - mock_put.assert_called_with( - '{0}/the-account/roles/somekind/admins?members&member={1}'.format( - config.authz_url, - 'the-account%3Auser%3Asomebody' - ), - data={} - ) - @patch.object(api, 'get') def test_public_keys(mock_get): response = "a b key1\na b key2" From 31bfcaf28a622c6e621d514ec341014f748a129d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Thu, 11 Aug 2016 19:37:48 +0200 Subject: [PATCH 09/62] Remove resource permission methods --- conjur/resource.py | 39 --------------------------------------- 1 file changed, 39 deletions(-) diff --git a/conjur/resource.py b/conjur/resource.py index 7b27c94..f368a2b 100644 --- a/conjur/resource.py +++ b/conjur/resource.py @@ -51,45 +51,6 @@ def resourceid(self): """ return ":".join([self.api.config.account, self.kind, self.identifier]) - def permit(self, role, privilege, grant_option=False): - """ - Permit `role` to perform `privilege` on this resource. - - `role` is a qualified conjur identifier (e.g. `'user:alice`') or an object - with a `role` attribute or `roleid` method, such as a `conjur.User` or - `conjur.Group`. - - If `grant_option` is True, the role will be able to grant this - permission to other resources. - - You must own the resource or have the permission with `grant_option` - to call this method. - """ - data = {} - params = { - 'permit': 'true', - 'privilege': privilege, - 'role': authzid(role, 'role') - } - if grant_option: - data['grant_option'] = 'true' - - self.api.post(self.url(), data=data, params=params) - - def deny(self, role, privilege): - """ - Deny `role` permission to perform `privilege` on this resource. - - You must own the resource or have the permission with `grant_option` - on it to call this method. - """ - params = { - 'permit': 'true', - 'privilege': privilege, - 'role': authzid(role) - } - - self.api.post(self.url(), params=params) def permitted(self, privilege, role=None): """ From a2ed263896ef860433fb4b5ad2f8c742918d824a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Thu, 11 Aug 2016 19:38:22 +0200 Subject: [PATCH 10/62] Remove role grant/revoke methods --- conjur/role.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/conjur/role.py b/conjur/role.py index 24766a0..fdcf16c 100644 --- a/conjur/role.py +++ b/conjur/role.py @@ -128,31 +128,6 @@ def is_permitted(self, resource, privilege): else: raise ConjurException("Request failed: %d" % response.status_code) - def grant_to(self, member, admin=None): - """ - Grant this role to `member`. - - `member` is a string or object with a `role` attribute or `roleid` method, - such as a `conjur.User` or `conjur.Group`. - - `admin` whether the member can grant this role to others. - - """ - data = {} - if admin is not None: - data['admin'] = 'true' if admin else 'false' - self.api.put(self._membership_url(member), data=data) - - def revoke_from(self, member): - """ - The inverse of `conjur.Role.grant_to`. Removes `member` from the members of this - role. - - `member` is a string or object with a `role` attribute or `roleid` method, - such as a `conjur.User` or `conjur.Group`. - """ - self.api.delete(self._membership_url(member)) - def members(self): """ Return a list of members of this role. Members are returned as `dict`s From b3829d99b4be206e7974dbc97b93607bda8aa134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Thu, 11 Aug 2016 19:39:53 +0200 Subject: [PATCH 11/62] Remove appliance_url remnants --- tests/conftest.py | 2 +- tests/role_test.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 1fc7a53..f57fa47 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,4 +2,4 @@ def pytest_runtest_setup(item): - config.appliance_url = 'https://example.com/api' + config.url = 'http://possum.test' diff --git a/tests/role_test.py b/tests/role_test.py index 6e5d350..1a1447e 100644 --- a/tests/role_test.py +++ b/tests/role_test.py @@ -23,7 +23,6 @@ config = Config() api = new_from_key('login', 'pass', config) config.account = 'the-account' -config.appliance_url = 'https://example.com/api' config.url = 'http://possum.test' From 5f787ac157703a79ac5efcbf5720d3774c8bc257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Tue, 16 Aug 2016 18:15:49 +0200 Subject: [PATCH 12/62] Integration test harness --- Dockerfile | 3 +- features/policy/conjur.yml | 23 ++++++++++++ jenkins.sh | 73 +++++++++++++++++++++++++------------- 3 files changed, 72 insertions(+), 27 deletions(-) create mode 100644 features/policy/conjur.yml diff --git a/Dockerfile b/Dockerfile index 8a33001..bc7d7dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,5 @@ WORKDIR /app COPY requirements* /app/ RUN pip install -r requirements.txt -r requirements_dev.txt -COPY . /app - VOLUME /artifacts +VOLUME /app diff --git a/features/policy/conjur.yml b/features/policy/conjur.yml new file mode 100644 index 0000000..b65c691 --- /dev/null +++ b/features/policy/conjur.yml @@ -0,0 +1,23 @@ +--- +- !user test + +- !webservice the-service + +- !user + id: alice + owner: !user test + public_keys: + - ssh-rsa AAAAB3NzHhIqxF alice@home + +- !group everyone + +- !variable db-password + +- !grant + role: !group everyone + member: !user alice + +- !permit + role: !user alice + resource: !webservice the-service + privilege: [update, execute] diff --git a/jenkins.sh b/jenkins.sh index b270e4f..42df9b8 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -1,46 +1,69 @@ #!/bin/bash -ex -function cleanup { - docker rm -f $(cat conjur-cid) - rm conjur-cid +CONJUR_VERSION=${CONJUR_VERSION:-"latest"} +DOCKER_IMAGE=${DOCKER_IMAGE:-"conjurinc/possum:$CONJUR_VERSION"} +NOKILL=${NOKILL:-"1"} +PULL=${PULL:-"0"} +CMD_PREFIX="" + +function finish { + # Stop and remove the Conjur container if env var NOKILL != "1" + if [ "$NOKILL" != "1" ]; then + docker rm -f ${pg} || true + docker rm -f ${cid} || true + fi } +trap finish EXIT -if [ -z "$KEEP" ] ; then - trap cleanup EXIT +job=$JOB_NAME +if [ -z $job ]; then + job=sandbox fi -APPLIANCE_VERSION=4.8-stable +tag=api-python:$job +docker build -t api-python:$job . -rm -rf artifacts +rm -rf report +mkdir report -docker build -t api-python . - -docker run -d \ - --cidfile=conjur-cid \ - --privileged \ - -p 443:443 \ - -v ${PWD}/ci:/ci \ - --add-host=conjur:127.0.0.1 \ - registry.tld/conjur-appliance-cuke-master:$APPLIANCE_VERSION +if [ "$PULL" == "1" ]; then + docker pull $DOCKER_IMAGE +fi -docker exec $(cat conjur-cid) /opt/conjur/evoke/bin/wait_for_conjur +if [ ! -f data_key ]; then + echo "Generating data key" + docker run --rm ${DOCKER_IMAGE} data-key generate > data_key +fi +export POSSUM_DATA_KEY="$(cat data_key)" -mkdir -p ${PWD}/certs +pg=$(docker run -d postgres:9.3) -docker cp $(cat conjur-cid):/opt/conjur/etc/ssl/cuke-master.pem ${PWD}/certs +# Launch and configure a Conjur container +cid=$(docker run -d \ + -e DATABASE_URL=postgresql://postgres@pg/postgres \ + -e POSSUM_DATA_KEY \ + -e POSSUM_ADMIN_PASSWORD=secret \ + -e CONJUR_PASSWORD_ALICE=secret \ + -v $PWD/features/policy:/run/possum/policy/ \ + --link ${pg}:pg \ + ${DOCKER_IMAGE} \ + server -a cucumber -f /run/possum/policy/conjur.yml) +>&2 echo "Container id:" +>&2 echo $cid -docker exec $(cat conjur-cid) /ci/setup.sh +sleep 10 docker run --rm -Pi \ - -v ${PWD}/certs:/certs \ -v ${PWD}/artifacts:/artifacts \ - --link $(cat conjur-cid):conjur \ -api-python sh < Date: Tue, 16 Aug 2016 18:40:40 +0200 Subject: [PATCH 13/62] Feature environment --- features/environment.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/features/environment.py b/features/environment.py index 49fbcbf..d8aba89 100644 --- a/features/environment.py +++ b/features/environment.py @@ -36,19 +36,16 @@ def conjur_env(name, default=None): def api(): config = Config( account=conjur_env('account', 'cucumber'), - appliance_url=conjur_env('appliance_url', 'https://conjur/api'), + url=conjur_env('url', 'http://possum.test'), cert_file=conjur_env('cert_file', '/opt/conjur/etc/ssl/conjur.pem') ) print("config={}".format(repr(config._config))) - if not os.path.exists(config.cert_file): - raise Exception("Missing cert file at {}".format(config.cert_file)) + login = 'alice' + password = 'secret' - login = conjur_env('authn_login', 'admin') - password = conjur_env('admin_password', 'secret') - - return conjur.new_from_key(login, password, config) + return conjur.new_from_password(login, password, config) def random_string(prefix, size=8): From 7bd920976d7dbd12e7bbe4558cac7b8d9208bbf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Tue, 16 Aug 2016 18:41:00 +0200 Subject: [PATCH 14/62] Update group features --- features/groups.feature | 22 ++---------- features/steps/group_steps.py | 66 ++++------------------------------- 2 files changed, 9 insertions(+), 79 deletions(-) diff --git a/features/groups.feature b/features/groups.feature index 825b457..dc19267 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -1,23 +1,5 @@ Feature: Group Management - Scenario: I can create groups - When I try to create a group "foo" - Then it succeeds - Scenario: I can list group memberships - When I create a group "bar" - Then I can list the group members - - Scenario: I can add members to a group - When I create a group "developers" - And I create a user "bob" - And I add the user to the group - Then the user is a member of the group - - Scenario: I can remove members from a group - When I create a group "test" - And I create a user "bill" - And I add the user to the group - Then the user is a member of the group - When I remove the user from the group - Then the user is not a member of the group + When I list members of "group:everyone" + Then "cucumber:user:alice" is a member diff --git a/features/steps/group_steps.py b/features/steps/group_steps.py index 93bf107..95be391 100644 --- a/features/steps/group_steps.py +++ b/features/steps/group_steps.py @@ -1,64 +1,12 @@ from behave import when, then -def group_has_member(group, user): - members = group.members() - userid = user.role.roleid - for m in members: - if m['member'] == userid: - return True - return False - - -@when('I create a group "{name}"') -def create_group_impl(context, name): - name = context.random_string(name) - context.group = context.api.create_group(name) - - -@when('I try to create a group "{name}"') -def try_create_group_impl(context, name): - name = context.random_string(name) - context.create_group_failed = False - try: - context.group = context.api.create_group(name) - except Exception as e: - context.create_group_failed = e - - -@when('I add the user to the group') -def add_user_to_group(context): - context.group.add_member(context.user) - - -@when('I remove the user from the group') -def remove_user_from_group(context): - context.group.remove_member(context.user) - - -@then('The user is a member of the group') -def user_is_a_member_of_group(context): - assert group_has_member(context.group, context.user) - - -@then('the user is not a member of the group') -def user_is_not_a_member_of_group(context): - assert not group_has_member(context.group, context.user) - - -@then('it succeeds') -def it_succeeds_impl(context): - if context.create_group_failed: - err = context.create_group_failed - context.create_group_failed = False - assert not err - - -@then('I can list the group members') -def list_members_impl(context): +@when(u'I list members of "{kind}:{name}"') +def list_members_impl(context, kind, name): + context.group = context.api.role(kind, name) context.group_members = context.group.members() - -@when('I add the member to the group') -def add_member_impl(context): - context.group.add_member(context.user.role) +@then(u'"{id}" is a member') +def is_group_member(context, id): + print(context.group_members) + assert id in (x['member'] for x in context.group_members) From 90d2326720262f90f301f3e56d6c32de00ddcd13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Tue, 16 Aug 2016 18:41:20 +0200 Subject: [PATCH 15/62] Add Role.info, adjust Role.members --- conjur/role.py | 23 +++++++++++++---------- tests/role_test.py | 24 ++++++++++++++++++++---- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/conjur/role.py b/conjur/role.py index fdcf16c..c28dd04 100644 --- a/conjur/role.py +++ b/conjur/role.py @@ -128,24 +128,27 @@ def is_permitted(self, resource, privilege): else: raise ConjurException("Request failed: %d" % response.status_code) + def info(self): + """ + Return role information. This will be a `dict` with the following keys: + + * `'created_at'` timestamp of role creation (eg. last refresh of the + policy that creates it) + * `'id'` fully qualified role id + * `'members'` members of the role (see `members` for details) + """ + return self.api.get(self._url()).json() + def members(self): """ Return a list of members of this role. Members are returned as `dict`s with the following keys: - * `'member'` the fully qualified identifier of the group + * `'member'` the fully qualified identifier of the member * `'role'` the fully qualified identifier of the group (redundant) - * `'grantor'` the role that granted the membership * `'admin_option'` whether this member can grant membership in the group to other roles. """ - return self.api.get(self._membership_url()).json() - - def _membership_url(self, member=None): - url = self._url() + "?members" - if member is not None: - memberid = authzid(member, 'role') - url += "&member=" + urlescape(memberid) - return url + return self.info()['members'] def _url(self, *args): return "/".join([self.api.config.url, diff --git a/tests/role_test.py b/tests/role_test.py index 1a1447e..7cbd6e3 100644 --- a/tests/role_test.py +++ b/tests/role_test.py @@ -30,14 +30,30 @@ def test_roleid(): role = api.role('some-kind', 'the-id') assert role.roleid == 'the-account:some-kind:the-id' +role_info = { + u'id': u'cucumber:group:everyone', + u'members': [ + { + u'member': u'cucumber:user:admin', + u'role': u'cucumber:group:everyone', + u'grantor': u'cucumber:group:everyone', + u'admin_option': True + }, { + u'member': u'cucumber:user:alice', + u'role': u'cucumber:group:everyone', + u'grantor': u'cucumber:group:everyone', + u'admin_option': False + } + ] +} + @patch.object(api, 'get') def test_role_members(mock_get): - members = ['foo', 'bar'] - mock_get.return_value = Mock(json=lambda: members) + mock_get.return_value = Mock(json=lambda: role_info) role = api.role('blah', 'boo') - assert role.members() == members + assert role.members() == role_info['members'] mock_get.assert_called_with( - 'http://possum.test/roles/the-account/blah/boo?members' + 'http://possum.test/roles/the-account/blah/boo' ) @patch.object(api, 'get') def test_public_keys(mock_get): From a76840ac143be5f79b8f95eb47f078e83c4cf12c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Tue, 16 Aug 2016 18:42:02 +0200 Subject: [PATCH 16/62] Remove variable features --- features/variables.feature | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 features/variables.feature diff --git a/features/variables.feature b/features/variables.feature deleted file mode 100644 index 076eca8..0000000 --- a/features/variables.feature +++ /dev/null @@ -1,8 +0,0 @@ -Feature: Variable manipuation - - Scenario: I can create a variable and add a value to it - When I create a variable - And I add a value "foo" - Then the variable should have attribute "version_count" with value 1 - And the variable should have value "foo" - From bfb0ab1049395d1f51f3bf8360639eb118eccaa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Tue, 16 Aug 2016 18:42:31 +0200 Subject: [PATCH 17/62] Remove users features --- features/users.feature | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 features/users.feature diff --git a/features/users.feature b/features/users.feature deleted file mode 100644 index a999a02..0000000 --- a/features/users.feature +++ /dev/null @@ -1,9 +0,0 @@ -Feature: User management - - Scenario: I can create a user and authenticate with its credentials - When I create a user - Then I can login as the user using the api key - - Scenario: I can create a user with a password - When I create a user with a password - Then I can login as the user using the password \ No newline at end of file From eada805cb3d2fd1206cc0949c973637412b3d36d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Tue, 16 Aug 2016 19:00:16 +0200 Subject: [PATCH 18/62] Modify permissions features to work with the loaded policy --- features/permissions.feature | 11 +++++------ features/steps/permission_steps.py | 4 +++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/features/permissions.feature b/features/permissions.feature index ed3cb8e..fa95d4f 100644 --- a/features/permissions.feature +++ b/features/permissions.feature @@ -1,9 +1,8 @@ Feature: I can check permissions from Python code. Scenario: Check some predefined permissions + Given the preloaded policy Then "job:programmer" can not "fry" "food:bacon" - And "job:programmer" can "eat" "food:bacon" - And "job:cook" can "fry" "food:bacon" - And "job:cook" can not "eat" "food:bacon" - And the current role can "eat" "food:bacon" - And the current role can "fry" "food:bacon" - But the current role can not "eat" "food:does-not-exist" + And "user:alice" can "execute" "webservice:the-service" + And the current role can "execute" "webservice:the-service" + And the current role can "update" "webservice:the-service" + But the current role can not "update" "variable:db-password" diff --git a/features/steps/permission_steps.py b/features/steps/permission_steps.py index 6fc8372..3a721d5 100644 --- a/features/steps/permission_steps.py +++ b/features/steps/permission_steps.py @@ -11,7 +11,9 @@ def check_permission_current_impl(api, privilege, resource, can): resource = api.resource(*resource.split(':')) assert resource.permitted(privilege) == can - +@given(u'the preloaded policy') +def preloaded_policy(context): + pass @then('"{role}" can "{privilege}" "{resource}"') def role_can_resource(context, role, privilege, resource): From 1aed91b4b81a89902bb18fec41949084dcaa3aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Tue, 16 Aug 2016 19:00:37 +0200 Subject: [PATCH 19/62] Resource check parameter is now 'resource', not 'resource_id' --- conjur/role.py | 2 +- tests/resource_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conjur/role.py b/conjur/role.py index c28dd04..6c966d1 100644 --- a/conjur/role.py +++ b/conjur/role.py @@ -116,7 +116,7 @@ def is_permitted(self, resource, privilege): """ params = { 'check': 'true', - 'resource_id': authzid(resource, 'resource'), + 'resource': authzid(resource, 'resource'), 'privilege': privilege } response = self.api.get(self._url(), params=params, diff --git a/tests/resource_test.py b/tests/resource_test.py index d41ccc6..b4c1000 100644 --- a/tests/resource_test.py +++ b/tests/resource_test.py @@ -39,7 +39,7 @@ def test_permitted_with_role(mock_get): mock_get.assert_called_with( 'http://possum.test/roles/conjur/user/bob', params={'privilege': 'fry', 'check': 'true', - 'resource_id': 'conjur:food:bacon'}, + 'resource': 'conjur:food:bacon'}, check_errors=False ) From b4e296af55f7e48eece03fa13fcff4e8a74bc337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Tue, 16 Aug 2016 19:02:32 +0200 Subject: [PATCH 20/62] Make /artifacts mounted with /app --- Dockerfile | 1 - jenkins.sh | 1 - 2 files changed, 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index bc7d7dd..aabdf57 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,5 +6,4 @@ WORKDIR /app COPY requirements* /app/ RUN pip install -r requirements.txt -r requirements_dev.txt -VOLUME /artifacts VOLUME /app diff --git a/jenkins.sh b/jenkins.sh index 42df9b8..0af1d9c 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -56,7 +56,6 @@ cid=$(docker run -d \ sleep 10 docker run --rm -Pi \ - -v ${PWD}/artifacts:/artifacts \ -v ${PWD}:/app \ --link $cid:possum.test \ $tag sh < Date: Tue, 16 Aug 2016 19:05:09 +0200 Subject: [PATCH 21/62] Pull by default on Jenkins --- jenkins.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins.sh b/jenkins.sh index 0af1d9c..3f9de86 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -3,7 +3,7 @@ CONJUR_VERSION=${CONJUR_VERSION:-"latest"} DOCKER_IMAGE=${DOCKER_IMAGE:-"conjurinc/possum:$CONJUR_VERSION"} NOKILL=${NOKILL:-"1"} -PULL=${PULL:-"0"} +PULL=${PULL:-"1"} CMD_PREFIX="" function finish { From 81aad77872f5be8de5e4394f7bd62f0c38432c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Tue, 16 Aug 2016 19:06:43 +0200 Subject: [PATCH 22/62] Or not This reverts commit b471f768eeb9447e8bbe1f0cef4dc3e137a29368. --- jenkins.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins.sh b/jenkins.sh index 3f9de86..0af1d9c 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -3,7 +3,7 @@ CONJUR_VERSION=${CONJUR_VERSION:-"latest"} DOCKER_IMAGE=${DOCKER_IMAGE:-"conjurinc/possum:$CONJUR_VERSION"} NOKILL=${NOKILL:-"1"} -PULL=${PULL:-"1"} +PULL=${PULL:-"0"} CMD_PREFIX="" function finish { From ba27cef98b1997270deb54ef7c1465b1296d60cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Thu, 18 Aug 2016 14:24:09 +0200 Subject: [PATCH 23/62] Change Jenkins script defaults --- jenkins.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jenkins.sh b/jenkins.sh index 0af1d9c..306d84f 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -2,8 +2,8 @@ CONJUR_VERSION=${CONJUR_VERSION:-"latest"} DOCKER_IMAGE=${DOCKER_IMAGE:-"conjurinc/possum:$CONJUR_VERSION"} -NOKILL=${NOKILL:-"1"} -PULL=${PULL:-"0"} +NOKILL=${NOKILL:-"0"} +PULL=${PULL:-"1"} CMD_PREFIX="" function finish { From baa55d86b102f7c80d6da93a5aea25e3c6b56c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Thu, 18 Aug 2016 14:27:31 +0200 Subject: [PATCH 24/62] Add a wait for possum to the jenkins flow --- jenkins.sh | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/jenkins.sh b/jenkins.sh index 306d84f..6373e0d 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -53,8 +53,6 @@ cid=$(docker run -d \ >&2 echo "Container id:" >&2 echo $cid -sleep 10 - docker run --rm -Pi \ -v ${PWD}:/app \ --link $cid:possum.test \ @@ -66,6 +64,19 @@ find . -name '*.pyc' -delete py.test --cov conjur --junitxml=pytest.xml --instafail +# wait for possum to be up +python -c " +import requests; import time +for _ in range(30): + try: + requests.get('http://possum.test', timeout=30) + exit(0) + except requests.exceptions.ConnectionError: + time.sleep(1) + +exit(1) +" + # Runs cukes with coverage coverage run --source='conjur/' -a -m behave --junit \ --junit-directory=/artifacts/ \ From a32dd98c6cfa47dfcbd5987c2e289c5d8437802a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Thu, 18 Aug 2016 14:41:50 +0200 Subject: [PATCH 25/62] Archive artifacts correctly --- Dockerfile | 1 + jenkins.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index aabdf57..9fb9773 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,3 +7,4 @@ COPY requirements* /app/ RUN pip install -r requirements.txt -r requirements_dev.txt VOLUME /app +VOLUME /artifacts diff --git a/jenkins.sh b/jenkins.sh index 6373e0d..d8ec5f3 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -55,6 +55,7 @@ cid=$(docker run -d \ docker run --rm -Pi \ -v ${PWD}:/app \ + -v ${PWD}/artifacts:/artifacts \ --link $cid:possum.test \ $tag sh < Date: Fri, 19 Aug 2016 15:26:50 +0200 Subject: [PATCH 26/62] Throw an error on authentication error --- conjur/__init__.py | 2 ++ tests/api_test.py | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/conjur/__init__.py b/conjur/__init__.py index 023a1b0..fba0d22 100644 --- a/conjur/__init__.py +++ b/conjur/__init__.py @@ -85,6 +85,8 @@ def new_from_password(login, password, configuration=None): configuration = _config(configuration) url = "%s/authn/%s/login" % (configuration.url, configuration.account) response = requests.get(url, auth=(login, password), verify=configuration.verify) + if response.status_code != 200: + raise ConjurException("Authentication error: {} {}".format(response.status_code, response.reason)) api_key = response.text return new_from_key(login, api_key, configuration) diff --git a/tests/api_test.py b/tests/api_test.py index ce8dcb1..67e885d 100644 --- a/tests/api_test.py +++ b/tests/api_test.py @@ -22,10 +22,10 @@ from mock import patch, Mock import requests +import pytest import conjur - @patch.object(requests, 'post') def test_authenticate(mock_post): api = conjur.new_from_key("alice", "api-key") @@ -60,13 +60,24 @@ def test_authenticate_password(mock_get, mock_post): mock_post.assert_called_with("http://possum.test/authn/conjur/alice/authenticate", "api-key", verify=api.config.verify) +@patch.object(requests, 'get') +def test_authenticate_wrong_password(mock_get): + mock_get.return_value = mock_response = Mock() + mock_response.status_code = 401 + + with pytest.raises(conjur.exceptions.ConjurException): + conjur.new_from_password("alice", "bad password") + + mock_get.assert_called_with("http://possum.test/authn/conjur/login", + auth=("alice", "bad password"), + verify=conjur.config.verify) + @patch.object(requests, 'post') def test_authenticate_with_cached_token(mock_post): api = conjur.new_from_token("token token") assert api.authenticate() == "token token" mock_post.assert_not_called() - def test_auth_header(): api = conjur.new_from_token("the token") expected = 'Token token="%s"' % (base64.b64encode("the token")) From d748170d542ce254390a972bc882c428e65449c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Thu, 25 Aug 2016 14:59:01 +0200 Subject: [PATCH 27/62] Add methods for listing resources and handling fq resources --- conjur/api.py | 25 ++++++++++++++++++++++++- conjur/resource.py | 18 +++++++++++------- conjur/util.py | 7 +++++++ tests/resource_test.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 8 deletions(-) diff --git a/conjur/api.py b/conjur/api.py index 8d7d31d..b075168 100644 --- a/conjur/api.py +++ b/conjur/api.py @@ -219,4 +219,27 @@ def resource(self, kind, identifier): get the resource for a user variable named db_password, you would call `api.resource('variable', 'db_password')`. """ - return Resource(self, kind, identifier) + return Resource(self, kind=kind, id=identifier) + + def resource_qualified(self, qualified_identifier): + """ + Return a `conjur.Resource` corresponding to the given qualified identifier. + """ + return Resource(self, id=qualified_identifier) + + def resources(self, kind=None): + """ + Return a list of all `conjur.Resources` from the account, optionally filtered by kind. + """ + resources = self.get(self._resources_url(kind=kind)).json() + return [self.resource_qualified(r["id"]) for r in resources] + + def _resources_url(self, kind=None): + pieces = [ + self.config.url, + 'resources', + self.config.account, + ] + if kind: + pieces += [kind] + return '/'.join(pieces) diff --git a/conjur/resource.py b/conjur/resource.py index f368a2b..08408d4 100644 --- a/conjur/resource.py +++ b/conjur/resource.py @@ -18,7 +18,7 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -from conjur.util import authzid +from conjur.util import authzid, split_id from conjur.role import Role from conjur.exceptions import ConjurException @@ -39,17 +39,21 @@ class Resource(object): given user, with the keys themselves attached as secrets. (Note public keys aren't secrets per se, but are stored as such for consistency.) """ - def __init__(self, api, kind, identifier): + def __init__(self, api, account=None, kind=None, id=None): self.api = api - self.kind = kind - self.identifier = identifier + [self.account, self.kind, self.identifier] = split_id(id) + self.account = self.account or account or api.config.account + self.kind = self.kind or kind + assert self.account and (not account or self.account == account) + assert self.kind and (not kind or self.kind == kind) + assert self.identifier @property def resourceid(self): """ The fully qualified resource id as a string, like `'the-account:variable:db-password`. """ - return ":".join([self.api.config.account, self.kind, self.identifier]) + return ":".join([self.account, self.kind, self.identifier]) def permitted(self, privilege, role=None): @@ -105,7 +109,7 @@ def url(self): return "/".join([ self.api.config.url, 'resources', - self.api.config.account, + self.account, self.kind, self.identifier ]) @@ -142,7 +146,7 @@ def secret_url(self): return "/".join([ self.api.config.url, 'secrets', - self.api.config.account, + self.account, self.kind, self.identifier ]) diff --git a/conjur/util.py b/conjur/util.py index 055972b..77ed75d 100644 --- a/conjur/util.py +++ b/conjur/util.py @@ -39,3 +39,10 @@ def authzid(obj, kind, with_account=True): if hasattr(obj, attr): return authzid(getattr(obj, attr), kind) raise TypeError("Can't get {0}id from {1}".format(kind, obj)) + +def split_id(id): + """ + Return id split into [account, kind, id], any of which might be None. + """ + pieces = id.split(':', 2) + return [None] * (3 - len(pieces)) + pieces diff --git a/tests/resource_test.py b/tests/resource_test.py index b4c1000..d666d17 100644 --- a/tests/resource_test.py +++ b/tests/resource_test.py @@ -44,6 +44,11 @@ def test_permitted_with_role(mock_get): ) +def test_fq_resource(): + res = api.resource_qualified('foo:bar:baz') + assert res.url() == 'http://possum.test/resources/foo/bar/baz' + + @patch.object(api, 'get') def test_permitted_self_role(mock_get): mock_get.return_value = Mock(status_code=204) @@ -99,3 +104,27 @@ def test_add_secret_value(mock_post): '%s/secrets/conjur/food/bacon' % api.config.url, data='boo', ) + + +@patch.object(api, 'get') +def test_resource_listing(mock_get): + resources = ['conjur:foo:bar', 'conjur:baz:bar'] + mock_get.return_value = resp = Mock( + json=lambda: [{"id": r} for r in resources], + status_code=200 + ) + resources_list = api.resources() + assert set(r.resourceid for r in resources_list) == set(resources) + mock_get.assert_called_with('http://possum.test/resources/conjur') + + +@patch.object(api, 'get') +def test_resource_listing_filtered(mock_get): + resources = ['conjur:foo:bar'] + mock_get.return_value = resp = Mock( + json=lambda: [{"id": r} for r in resources], + status_code=200 + ) + resources_list = api.resources(kind='foo') + assert set(r.resourceid for r in resources_list) == set(resources) + mock_get.assert_called_with('http://possum.test/resources/conjur/foo') From 479a53adb68434dbaf225fe10cedb21857dd1ada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Thu, 25 Aug 2016 15:18:59 +0200 Subject: [PATCH 28/62] Allow creating Role from a fqi --- conjur/api.py | 8 ++++++- conjur/role.py | 53 +++++++++++++--------------------------------- tests/role_test.py | 6 ++++++ 3 files changed, 28 insertions(+), 39 deletions(-) diff --git a/conjur/api.py b/conjur/api.py index b075168..a41842d 100644 --- a/conjur/api.py +++ b/conjur/api.py @@ -204,7 +204,13 @@ def role(self, kind, identifier): `identifier` should be the *unqualified* Conjur id. For example, to get the role for a user named bub, you would call `api.role('user', 'bub')`. """ - return Role(self, kind, identifier) + return Role(self, kind=kind, id=identifier) + + def role_qualified(self, qualified_identifier): + """ + Return a `conjur.Role` corresponding to the given qualified identifier. + """ + return Role(self, id=qualified_identifier) def resource(self, kind, identifier): """ diff --git a/conjur/role.py b/conjur/role.py index 6c966d1..368b160 100644 --- a/conjur/role.py +++ b/conjur/role.py @@ -18,7 +18,7 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -from conjur.util import urlescape, authzid +from conjur.util import urlescape, authzid, split_id from conjur.exceptions import ConjurException import logging @@ -38,34 +38,14 @@ class Role(object): `conjur.User` and `conjur.Group` objects have `role` members that reference the role corresponding to that Conjur asset. """ - def __init__(self, api, kind, identifier): - """ - Create a role to represent the Conjur role with id `:`. For - example, to represent the role associated with a user named bob, - - role = Role(api, 'user', 'bob') - - `api` must be a `conjur.API` instance, used to implement this classes interactions with Conjur - - `kind` is a string giving the role kind - - `identifier` is the unqualified identifier of the role. - """ - + def __init__(self, api, account=None, kind=None, id=None): self.api = api - """ - The `conjur.API` instance used to implement our methods. - """ - - self.kind = kind - """ - The `kind` portion of the role's id. - """ - - self.identifier = identifier - """ - The `identifier` portion of the role's id. - """ + [self.account, self.kind, self.identifier] = split_id(id) + self.account = self.account or account or api.config.account + self.kind = self.kind or kind + assert self.account and (not account or self.account == account) + assert self.kind and (not kind or self.kind == kind) + assert self.identifier @classmethod def from_roleid(cls, api, roleid): @@ -77,10 +57,7 @@ def from_roleid(cls, api, roleid): `roleid` is a fully or partially qualified Conjur identifier, for example, `"the-account:service:some-service"` or `"service:some-service"` resolve to the same role. """ - tokens = authzid(roleid, 'role').split(':', 3) - if len(tokens) == 3: - tokens.pop(0) - return cls(api, *tokens) + return cls(api, id=authzid(roleid, 'role')) @property def roleid(self): @@ -94,7 +71,7 @@ def roleid(self): 'the-account:user:bob' """ - return ':'.join([self.api.config.account, self.kind, self.identifier]) + return ':'.join([self.account, self.kind, self.identifier]) def is_permitted(self, resource, privilege): """ @@ -119,7 +96,7 @@ def is_permitted(self, resource, privilege): 'resource': authzid(resource, 'resource'), 'privilege': privilege } - response = self.api.get(self._url(), params=params, + response = self.api.get(self.url(), params=params, check_errors=False) if response.status_code == 204: return True @@ -137,7 +114,7 @@ def info(self): * `'id'` fully qualified role id * `'members'` members of the role (see `members` for details) """ - return self.api.get(self._url()).json() + return self.api.get(self.url()).json() def members(self): """ @@ -150,10 +127,10 @@ def members(self): """ return self.info()['members'] - def _url(self, *args): + def url(self, *args): return "/".join([self.api.config.url, 'roles', - self.api.config.account, + self.account, self.kind, self.identifier] + list(args)) @@ -161,7 +138,7 @@ def _public_keys_url(self): return '/'.join([ self.api.config.url, 'public_keys', - self.api.config.account, + self.account, self.kind, self.identifier ]) diff --git a/tests/role_test.py b/tests/role_test.py index 7cbd6e3..25a4f0a 100644 --- a/tests/role_test.py +++ b/tests/role_test.py @@ -30,6 +30,12 @@ def test_roleid(): role = api.role('some-kind', 'the-id') assert role.roleid == 'the-account:some-kind:the-id' + +def test_role_qualified(): + role = api.role_qualified('foo:bar:baz') + assert role.url() == 'http://possum.test/roles/foo/bar/baz' + + role_info = { u'id': u'cucumber:group:everyone', u'members': [ From 54a44a3af2315d0750a029aabe72317954c466b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Thu, 25 Aug 2016 18:33:43 +0200 Subject: [PATCH 29/62] Strip Role.public_keys output --- conjur/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conjur/role.py b/conjur/role.py index 368b160..4d851dc 100644 --- a/conjur/role.py +++ b/conjur/role.py @@ -147,4 +147,4 @@ def public_keys(self): """ Returns all SSH public keys for this role, if any, as a newline delimited string. """ - return self.api.get(self._public_keys_url()).text + return self.api.get(self._public_keys_url()).text.strip() From 6216a7801c7148d3f025f9db64711c1545338564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Thu, 25 Aug 2016 19:00:57 +0200 Subject: [PATCH 30/62] Add Role.resource and Resource.role methods --- conjur/resource.py | 8 ++++++++ conjur/role.py | 8 ++++++++ tests/resource_test.py | 3 +++ tests/role_test.py | 4 ++++ 4 files changed, 23 insertions(+) diff --git a/conjur/resource.py b/conjur/resource.py index 08408d4..86526c3 100644 --- a/conjur/resource.py +++ b/conjur/resource.py @@ -150,3 +150,11 @@ def secret_url(self): self.kind, self.identifier ]) + + def role(self): + """ + Return the corresponding role (ie. with the same id). + Note not every resource will have one, so the + returned object may refer to a nonexistent role. + """ + return self.api.role_qualified(self.resourceid) diff --git a/conjur/role.py b/conjur/role.py index 4d851dc..0995f84 100644 --- a/conjur/role.py +++ b/conjur/role.py @@ -148,3 +148,11 @@ def public_keys(self): Returns all SSH public keys for this role, if any, as a newline delimited string. """ return self.api.get(self._public_keys_url()).text.strip() + + def resource(self): + """ + Return the corresponding resource (ie. with the same id). + Note not every role will have one, so the + returned object may refer to a nonexistent resource. + """ + return self.api.resource_qualified(self.roleid) diff --git a/tests/resource_test.py b/tests/resource_test.py index d666d17..5feb6f9 100644 --- a/tests/resource_test.py +++ b/tests/resource_test.py @@ -48,6 +48,9 @@ def test_fq_resource(): res = api.resource_qualified('foo:bar:baz') assert res.url() == 'http://possum.test/resources/foo/bar/baz' +def test_role_of_resource(): + res = api.resource_qualified('foo:bar:baz') + assert res.role().roleid == 'foo:bar:baz' @patch.object(api, 'get') def test_permitted_self_role(mock_get): diff --git a/tests/role_test.py b/tests/role_test.py index 25a4f0a..8f2238f 100644 --- a/tests/role_test.py +++ b/tests/role_test.py @@ -36,6 +36,10 @@ def test_role_qualified(): assert role.url() == 'http://possum.test/roles/foo/bar/baz' +def test_resource_of_role(): + role = api.role_qualified('foo:bar:baz') + assert role.resource().resourceid == 'foo:bar:baz' + role_info = { u'id': u'cucumber:group:everyone', u'members': [ From 51402a320664ac5aab76abef231616a0dff3a745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Thu, 25 Aug 2016 19:01:28 +0200 Subject: [PATCH 31/62] Directory example --- examples/directory.py | 33 +++++++++++++++++++++++++++++++++ examples/docker-compose.yml | 29 +++++++++++++++++++++++++++++ examples/env | 5 +++++ examples/start.sh | 13 +++++++++++++ examples/stop.sh | 4 ++++ 5 files changed, 84 insertions(+) create mode 100644 examples/directory.py create mode 100644 examples/docker-compose.yml create mode 100644 examples/env create mode 100755 examples/start.sh create mode 100755 examples/stop.sh diff --git a/examples/directory.py b/examples/directory.py new file mode 100644 index 0000000..1bb513e --- /dev/null +++ b/examples/directory.py @@ -0,0 +1,33 @@ +import conjur +import os + +possum_url = os.environ['POSSUM_URL'] +possum_account = os.environ['POSSUM_ACCOUNT'] +possum_login = os.environ['POSSUM_LOGIN'] +possum_password = os.environ['POSSUM_PASSWORD'] + +print('========================================================================='); +print('Base url :', possum_url); +print('Account :', possum_account); +print('Login :', possum_login); +print('Password :', possum_password); +print('========================================================================='); + +conjur.config.url = possum_url +conjur.config.account = possum_account + +client = conjur.new_from_password(possum_login, possum_password) + +for group in client.resources(kind='group'): + print("Group {}; members:".format(group.resourceid)) + for mem in group.role().members(): + roleid = mem["member"] + print(" - {}".format(roleid)) + +for user in client.resources(kind='user'): + print("User {}".format(user.resourceid)) + keys = user.role().public_keys() + if len(keys): + print(" public keys:") + for key in keys: + print(" - {}".format(key)) diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml new file mode 100644 index 0000000..282dadf --- /dev/null +++ b/examples/docker-compose.yml @@ -0,0 +1,29 @@ +pg: + image: postgres:9.3 + +example: + image: conjurinc/possum-example + entrypoint: /bin/sh + +possum: + image: conjurinc/possum + command: server -a example -f /var/lib/possum-example/policy/conjur.yml + environment: + DATABASE_URL: postgres://postgres@pg/postgres + POSSUM_ADMIN_PASSWORD: secret + POSSUM_DATA_KEY: + volumes_from: + - example + links: + - pg:pg + +api: + build: .. + entrypoint: bash + environment: + CONJUR_APPLIANCE_URL: http://possum + env_file: env + volumes: + - ..:/app + links: + - possum:possum.example diff --git a/examples/env b/examples/env new file mode 100644 index 0000000..1ce5f77 --- /dev/null +++ b/examples/env @@ -0,0 +1,5 @@ +POSSUM_URL=http://possum.example +POSSUM_ACCOUNT=example +POSSUM_LOGIN=admin +POSSUM_PASSWORD=secret +PYTHONPATH=/app diff --git a/examples/start.sh b/examples/start.sh new file mode 100755 index 0000000..a4d6fc2 --- /dev/null +++ b/examples/start.sh @@ -0,0 +1,13 @@ +#!/bin/bash -ex + +docker-compose build + +if [ ! -f data_key ]; then + echo "Generating data key" + docker-compose run --no-deps --rm possum data-key generate > data_key +fi + +export POSSUM_DATA_KEY="$(cat data_key)" + +docker-compose up -d pg possum +docker-compose run --rm api diff --git a/examples/stop.sh b/examples/stop.sh new file mode 100755 index 0000000..0ea693e --- /dev/null +++ b/examples/stop.sh @@ -0,0 +1,4 @@ +#!/bin/bash -ex + +docker-compose stop +docker-compose rm -f From 86481d8648be164cb02fd418333cc0e46c512e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Thu, 25 Aug 2016 19:24:36 +0200 Subject: [PATCH 32/62] Add secrets example --- examples/secrets.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 examples/secrets.py diff --git a/examples/secrets.py b/examples/secrets.py new file mode 100644 index 0000000..b74fdd1 --- /dev/null +++ b/examples/secrets.py @@ -0,0 +1,42 @@ +import conjur +import os +import random +import string + +possum_url = os.environ['POSSUM_URL'] +possum_account = os.environ['POSSUM_ACCOUNT'] +possum_login = os.environ['POSSUM_LOGIN'] +possum_password = os.environ['POSSUM_PASSWORD'] + +print('========================================================================='); +print('Base url :', possum_url); +print('Account :', possum_account); +print('Login :', possum_login); +print('Password :', possum_password); +print('========================================================================='); + +conjur.config.url = possum_url +conjur.config.account = possum_account + +client = conjur.new_from_password(possum_login, possum_password) + +def random_password(): + # NOTE: just for example purposes. + # Use strong crypto to generate actual passwords! + return ''.join([random.choice(string.digits + string.letters + string.punctuation) for _ in range(12)]) + +def populate_some_variables(api): + for password in [ + var for var in api.resources(kind='variable') + if 'password' in var.identifier + ]: + pwd = random_password() + print("Setting {} = {}".format(password.resourceid, pwd)) + password.add_secret(pwd) + +def print_all_vars(api): + for var in api.resources(kind='variable'): + print("{} = {}".format(var.resourceid, var.secret())) + +populate_some_variables(client) +print_all_vars(client) From ac8b3cfb6e2b5f9c32f63281e170ad8388409d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Thu, 25 Aug 2016 19:24:23 +0200 Subject: [PATCH 33/62] Return None when a secret doesn't exist --- conjur/resource.py | 10 ++++++++-- tests/resource_test.py | 13 ++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/conjur/resource.py b/conjur/resource.py index 86526c3..6db47da 100644 --- a/conjur/resource.py +++ b/conjur/resource.py @@ -120,14 +120,20 @@ def secret(self, version=None): `version` is a *one based* index of the version to be retrieved. - If no such version exists, a 404 error is raised. + If no such version exists, None is returned. Returns the value of the secret as a string. """ url = self.secret_url() if version is not None: url = "%s?version=%s" % (url, version) - return self.api.get(url).text + res = self.api.get(url, check_errors = False) + if res.status_code < 300: + return res.text + elif res.status_code == 404: + return None + else: + raise ConjurException("Request failed: %d" % res.status_code) def add_secret(self, value): """ diff --git a/tests/resource_test.py b/tests/resource_test.py index 5feb6f9..a69ae90 100644 --- a/tests/resource_test.py +++ b/tests/resource_test.py @@ -95,7 +95,18 @@ def test_get_secret_value(mock_get): resp.text = 'teh value' assert resource.secret() == 'teh value' mock_get.assert_called_with( - '%s/secrets/conjur/food/bacon' % api.config.url + '%s/secrets/conjur/food/bacon' % api.config.url, + check_errors = False + ) + +@patch.object(api, 'get') +def test_get_no_secret_value(mock_get): + mock_get.return_value = resp = Mock() + resp.status_code = 404 + assert resource.secret() == None + mock_get.assert_called_with( + '%s/secrets/conjur/food/bacon' % api.config.url, + check_errors = False ) @patch.object(api, 'post') From e8befb8fa75e35f639619adaf92edd1b824d6ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Thu, 25 Aug 2016 19:58:40 +0200 Subject: [PATCH 34/62] Add authorization example --- examples/authorization.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 examples/authorization.py diff --git a/examples/authorization.py b/examples/authorization.py new file mode 100644 index 0000000..d9ce729 --- /dev/null +++ b/examples/authorization.py @@ -0,0 +1,36 @@ +from wsgiref.simple_server import make_server +import conjur +from base64 import b64decode + +conjur.config.update( + url = "http://possum.example", + account = "example" +) + +possum_resource = 'example:webservice:prod/analytics/v1' + +def simple_app(environ, start_response): + # Usually you'd use some utility for extracting auth, but let's keep it simple + username, password = b64decode(environ['HTTP_AUTHORIZATION'].split(' ')[1]).split(':') + + try: + # can also use API key or token directly + possum = conjur.new_from_password(username, password) + except conjur.ConjurException: + start_response("401 Unauthorized", []) + return ["Unauthorized\r\n"] + + if not possum.resource_qualified(possum_resource).permitted('execute'): + start_response("403 Forbidden", []) + return ["Forbidden\r\n"] + else: + status = '200 OK' + headers = [('Content-type', 'text/plain')] + + start_response(status, headers) + + return 'You are authorized!!!!!\n' + +httpd = make_server('', 8000, simple_app) +print "Serving on port 8000..." +httpd.serve_forever() From 4a4902687a416c41a35bb93b6f4cb462544d5cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Fri, 9 Sep 2016 16:34:45 +0200 Subject: [PATCH 35/62] Add conjur.new_from_header --- conjur/__init__.py | 19 +++++++++++++++++++ tests/conjur_test.py | 8 ++++++++ 2 files changed, 27 insertions(+) diff --git a/conjur/__init__.py b/conjur/__init__.py index fba0d22..49006e8 100644 --- a/conjur/__init__.py +++ b/conjur/__init__.py @@ -17,7 +17,9 @@ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +import base64 import os +import re import requests from config import Config @@ -105,6 +107,23 @@ def new_from_token(token, configuration=None): """ return API(token=token, config=_config(configuration)) +def new_from_header(authorization_header, configuration=None): + """ + Create a `conjur.API` instance based on an Authorization header. + + This is mostly useful for proxies, authenticators and wrappers which + forward Authorization header supplied by the client. + + `authorization_header` is the Authorization header contents, + eg. `Token token=""`. + + `configuration` is a conjur.Config instance for the api. If not given, the global Config + instance (`conjur.config`) will be used. + """ + m = re.match('Token token="(.*)"', authorization_header) + token = base64.b64decode(m.group(1)) + return API(token=token, config=_config(configuration)) + __all__ = ( 'config', 'Config', 'Group', 'API', 'User', 'Host', 'Layer', 'Resource', 'Role', 'Variable', 'new_from_key', 'new_from_netrc', 'new_from_token', 'configure', 'ConjurException' diff --git a/tests/conjur_test.py b/tests/conjur_test.py index 46bb42c..b9d58d7 100644 --- a/tests/conjur_test.py +++ b/tests/conjur_test.py @@ -38,6 +38,14 @@ def test_new_from_token(): assert api.config == config +def test_new_from_header(): + api = conjur.new_from_header("Token token=\"dGhlIHRva2Vu\"") + assert api.token == "the token" + assert api.api_key is None + assert api.login is None + assert api.config == config + + def test_new_with_config(): cfg = Config() api = conjur.new_from_key("login", "secret", cfg) From 9dbdfa2ec48fa74aef18aa7581f88499d89efe32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Wed, 14 Sep 2016 16:09:29 +0200 Subject: [PATCH 36/62] Use user-provided header directly Don't try to be smart. Rely on the server to recognize malformed headers. --- conjur/__init__.py | 4 +--- conjur/api.py | 14 ++++++++++---- tests/conjur_test.py | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/conjur/__init__.py b/conjur/__init__.py index 49006e8..9362ebf 100644 --- a/conjur/__init__.py +++ b/conjur/__init__.py @@ -120,9 +120,7 @@ def new_from_header(authorization_header, configuration=None): `configuration` is a conjur.Config instance for the api. If not given, the global Config instance (`conjur.config`) will be used. """ - m = re.match('Token token="(.*)"', authorization_header) - token = base64.b64decode(m.group(1)) - return API(token=token, config=_config(configuration)) + return API(header=authorization_header, config=_config(configuration)) __all__ = ( 'config', 'Config', 'Group', 'API', 'User', 'Host', 'Layer', 'Resource', 'Role', 'Variable', diff --git a/conjur/api.py b/conjur/api.py index a41842d..c2c0ee3 100644 --- a/conjur/api.py +++ b/conjur/api.py @@ -28,7 +28,7 @@ from conjur.exceptions import ConjurException class API(object): - def __init__(self, credentials=None, token=None, config=None): + def __init__(self, credentials=None, token=None, header=None, config=None): """ Creates an API instance configured with the given credentials or token and config. @@ -49,6 +49,9 @@ def __init__(self, credentials=None, token=None, config=None): elif token: self.token = token self.login = self.api_key = None + elif header: + self.header = header + self.login = self.api_key = None else: raise TypeError("must be given a credentials or token argument") if config: @@ -93,9 +96,12 @@ def auth_header(self): Returns a string suitable for use as an `Authorization` header value. """ - token = self.authenticate() - enc = base64.b64encode(token) - return 'Token token="%s"' % enc + try: + return self.header + except AttributeError: + token = self.authenticate() + enc = base64.b64encode(token) + return 'Token token="%s"' % enc def request(self, method, url, **kwargs): """ diff --git a/tests/conjur_test.py b/tests/conjur_test.py index b9d58d7..fbbf8f9 100644 --- a/tests/conjur_test.py +++ b/tests/conjur_test.py @@ -40,7 +40,7 @@ def test_new_from_token(): def test_new_from_header(): api = conjur.new_from_header("Token token=\"dGhlIHRva2Vu\"") - assert api.token == "the token" + assert api.auth_header() == "Token token=\"dGhlIHRva2Vu\"" assert api.api_key is None assert api.login is None assert api.config == config From b3584b7188944de72f15bee6f4713de31ed573ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Wed, 14 Sep 2016 16:10:28 +0200 Subject: [PATCH 37/62] Correctly print secret keys in directory example The doc says it's a single, newline-separated string. This might be weird, but it is what it is... --- examples/directory.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/directory.py b/examples/directory.py index 1bb513e..b7b13e2 100644 --- a/examples/directory.py +++ b/examples/directory.py @@ -28,6 +28,4 @@ print("User {}".format(user.resourceid)) keys = user.role().public_keys() if len(keys): - print(" public keys:") - for key in keys: - print(" - {}".format(key)) + print(" public keys:\n{}".format(keys)) From 284314659411569db31c684ee5613d57dc9508f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Wed, 14 Sep 2016 16:11:26 +0200 Subject: [PATCH 38/62] Add authorization client example Also adjust the authorization server code to use new_from_header. --- examples/authorization.py | 14 +++----------- examples/authorization_client.py | 16 ++++++++++++++++ examples/env | 2 +- 3 files changed, 20 insertions(+), 12 deletions(-) create mode 100644 examples/authorization_client.py diff --git a/examples/authorization.py b/examples/authorization.py index d9ce729..590d90f 100644 --- a/examples/authorization.py +++ b/examples/authorization.py @@ -1,24 +1,16 @@ from wsgiref.simple_server import make_server import conjur -from base64 import b64decode conjur.config.update( url = "http://possum.example", account = "example" ) -possum_resource = 'example:webservice:prod/analytics/v1' +possum_resource = 'example:host:myapp-01' def simple_app(environ, start_response): - # Usually you'd use some utility for extracting auth, but let's keep it simple - username, password = b64decode(environ['HTTP_AUTHORIZATION'].split(' ')[1]).split(':') - - try: - # can also use API key or token directly - possum = conjur.new_from_password(username, password) - except conjur.ConjurException: - start_response("401 Unauthorized", []) - return ["Unauthorized\r\n"] + # use authorization header supplied by the client + possum = conjur.new_from_header(environ['HTTP_AUTHORIZATION']) if not possum.resource_qualified(possum_resource).permitted('execute'): start_response("403 Forbidden", []) diff --git a/examples/authorization_client.py b/examples/authorization_client.py new file mode 100644 index 0000000..05aa31b --- /dev/null +++ b/examples/authorization_client.py @@ -0,0 +1,16 @@ +import conjur +import httplib + +conjur.config.update( + url = "http://possum.example", + account = "example" +) + +conn = httplib.HTTPConnection("service.example:8000") + +possum = conjur.new_from_password('admin', 'admin') +conn.request("GET", "/", None, {"Authorization": possum.auth_header()}) + +response = conn.getresponse() +print response.status, response.reason +print response.read() diff --git a/examples/env b/examples/env index 1ce5f77..0187d43 100644 --- a/examples/env +++ b/examples/env @@ -1,5 +1,5 @@ POSSUM_URL=http://possum.example POSSUM_ACCOUNT=example POSSUM_LOGIN=admin -POSSUM_PASSWORD=secret +POSSUM_PASSWORD=admin PYTHONPATH=/app From 1ad2ab2b2a0b4cebf52b74d9d4f470284367dcd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Wed, 14 Sep 2016 16:33:50 +0200 Subject: [PATCH 39/62] Set PYTHONPATH in Dockerfile --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 9fb9773..9826ccf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,5 +6,7 @@ WORKDIR /app COPY requirements* /app/ RUN pip install -r requirements.txt -r requirements_dev.txt +ENV PYTHONPATH /app + VOLUME /app VOLUME /artifacts From 5e65698d5c15260d3eb36f9a9cbbc57c3c03ed02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Wed, 14 Sep 2016 16:34:06 +0200 Subject: [PATCH 40/62] Add readme for the examples --- examples/README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 examples/README.md diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..b9fdb1a --- /dev/null +++ b/examples/README.md @@ -0,0 +1,35 @@ +# Python API examples + +```sh-session +host$ cd possum/dev +host$ ./start.sh +possum$ possum db migrate +possum$ possum policy load example run/policy.yml +possum$ possum server -p 80 +host$ docker build -t python-api:sandbox +host$ docker run --rm -it --name python-server --link possumdev_possum_1:possum.example api-python:sandbox bash +python-server$ cd examples; python authorization.py +host$ docker run --rm -it --name python-examples --link possumdev_possum_1:possum.example --link python-server:service.example api-python:sandbox bash +python-examples$ cd examples +python-examples$ export `cat env` +python-examples$ python directory.py +python-examples$ python secrets.py +python-examples$ python authorization_client.py +``` + +## directory.py + +Logs in, lists groups and their members, then users and their public keys. + +## secrets.py + +Logs in, sets secrets on all resources with kind 'variable' and id like +'*password*' to a random value, then lists all the secrets. + +## authorization.py + +Starts a web server authorizing with possum resource. + +## authorization_client.py + +Client for the above webservice. From c58edab2d8f2d9cbb1add2647e59df0f66ca9528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Thu, 15 Sep 2016 16:19:42 +0200 Subject: [PATCH 41/62] Fix README to use docker-compose --- examples/README.md | 93 +++++++++++++++++++++++++------- examples/authorization.py | 2 +- examples/authorization_client.py | 4 +- examples/env | 2 +- 4 files changed, 78 insertions(+), 23 deletions(-) diff --git a/examples/README.md b/examples/README.md index b9fdb1a..2010326 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,35 +1,90 @@ # Python API examples +These examples show how to use the Python API. + +A docker-compose environment is included for ease of use. Note it needs +[conjurinc/possum-example](https://github.com/conjurinc/possum-example) +and [conjurinc/possum](https://github.com/conjurinc/possum) images available +in docker repository. + +To start, use `./start.sh`: + ```sh-session -host$ cd possum/dev -host$ ./start.sh -possum$ possum db migrate -possum$ possum policy load example run/policy.yml -possum$ possum server -p 80 -host$ docker build -t python-api:sandbox -host$ docker run --rm -it --name python-server --link possumdev_possum_1:possum.example api-python:sandbox bash -python-server$ cd examples; python authorization.py -host$ docker run --rm -it --name python-examples --link possumdev_possum_1:possum.example --link python-server:service.example api-python:sandbox bash -python-examples$ cd examples -python-examples$ export `cat env` -python-examples$ python directory.py -python-examples$ python secrets.py -python-examples$ python authorization_client.py +$ ./start.sh ++ docker-compose build +pg uses an image, skipping +example uses an image, skipping +possum uses an image, skipping +Building api +[...] ++ POSSUM_DATA_KEY=40jLjbr3O2n1Z//MgU6G3SzFVjuO/fv6zQkeyzu1sxU= ++ docker-compose up -d pg possum +Creating examples_example_1 +Creating examples_pg_1 +Creating examples_possum_1 ++ docker-compose run --rm api +Starting examples_example_1 +root@0d85ff261d5d:/app# ``` ## directory.py -Logs in, lists groups and their members, then users and their public keys. +Logs in, lists groups and their members, then users and their public keys: + +```sh-session +# python examples/directory.py +========================================================================= +('Base url :', 'http://possum.example') +('Account :', 'example') +('Login :', 'admin') +('Password :', 'secret') +========================================================================= +Group example:group:security_admin; members: + - example:user:admin +Group example:group:field-admin; members: + - example:group:security_admin + - example:user:kyle.wheeler + - example:user:marin.dubois +[...] +User example:user:kyle.wheeler +User example:user:marin.dubois +User example:user:carol.rodriquez +[...] +``` ## secrets.py Logs in, sets secrets on all resources with kind 'variable' and id like '*password*' to a random value, then lists all the secrets. -## authorization.py +```sh-session +# python examples/secrets.py +========================================================================= +('Base url :', 'http://possum.example') +('Account :', 'example') +('Login :', 'admin') +('Password :', 'secret') +========================================================================= +Setting example:variable:prod/analytics/v1/redshift/master_user_password = jdPa8)sOW#XM +Setting example:variable:prod/frontend/v1/mongo/password = ,xV:An%3cmSE +Setting example:variable:prod/user-database/v1/postgres/master_user_password = (Y`{5(y3uRUK +[...] +example:variable:prod/user-database/v1/postgres/master_user_name = None +example:variable:prod/user-database/v1/postgres/master_user_password = (Y`{5(y3uRUK +example:variable:prod/user-database/v1/postgres/database_name = None +example:variable:prod/user-database/v1/postgres/database_url = None +``` -Starts a web server authorizing with possum resource. +## authorization.py and authorization_client.py -## authorization_client.py +A web server authorizing with possum resource: -Client for the above webservice. +```sh-session +# python examples/authorization.py & +[1] 14 +Serving on port 8000... +# python examples/authorization_client.py +127.0.0.1 - - [15/Sep/2016 14:19:13] "GET / HTTP/1.1" 200 24 +200 OK +You are authorized!!!!! +``` diff --git a/examples/authorization.py b/examples/authorization.py index 590d90f..4112988 100644 --- a/examples/authorization.py +++ b/examples/authorization.py @@ -6,7 +6,7 @@ account = "example" ) -possum_resource = 'example:host:myapp-01' +possum_resource = 'example:webservice:prod/analytics/v1' def simple_app(environ, start_response): # use authorization header supplied by the client diff --git a/examples/authorization_client.py b/examples/authorization_client.py index 05aa31b..c28a00b 100644 --- a/examples/authorization_client.py +++ b/examples/authorization_client.py @@ -6,9 +6,9 @@ account = "example" ) -conn = httplib.HTTPConnection("service.example:8000") +conn = httplib.HTTPConnection("localhost:8000") -possum = conjur.new_from_password('admin', 'admin') +possum = conjur.new_from_password('admin', 'secret') conn.request("GET", "/", None, {"Authorization": possum.auth_header()}) response = conn.getresponse() diff --git a/examples/env b/examples/env index 0187d43..1ce5f77 100644 --- a/examples/env +++ b/examples/env @@ -1,5 +1,5 @@ POSSUM_URL=http://possum.example POSSUM_ACCOUNT=example POSSUM_LOGIN=admin -POSSUM_PASSWORD=admin +POSSUM_PASSWORD=secret PYTHONPATH=/app From d6f96787651106bd686d8620f2cbb3e16f1ce6d7 Mon Sep 17 00:00:00 2001 From: Kevin Gilpin Date: Thu, 15 Sep 2016 10:34:01 -0400 Subject: [PATCH 42/62] ignore data_key --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 20d1a76..a02afe6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +data_key artifacts pytest.xml htmlcov From b9901e0445c40a79fd19b150642c9a3f283cae9b Mon Sep 17 00:00:00 2001 From: Kevin Gilpin Date: Thu, 15 Sep 2016 10:34:09 -0400 Subject: [PATCH 43/62] add to example README a bit --- examples/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/examples/README.md b/examples/README.md index 2010326..876f4d2 100644 --- a/examples/README.md +++ b/examples/README.md @@ -88,3 +88,12 @@ Serving on port 8000... 200 OK You are authorized!!!!! ``` + +In the Possum server log, you'll see the authorization check: + +``` +possum_1 | Started GET "/resources/example/webservice/prod/analytics/v1?privilege=execute&check=true" for 172.17.0.7 at 2016-09-15 14:32:55 +0000 +possum_1 | Processing by ResourcesController#check_permission as */* +possum_1 | Parameters: {"privilege"=>"execute", "check"=>"true", "account"=>"example", "kind"=>"webservice", "identifier"=>"prod/analytics/v1"} +possum_1 | Completed 204 No Content in 3ms +``` From fffa7ecde7d6fb7c26b86425b8fd94aaeeea0f6c Mon Sep 17 00:00:00 2001 From: Dustin Collins Date: Thu, 22 Sep 2016 15:43:43 -0500 Subject: [PATCH 44/62] CON-3437 - Flask app example --- examples/flaskapp/README.md | 53 +++++++++++++++++++++++++++ examples/flaskapp/app.py | 23 ++++++++++++ examples/flaskapp/docker-compose.yml | 18 +++++++++ examples/flaskapp/load_secrets.py | 19 ++++++++++ examples/flaskapp/policy.yml | 6 +++ examples/flaskapp/requirements.txt | 3 ++ examples/flaskapp/start.sh | 12 ++++++ examples/flaskapp/stop.sh | 4 ++ examples/flaskapp/templates/home.html | 14 +++++++ 9 files changed, 152 insertions(+) create mode 100644 examples/flaskapp/README.md create mode 100644 examples/flaskapp/app.py create mode 100644 examples/flaskapp/docker-compose.yml create mode 100644 examples/flaskapp/load_secrets.py create mode 100644 examples/flaskapp/policy.yml create mode 100644 examples/flaskapp/requirements.txt create mode 100755 examples/flaskapp/start.sh create mode 100755 examples/flaskapp/stop.sh create mode 100644 examples/flaskapp/templates/home.html diff --git a/examples/flaskapp/README.md b/examples/flaskapp/README.md new file mode 100644 index 0000000..f1b9e95 --- /dev/null +++ b/examples/flaskapp/README.md @@ -0,0 +1,53 @@ +# Example of using Possum with Flask + +Example project showing how to use the [Conjur API client](https://pypi.python.org/pypi/Conjur) to +fetch secrets from Possum and use them in a Flask web application. + +## Requirements + +* Docker and docker-compose +* Python 2.7+ and pip + +## Example + +First, launch a local Possum instance: + +``` +./start.sh +``` + +Possum has now loaded [policy.yml](policy.yml) and is running and listening on +local port `3030`. For this example, the `admin` user's password is +`secret`. + +Now that Possum is running, run the Flask app from this directory: + +``` +pip install -r requirements.txt +python app.py +``` + +The Flask web app is now listening on port `8080`. +Open [localhost:8080](http://localhost:8080) in your browser. +Notice that the secrets have no value yet. + +![secrets with no values](https://i.imgur.com/5blnU8O.png) + +Load new secrets with: + +``` +python load_secrets.py +``` + +Reload the page at `localhost:8080` to see that the secret values +have now been fetched and are displayed in the browser. + +![secrets with values](https://i.imgur.com/WRrS8Ih.png) + +Secrets have been fetched from Possum and passed through to the Flask template. + +To stop the possum server run: + +``` +./stop.sh +``` diff --git a/examples/flaskapp/app.py b/examples/flaskapp/app.py new file mode 100644 index 0000000..7ad517a --- /dev/null +++ b/examples/flaskapp/app.py @@ -0,0 +1,23 @@ +from flask import Flask, render_template + +import conjur + +app = Flask(__name__) + +conjur.config.url = 'http://localhost:3030' +conjur.config.account = 'example' + +api = conjur.new_from_password('admin', 'secret') # TODO: swap this out with host creds + + +@app.route('/') +def home(): + secrets = [ + api.resource('variable', 'dbpassword'), + api.resource('variable', 'aws_access_key_id'), + api.resource('variable', 'aws_secret_access_key'), + ] + return render_template('home.html', secrets=secrets) + +if __name__ == '__main__': + app.run('0.0.0.0', 8080) diff --git a/examples/flaskapp/docker-compose.yml b/examples/flaskapp/docker-compose.yml new file mode 100644 index 0000000..18662b8 --- /dev/null +++ b/examples/flaskapp/docker-compose.yml @@ -0,0 +1,18 @@ +version: '2' +services: + db: + image: postgres:9.3 + + possum: + image: conjurinc/possum + ports: + - "3030:80" + command: server -a example -f /src/policy.yml + volumes: + - .:/src + environment: + DATABASE_URL: postgres://postgres@db/postgres + POSSUM_ADMIN_PASSWORD: secret + POSSUM_DATA_KEY: + depends_on: + - db diff --git a/examples/flaskapp/load_secrets.py b/examples/flaskapp/load_secrets.py new file mode 100644 index 0000000..776917c --- /dev/null +++ b/examples/flaskapp/load_secrets.py @@ -0,0 +1,19 @@ +import random +import string + +import conjur + + +def random_value(): + return ''.join(random.choice(string.ascii_lowercase) for _ in range(20)) + +conjur.config.url = 'http://localhost:3030' +conjur.config.account = 'example' + +api = conjur.new_from_password('admin', 'secret') + + +# Set random values for secrets +api.resource('variable', 'dbpassword').add_secret(random_value()) +api.resource('variable', 'aws_access_key_id').add_secret(random_value()) +api.resource('variable', 'aws_secret_access_key').add_secret(random_value()) diff --git a/examples/flaskapp/policy.yml b/examples/flaskapp/policy.yml new file mode 100644 index 0000000..2f72ec4 --- /dev/null +++ b/examples/flaskapp/policy.yml @@ -0,0 +1,6 @@ +--- +- !variable dbpassword +- !variable aws_access_key_id +- !variable aws_secret_access_key + +- !host pythonapp diff --git a/examples/flaskapp/requirements.txt b/examples/flaskapp/requirements.txt new file mode 100644 index 0000000..7129d41 --- /dev/null +++ b/examples/flaskapp/requirements.txt @@ -0,0 +1,3 @@ +Flask + +-e git+git://github.com/conjurinc/api-python.git@work/possum#egg=conjur diff --git a/examples/flaskapp/start.sh b/examples/flaskapp/start.sh new file mode 100755 index 0000000..65e8fdb --- /dev/null +++ b/examples/flaskapp/start.sh @@ -0,0 +1,12 @@ +#!/bin/bash -e + +docker-compose build + +if [ ! -f data_key ]; then + echo "Generating data key" + docker-compose run --no-deps --rm possum data-key generate > data_key +fi + +export POSSUM_DATA_KEY="$(cat data_key)" + +docker-compose up -d diff --git a/examples/flaskapp/stop.sh b/examples/flaskapp/stop.sh new file mode 100755 index 0000000..f9a246a --- /dev/null +++ b/examples/flaskapp/stop.sh @@ -0,0 +1,4 @@ +#!/bin/bash -e + +docker-compose stop +docker-compose rm -f diff --git a/examples/flaskapp/templates/home.html b/examples/flaskapp/templates/home.html new file mode 100644 index 0000000..d4f2d1f --- /dev/null +++ b/examples/flaskapp/templates/home.html @@ -0,0 +1,14 @@ + + + Flask Possum Example + + +

Secrets

+ +
    + {% for secret in secrets %} +
  • {{secret.identifier}} = {{secret.secret() }}
  • + {% endfor %} +
+ + From 61b227b13bb4eea69040d5c74accd0b69b2f60d9 Mon Sep 17 00:00:00 2001 From: Dustin Collins Date: Fri, 23 Sep 2016 12:09:01 -0500 Subject: [PATCH 45/62] Start on pet store example [CON-3437] --- examples/flaskapp/README.md | 42 +++++++++++------- examples/flaskapp/app.py | 64 +++++++++++++++++++++++---- examples/flaskapp/docker-compose.yml | 13 ++++-- examples/flaskapp/init-user-db.sh | 7 +++ examples/flaskapp/load_secrets.py | 4 +- examples/flaskapp/policy.yml | 4 -- examples/flaskapp/requirements.txt | 2 + examples/flaskapp/start.sh | 4 ++ examples/flaskapp/stop.sh | 3 +- examples/flaskapp/templates/home.html | 33 +++++++++++--- 10 files changed, 132 insertions(+), 44 deletions(-) create mode 100755 examples/flaskapp/init-user-db.sh diff --git a/examples/flaskapp/README.md b/examples/flaskapp/README.md index f1b9e95..44b5258 100644 --- a/examples/flaskapp/README.md +++ b/examples/flaskapp/README.md @@ -1,7 +1,20 @@ -# Example of using Possum with Flask +# Pet Store - Using Possum with Python's Flask web framework -Example project showing how to use the [Conjur API client](https://pypi.python.org/pypi/Conjur) to -fetch secrets from Possum and use them in a Flask web application. +This example project illustrates how to: + +* Fetch a secret (database password) using the [Conjur Python API client](https://pypi.python.org/pypi/Conjur) +* Authorize traffic from other clients that want to call this web service [TODO] + +The scenario for this example: + +> A local pet store wants to be able to display all pets that they have available to the general public. +Pet info is fetched from a PostgreSQL database and displayed on a website page. The pet store wants to be able +to add and remove pets from their inventory securely. Only their employees should be able to add and remove pets. +The pet store also has an inventory manager, another web service that can be used to add or remove pets as needed. +When the `pet store` service receives a request from `employees` or the `inventory manager`, it authenticates and authorizes the +request with Possum. + +![Pet store diagram](http://i.imgur.com/HLSO2VB.png) ## Requirements @@ -10,7 +23,7 @@ fetch secrets from Possum and use them in a Flask web application. ## Example -First, launch a local Possum instance: +First, set up your environment. ``` ./start.sh @@ -20,6 +33,10 @@ Possum has now loaded [policy.yml](policy.yml) and is running and listening on local port `3030`. For this example, the `admin` user's password is `secret`. +The `petstore` database has also been created, with user `petstore`. +The database user's password has been loaded into the `dbpassword` variable +in Possum with [load_secrets.py](load_secrets.py). + Now that Possum is running, run the Flask app from this directory: ``` @@ -29,22 +46,15 @@ python app.py The Flask web app is now listening on port `8080`. Open [localhost:8080](http://localhost:8080) in your browser. -Notice that the secrets have no value yet. +You will notice that there are no pets displayed. -![secrets with no values](https://i.imgur.com/5blnU8O.png) - -Load new secrets with: - -``` -python load_secrets.py -``` +Pets can be added and removed by using the pet store's API. -Reload the page at `localhost:8080` to see that the secret values -have now been fetched and are displayed in the browser. +* add pet: `POST` `/api/pets`, JSON body with `name` and `type` fields +* remove pet: `DELETE` `/api/pets/` with pet ID -![secrets with values](https://i.imgur.com/WRrS8Ih.png) +TODO: add section on traffic auth for users and hosts -Secrets have been fetched from Possum and passed through to the Flask template. To stop the possum server run: diff --git a/examples/flaskapp/app.py b/examples/flaskapp/app.py index 7ad517a..e00c79c 100644 --- a/examples/flaskapp/app.py +++ b/examples/flaskapp/app.py @@ -1,4 +1,5 @@ -from flask import Flask, render_template +from flask import Flask, render_template, jsonify, request +from flask_sqlalchemy import SQLAlchemy import conjur @@ -6,18 +7,63 @@ conjur.config.url = 'http://localhost:3030' conjur.config.account = 'example' - api = conjur.new_from_password('admin', 'secret') # TODO: swap this out with host creds +app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://petstore:{}@localhost/petstore'.format( + api.resource('variable', 'dbpassword').secret() +) +db = SQLAlchemy(app) + + +class Pets(db.Model): + __tablename__ = 'pets' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(80)) + type = db.Column(db.String(80), index=True) + + def __init__(self, name, type): + self.name = name + self.type = type + + def __repr__(self): + return ''.format(self.name, self.type) + +db.create_all() + @app.route('/') def home(): - secrets = [ - api.resource('variable', 'dbpassword'), - api.resource('variable', 'aws_access_key_id'), - api.resource('variable', 'aws_secret_access_key'), - ] - return render_template('home.html', secrets=secrets) + return render_template('home.html', pets=Pets.query.all()) + + +# API routes + +@app.route('/api/pets', methods=['POST']) +def add_pet(): + json = request.get_json(force=True) + valid = json.has_key('name') and json.has_key('type') + + if not valid: + return jsonify({'ok': False, 'id': None, 'msg': "'name' or 'type' missing from JSON body"}), 400 + + pet = Pets(json['name'], json['type']) + db.session.add(pet) + db.session.commit() + + return jsonify({'ok': True, 'id': pet.id}), 201 + + +@app.route('/api/pets/', methods=['DELETE']) +def remove_pet(id): + pet = Pets.query.filter_by(id=id).first() + + if pet is None: + return jsonify({'ok': False, 'id': id, 'msg': 'Pet ID {} not found'.format(id)}), 404 + + db.session.delete(pet) + db.session.commit() + + return jsonify({'ok': True, 'id': id}) if __name__ == '__main__': - app.run('0.0.0.0', 8080) + app.run('0.0.0.0', 8080, debug=True) diff --git a/examples/flaskapp/docker-compose.yml b/examples/flaskapp/docker-compose.yml index 18662b8..8e86465 100644 --- a/examples/flaskapp/docker-compose.yml +++ b/examples/flaskapp/docker-compose.yml @@ -1,7 +1,11 @@ version: '2' services: - db: + appdb: image: postgres:9.3 + ports: + - "5432:5432" + volumes: + - ./init-user-db.sh:/docker-entrypoint-initdb.d/init-user-db.sh possum: image: conjurinc/possum @@ -11,8 +15,11 @@ services: volumes: - .:/src environment: - DATABASE_URL: postgres://postgres@db/postgres + DATABASE_URL: postgres://postgres@possumdb/postgres POSSUM_ADMIN_PASSWORD: secret POSSUM_DATA_KEY: depends_on: - - db + - possumdb + + possumdb: + image: postgres:9.3 diff --git a/examples/flaskapp/init-user-db.sh b/examples/flaskapp/init-user-db.sh new file mode 100755 index 0000000..20b81af --- /dev/null +++ b/examples/flaskapp/init-user-db.sh @@ -0,0 +1,7 @@ +#!/bin/bash -e + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL + CREATE USER petstore WITH PASSWORD 'w^kftUagHmF2Ahph'; + CREATE DATABASE petstore; + GRANT ALL PRIVILEGES ON DATABASE petstore TO petstore; +EOSQL diff --git a/examples/flaskapp/load_secrets.py b/examples/flaskapp/load_secrets.py index 776917c..4fb3857 100644 --- a/examples/flaskapp/load_secrets.py +++ b/examples/flaskapp/load_secrets.py @@ -14,6 +14,4 @@ def random_value(): # Set random values for secrets -api.resource('variable', 'dbpassword').add_secret(random_value()) -api.resource('variable', 'aws_access_key_id').add_secret(random_value()) -api.resource('variable', 'aws_secret_access_key').add_secret(random_value()) +api.resource('variable', 'dbpassword').add_secret('w^kftUagHmF2Ahph') diff --git a/examples/flaskapp/policy.yml b/examples/flaskapp/policy.yml index 2f72ec4..e700376 100644 --- a/examples/flaskapp/policy.yml +++ b/examples/flaskapp/policy.yml @@ -1,6 +1,2 @@ --- - !variable dbpassword -- !variable aws_access_key_id -- !variable aws_secret_access_key - -- !host pythonapp diff --git a/examples/flaskapp/requirements.txt b/examples/flaskapp/requirements.txt index 7129d41..82f9757 100644 --- a/examples/flaskapp/requirements.txt +++ b/examples/flaskapp/requirements.txt @@ -1,3 +1,5 @@ Flask +psycopg2 +Flask-SQLAlchemy -e git+git://github.com/conjurinc/api-python.git@work/possum#egg=conjur diff --git a/examples/flaskapp/start.sh b/examples/flaskapp/start.sh index 65e8fdb..2a4d917 100755 --- a/examples/flaskapp/start.sh +++ b/examples/flaskapp/start.sh @@ -10,3 +10,7 @@ fi export POSSUM_DATA_KEY="$(cat data_key)" docker-compose up -d + +sleep 20 # a better way to do this? + +python load_secrets.py diff --git a/examples/flaskapp/stop.sh b/examples/flaskapp/stop.sh index f9a246a..9a8718d 100755 --- a/examples/flaskapp/stop.sh +++ b/examples/flaskapp/stop.sh @@ -1,4 +1,3 @@ #!/bin/bash -e -docker-compose stop -docker-compose rm -f +docker-compose down -v diff --git a/examples/flaskapp/templates/home.html b/examples/flaskapp/templates/home.html index d4f2d1f..d6c7125 100644 --- a/examples/flaskapp/templates/home.html +++ b/examples/flaskapp/templates/home.html @@ -1,14 +1,33 @@ - Flask Possum Example + Pets + -

Secrets

+
+

Pets

-
    - {% for secret in secrets %} -
  • {{secret.identifier}} = {{secret.secret() }}
  • - {% endfor %} -
+ + + + + + + + + + + {% for pet in pets %} + + + + + + + {% endfor %} + +
IDNameType
{{pet.id}}{{pet.name}}{{pet.type}}x
+
+ From b4dddc121df08d112a7d705309540ebde95fa5ef Mon Sep 17 00:00:00 2001 From: Dustin Collins Date: Fri, 23 Sep 2016 15:21:45 -0500 Subject: [PATCH 46/62] Finish petstore example [CON-3437] --- examples/flaskapp/README.md | 62 ++++++++++++++++++++++++-- examples/flaskapp/app.py | 26 +++++++++++ examples/flaskapp/employee.py | 30 +++++++++++++ examples/flaskapp/inventory_manager.py | 28 ++++++++++++ examples/flaskapp/nonemployee.py | 13 ++++++ examples/flaskapp/policy.yml | 32 ++++++++++++- examples/flaskapp/start.sh | 2 +- examples/flaskapp/templates/home.html | 2 - 8 files changed, 187 insertions(+), 8 deletions(-) create mode 100644 examples/flaskapp/employee.py create mode 100644 examples/flaskapp/inventory_manager.py create mode 100644 examples/flaskapp/nonemployee.py diff --git a/examples/flaskapp/README.md b/examples/flaskapp/README.md index 44b5258..64258f0 100644 --- a/examples/flaskapp/README.md +++ b/examples/flaskapp/README.md @@ -3,13 +3,13 @@ This example project illustrates how to: * Fetch a secret (database password) using the [Conjur Python API client](https://pypi.python.org/pypi/Conjur) -* Authorize traffic from other clients that want to call this web service [TODO] +* Authorize traffic from other clients that want to call this web service The scenario for this example: > A local pet store wants to be able to display all pets that they have available to the general public. Pet info is fetched from a PostgreSQL database and displayed on a website page. The pet store wants to be able -to add and remove pets from their inventory securely. Only their employees should be able to add and remove pets. +to add and remove pets from their inventory securely. Only their employees should be able to add pets. The pet store also has an inventory manager, another web service that can be used to add or remove pets as needed. When the `pet store` service receives a request from `employees` or the `inventory manager`, it authenticates and authorizes the request with Possum. @@ -48,16 +48,70 @@ The Flask web app is now listening on port `8080`. Open [localhost:8080](http://localhost:8080) in your browser. You will notice that there are no pets displayed. +## Policy + +[policy.yml](policy.yml) is loaded when Possum starts up. The policy defines: + +* variable `dbpassword` - Variable resource holds, the `petstore` database password +* webservice `petstore_ws` - Webservice resource fpr the `petstore` app +* host `petstore` - Host role for the `petstore` app +* host `inventory_manager` - Host role for the `inventory_manager` app +* group `employees` - Group role with three users + +The host `petstore` is granted `execute` (read) access to the `dbpassword` variable. `petstore` fetches this password from Possum when starting up. +`inventory_manager` is granted `add_pet` and `remove_pet` privileges on `petstore_ws`. +Members of group `employees` are granted `add_pet` permission on the `petstore` webservice. Removing pets is a more privileged operation than adding them. + Pets can be added and removed by using the pet store's API. * add pet: `POST` `/api/pets`, JSON body with `name` and `type` fields * remove pet: `DELETE` `/api/pets/` with pet ID -TODO: add section on traffic auth for users and hosts +The add and remove views are protected with the `validate_privilege` decorator +in [app.py](app.py). A user or machine must pass an `Authorization` header +when calling the `petstore` webservice. `petstore` consults Possum to ensure +that the caller has `update` privilege on the `petstore` webservice defined +in [policy.yml](policy.yml). If so, the request proceeds. If not, an error +message is returned. + +## Demo + +Simulate calling the `petstore` webservice as different identities: + +```sh-session +# non-employee +$ python nonemployee.py +Adding pet +401: {u'msg': u'Authorization header missing', u'ok': False} + +# employee +$ python employee.py +Adding pet +201: {u'ok': True, u'id': 7} +Removing pet +401: {u'msg': u'Not authorized', u'ok': False} + +# inventory_manager +$ python inventory_manager.py +Adding pet +201: {u'ok': True, u'id': 8} +Removing pet +200: {u'ok': True, u'id': u'8'} +``` +As expected, non-employees cannot update the pet inventory at all. Users in group +`employees` can add pets, but not remove them. Finally, the `inventory_manager` service +can add and remove pets. -To stop the possum server run: +To stop the possum local environment run: ``` ./stop.sh ``` + +## Conclusion + +In this example, we simulated a pet store system where anyone can view the +pets available but only trusted people and machines are allowed to manage +pet inventory. Different tiers of access are easily defined and implemented +using Possum's YAML policy. diff --git a/examples/flaskapp/app.py b/examples/flaskapp/app.py index e00c79c..aaae6f1 100644 --- a/examples/flaskapp/app.py +++ b/examples/flaskapp/app.py @@ -1,3 +1,5 @@ +from functools import wraps + from flask import Flask, render_template, jsonify, request from flask_sqlalchemy import SQLAlchemy @@ -31,6 +33,28 @@ def __repr__(self): db.create_all() +# This decorater validates that the user/host calling the route has privilege to do so +# Arguments +# - resource: Kind and ID of a Possum resource, separated by : - example 'variable:dbpassword' +# - privilege: The privilege the callers needs on the resource to be allowed to call the route +def validate_privilege(resource, privilege): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + auth_token = request.headers.get('AUTHORIZATION') + if auth_token is None: + return jsonify({'ok': False, 'msg': 'Authorization header missing'}), 401 + + _api = conjur.new_from_header(auth_token) + kind, identifier = resource.split(':') + if not _api.resource(kind=kind, identifier=identifier).permitted(privilege): + return jsonify({'ok': False, 'msg': 'Not authorized'}), 403 + + return f(*args, **kwargs) + return decorated_function + return decorator + + @app.route('/') def home(): return render_template('home.html', pets=Pets.query.all()) @@ -39,6 +63,7 @@ def home(): # API routes @app.route('/api/pets', methods=['POST']) +@validate_privilege('webservice:petstore', 'add_pet') def add_pet(): json = request.get_json(force=True) valid = json.has_key('name') and json.has_key('type') @@ -54,6 +79,7 @@ def add_pet(): @app.route('/api/pets/', methods=['DELETE']) +@validate_privilege('webservice:petstore', 'remove_pet') def remove_pet(id): pet = Pets.query.filter_by(id=id).first() diff --git a/examples/flaskapp/employee.py b/examples/flaskapp/employee.py new file mode 100644 index 0000000..63ac568 --- /dev/null +++ b/examples/flaskapp/employee.py @@ -0,0 +1,30 @@ +# Simulates an employee trying to add a pet to the pet store +from pprint import pprint + +import requests + +import conjur + +PETSTORE_URL = 'http://localhost:8080' + +conjur.config.url = 'http://localhost:3030' +conjur.config.account = 'example' + +api = conjur.new_from_password('admin', 'secret') # TODO: swap this with creds of an employee + +print 'Adding pet' +response = requests.post( + '{}/api/pets'.format(PETSTORE_URL), + json={'name': 'Clarence', 'type': 'Fur Seal'}, + headers={'Authorization': api.auth_header()} +) + +print '{}: {}'.format(response.status_code, response.json()) + +print 'Removing pet' +response = requests.delete( + '{}/api/pets/{}'.format(PETSTORE_URL, response.json()['id']), + headers={'Authorization': api.auth_header()} +) + +print '{}: {}'.format(response.status_code, response.json()) diff --git a/examples/flaskapp/inventory_manager.py b/examples/flaskapp/inventory_manager.py new file mode 100644 index 0000000..20b14fe --- /dev/null +++ b/examples/flaskapp/inventory_manager.py @@ -0,0 +1,28 @@ +# Simulates webservice inventory-manager trying to add a pet to the pet store + +import requests + +import conjur + +PETSTORE_URL = 'http://localhost:8080' + +conjur.config.url = 'http://localhost:3030' +conjur.config.account = 'example' + +api = conjur.new_from_password('admin', 'secret') # TODO: swap this with inventory_manager creds + +response = requests.post( + '{}/api/pets'.format(PETSTORE_URL), + json={'name': 'Spot', 'type': 'Beagle'}, + headers={'Authorization': api.auth_header()} +) + +print '{}: {}'.format(response.status_code, response.json()) + +print 'Removing pet' +response = requests.delete( + '{}/api/pets/{}'.format(PETSTORE_URL, response.json()['id']), + headers={'Authorization': api.auth_header()} +) + +print '{}: {}'.format(response.status_code, response.json()) \ No newline at end of file diff --git a/examples/flaskapp/nonemployee.py b/examples/flaskapp/nonemployee.py new file mode 100644 index 0000000..b8a18ec --- /dev/null +++ b/examples/flaskapp/nonemployee.py @@ -0,0 +1,13 @@ +# Simulates Kate, a non-employee trying to add a pet to the pet store + +import requests + +PETSTORE_URL = 'http://localhost:8080' + +print 'Adding pet' +response = requests.post( + '{}/api/pets'.format(PETSTORE_URL), + json={'name': 'Johnny', 'type': 'Gibbon'} +) + +print '{}: {}'.format(response.status_code, response.json()) diff --git a/examples/flaskapp/policy.yml b/examples/flaskapp/policy.yml index e700376..1d9942e 100644 --- a/examples/flaskapp/policy.yml +++ b/examples/flaskapp/policy.yml @@ -1,2 +1,32 @@ --- -- !variable dbpassword +- !variable &dbpassword dbpassword + +- !webservice &petstore_ws petstore_ws +- !host &petstore_host petstore_host +- !host &inventory_manager inventory_manager + +- !group &employees_group employees + +- &employees + - !user dan + - !user lisa + - !user jamal + +- !grant + role: *employees_group + members: *employees + +- !permit + role: *employees + resource: *petstore_ws + privileges: [add_pet] + +- !permit + role: *inventory_manager + resource: *petstore_ws + privileges: [add_pet, remove_pet] + +- !permit + role: *petstore_host + resource: *dbpassword + privileges: [execute] diff --git a/examples/flaskapp/start.sh b/examples/flaskapp/start.sh index 2a4d917..ca02784 100755 --- a/examples/flaskapp/start.sh +++ b/examples/flaskapp/start.sh @@ -11,6 +11,6 @@ export POSSUM_DATA_KEY="$(cat data_key)" docker-compose up -d -sleep 20 # a better way to do this? +sleep 15 # a better way to do this? python load_secrets.py diff --git a/examples/flaskapp/templates/home.html b/examples/flaskapp/templates/home.html index d6c7125..a4d9ccc 100644 --- a/examples/flaskapp/templates/home.html +++ b/examples/flaskapp/templates/home.html @@ -13,7 +13,6 @@

Pets

ID Name Type - @@ -22,7 +21,6 @@

Pets

{{pet.id}} {{pet.name}} {{pet.type}} - x {% endfor %} From c13635bc000e5cfa4f08fa5f4ecd5fa2d70a2eb7 Mon Sep 17 00:00:00 2001 From: Dustin Collins Date: Fri, 23 Sep 2016 16:03:19 -0500 Subject: [PATCH 47/62] Simulate users with different permissions [CON-3437] --- conjur/role.py | 9 +++++++++ examples/flaskapp/README.md | 2 +- examples/flaskapp/employee.py | 12 +++++++++--- examples/flaskapp/inventory_manager.py | 8 +++++++- examples/flaskapp/load_secrets.py | 6 ++++-- examples/flaskapp/requirements.txt | 2 +- 6 files changed, 31 insertions(+), 8 deletions(-) diff --git a/conjur/role.py b/conjur/role.py index 0995f84..3953a8a 100644 --- a/conjur/role.py +++ b/conjur/role.py @@ -156,3 +156,12 @@ def resource(self): returned object may refer to a nonexistent resource. """ return self.api.resource_qualified(self.roleid) + + def rotate_api_key(self): + """ + Rotates the API key of a role + The calling role must either be the target role itself or have 'update' privilege on the target role + """ + return self.api.put( + '{}/authn/{}/api_key?role={}:{}'.format(self.api.config.url, self.account, self.kind, self.identifier) + ).text.strip() diff --git a/examples/flaskapp/README.md b/examples/flaskapp/README.md index 64258f0..a99c5e7 100644 --- a/examples/flaskapp/README.md +++ b/examples/flaskapp/README.md @@ -89,7 +89,7 @@ $ python employee.py Adding pet 201: {u'ok': True, u'id': 7} Removing pet -401: {u'msg': u'Not authorized', u'ok': False} +403: {u'msg': u'Not authorized', u'ok': False} # inventory_manager $ python inventory_manager.py diff --git a/examples/flaskapp/employee.py b/examples/flaskapp/employee.py index 63ac568..c8fb7a5 100644 --- a/examples/flaskapp/employee.py +++ b/examples/flaskapp/employee.py @@ -1,8 +1,10 @@ -# Simulates an employee trying to add a pet to the pet store -from pprint import pprint +# Simulates an employee trying to add and remove a pet + +import sys import requests +sys.path.append('../..') import conjur PETSTORE_URL = 'http://localhost:8080' @@ -10,7 +12,11 @@ conjur.config.url = 'http://localhost:3030' conjur.config.account = 'example' -api = conjur.new_from_password('admin', 'secret') # TODO: swap this with creds of an employee +api = conjur.new_from_password('admin', 'secret') +key = api.role('user', 'dan').rotate_api_key() + +api = conjur.new_from_key('dan', key) + print 'Adding pet' response = requests.post( diff --git a/examples/flaskapp/inventory_manager.py b/examples/flaskapp/inventory_manager.py index 20b14fe..ddb7e23 100644 --- a/examples/flaskapp/inventory_manager.py +++ b/examples/flaskapp/inventory_manager.py @@ -1,7 +1,10 @@ -# Simulates webservice inventory-manager trying to add a pet to the pet store +# Simulates host inventory-manager trying to add and remove a pet + +import sys import requests +sys.path.append('../..') import conjur PETSTORE_URL = 'http://localhost:8080' @@ -10,6 +13,9 @@ conjur.config.account = 'example' api = conjur.new_from_password('admin', 'secret') # TODO: swap this with inventory_manager creds +key = api.role('host', 'inventory_manager').rotate_api_key() + +api = conjur.new_from_key('host/inventory_manager', key) response = requests.post( '{}/api/pets'.format(PETSTORE_URL), diff --git a/examples/flaskapp/load_secrets.py b/examples/flaskapp/load_secrets.py index 4fb3857..af93053 100644 --- a/examples/flaskapp/load_secrets.py +++ b/examples/flaskapp/load_secrets.py @@ -1,6 +1,9 @@ import random import string +import sys + +sys.path.append('../..') import conjur @@ -12,6 +15,5 @@ def random_value(): api = conjur.new_from_password('admin', 'secret') - -# Set random values for secrets +# Set the database password to a known value api.resource('variable', 'dbpassword').add_secret('w^kftUagHmF2Ahph') diff --git a/examples/flaskapp/requirements.txt b/examples/flaskapp/requirements.txt index 82f9757..0a8febc 100644 --- a/examples/flaskapp/requirements.txt +++ b/examples/flaskapp/requirements.txt @@ -2,4 +2,4 @@ Flask psycopg2 Flask-SQLAlchemy --e git+git://github.com/conjurinc/api-python.git@work/possum#egg=conjur +# conjur - this library is being loaded locally From 1667e85bd74e4d40461b11a424d1e55404e0d801 Mon Sep 17 00:00:00 2001 From: Dustin Collins Date: Fri, 23 Sep 2016 16:19:47 -0500 Subject: [PATCH 48/62] Rename flaskapp -> petstore [CON-3437] --- examples/README.md | 10 ++++++++++ examples/{flaskapp => petstore}/README.md | 11 +++++------ examples/{flaskapp => petstore}/app.py | 9 ++++++--- examples/{flaskapp => petstore}/docker-compose.yml | 0 examples/{flaskapp => petstore}/employee.py | 1 - examples/{flaskapp => petstore}/init-user-db.sh | 0 examples/{flaskapp => petstore}/inventory_manager.py | 3 +-- examples/{flaskapp => petstore}/load_secrets.py | 0 examples/{flaskapp => petstore}/nonemployee.py | 0 examples/{flaskapp => petstore}/policy.yml | 9 ++++----- examples/{flaskapp => petstore}/requirements.txt | 0 examples/{flaskapp => petstore}/start.sh | 0 examples/{flaskapp => petstore}/stop.sh | 0 examples/{flaskapp => petstore}/templates/home.html | 0 14 files changed, 26 insertions(+), 17 deletions(-) rename examples/{flaskapp => petstore}/README.md (90%) rename examples/{flaskapp => petstore}/app.py (91%) rename examples/{flaskapp => petstore}/docker-compose.yml (100%) rename examples/{flaskapp => petstore}/employee.py (99%) rename examples/{flaskapp => petstore}/init-user-db.sh (100%) rename examples/{flaskapp => petstore}/inventory_manager.py (89%) rename examples/{flaskapp => petstore}/load_secrets.py (100%) rename examples/{flaskapp => petstore}/nonemployee.py (100%) rename examples/{flaskapp => petstore}/policy.yml (74%) rename examples/{flaskapp => petstore}/requirements.txt (100%) rename examples/{flaskapp => petstore}/start.sh (100%) rename examples/{flaskapp => petstore}/stop.sh (100%) rename examples/{flaskapp => petstore}/templates/home.html (100%) diff --git a/examples/README.md b/examples/README.md index 876f4d2..b31ad7f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,6 +2,16 @@ These examples show how to use the Python API. +## petstore + +An end-to-end example that illustrates fetching secrets using host identity +and enabling traffic authorization can be found in the [petstore](petstore) +subdirectory. + +Read on for other examples of how to use the Conjur Python API client. + +--- + A docker-compose environment is included for ease of use. Note it needs [conjurinc/possum-example](https://github.com/conjurinc/possum-example) and [conjurinc/possum](https://github.com/conjurinc/possum) images available diff --git a/examples/flaskapp/README.md b/examples/petstore/README.md similarity index 90% rename from examples/flaskapp/README.md rename to examples/petstore/README.md index a99c5e7..2970f67 100644 --- a/examples/flaskapp/README.md +++ b/examples/petstore/README.md @@ -53,14 +53,13 @@ You will notice that there are no pets displayed. [policy.yml](policy.yml) is loaded when Possum starts up. The policy defines: * variable `dbpassword` - Variable resource holds, the `petstore` database password -* webservice `petstore_ws` - Webservice resource fpr the `petstore` app * host `petstore` - Host role for the `petstore` app * host `inventory_manager` - Host role for the `inventory_manager` app * group `employees` - Group role with three users The host `petstore` is granted `execute` (read) access to the `dbpassword` variable. `petstore` fetches this password from Possum when starting up. -`inventory_manager` is granted `add_pet` and `remove_pet` privileges on `petstore_ws`. -Members of group `employees` are granted `add_pet` permission on the `petstore` webservice. Removing pets is a more privileged operation than adding them. +`inventory_manager` is granted `add_pet` and `remove_pet` privileges on the`petstore` host. +Members of group `employees` are granted `add_pet` permission on the `petstore` host. Removing pets is a more privileged operation than adding them. Pets can be added and removed by using the pet store's API. @@ -69,14 +68,14 @@ Pets can be added and removed by using the pet store's API. The add and remove views are protected with the `validate_privilege` decorator in [app.py](app.py). A user or machine must pass an `Authorization` header -when calling the `petstore` webservice. `petstore` consults Possum to ensure -that the caller has `update` privilege on the `petstore` webservice defined +when calling the `petstore` host. `petstore` consults Possum to ensure +that the caller has `update` privilege on the `petstore` host defined in [policy.yml](policy.yml). If so, the request proceeds. If not, an error message is returned. ## Demo -Simulate calling the `petstore` webservice as different identities: +Simulate calling the `petstore` host as different identities: ```sh-session # non-employee diff --git a/examples/flaskapp/app.py b/examples/petstore/app.py similarity index 91% rename from examples/flaskapp/app.py rename to examples/petstore/app.py index aaae6f1..eea0a27 100644 --- a/examples/flaskapp/app.py +++ b/examples/petstore/app.py @@ -9,7 +9,10 @@ conjur.config.url = 'http://localhost:3030' conjur.config.account = 'example' -api = conjur.new_from_password('admin', 'secret') # TODO: swap this out with host creds + +api = conjur.new_from_password('admin', 'secret') +key = api.role('host', 'petstore').rotate_api_key() +api = conjur.new_from_key('host/petstore', key) app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://petstore:{}@localhost/petstore'.format( api.resource('variable', 'dbpassword').secret() @@ -63,7 +66,7 @@ def home(): # API routes @app.route('/api/pets', methods=['POST']) -@validate_privilege('webservice:petstore', 'add_pet') +@validate_privilege('host:petstore', 'add_pet') def add_pet(): json = request.get_json(force=True) valid = json.has_key('name') and json.has_key('type') @@ -79,7 +82,7 @@ def add_pet(): @app.route('/api/pets/', methods=['DELETE']) -@validate_privilege('webservice:petstore', 'remove_pet') +@validate_privilege('host:petstore', 'remove_pet') def remove_pet(id): pet = Pets.query.filter_by(id=id).first() diff --git a/examples/flaskapp/docker-compose.yml b/examples/petstore/docker-compose.yml similarity index 100% rename from examples/flaskapp/docker-compose.yml rename to examples/petstore/docker-compose.yml diff --git a/examples/flaskapp/employee.py b/examples/petstore/employee.py similarity index 99% rename from examples/flaskapp/employee.py rename to examples/petstore/employee.py index c8fb7a5..aed048a 100644 --- a/examples/flaskapp/employee.py +++ b/examples/petstore/employee.py @@ -14,7 +14,6 @@ api = conjur.new_from_password('admin', 'secret') key = api.role('user', 'dan').rotate_api_key() - api = conjur.new_from_key('dan', key) diff --git a/examples/flaskapp/init-user-db.sh b/examples/petstore/init-user-db.sh similarity index 100% rename from examples/flaskapp/init-user-db.sh rename to examples/petstore/init-user-db.sh diff --git a/examples/flaskapp/inventory_manager.py b/examples/petstore/inventory_manager.py similarity index 89% rename from examples/flaskapp/inventory_manager.py rename to examples/petstore/inventory_manager.py index ddb7e23..ffa55d5 100644 --- a/examples/flaskapp/inventory_manager.py +++ b/examples/petstore/inventory_manager.py @@ -12,9 +12,8 @@ conjur.config.url = 'http://localhost:3030' conjur.config.account = 'example' -api = conjur.new_from_password('admin', 'secret') # TODO: swap this with inventory_manager creds +api = conjur.new_from_password('admin', 'secret') key = api.role('host', 'inventory_manager').rotate_api_key() - api = conjur.new_from_key('host/inventory_manager', key) response = requests.post( diff --git a/examples/flaskapp/load_secrets.py b/examples/petstore/load_secrets.py similarity index 100% rename from examples/flaskapp/load_secrets.py rename to examples/petstore/load_secrets.py diff --git a/examples/flaskapp/nonemployee.py b/examples/petstore/nonemployee.py similarity index 100% rename from examples/flaskapp/nonemployee.py rename to examples/petstore/nonemployee.py diff --git a/examples/flaskapp/policy.yml b/examples/petstore/policy.yml similarity index 74% rename from examples/flaskapp/policy.yml rename to examples/petstore/policy.yml index 1d9942e..53a72bf 100644 --- a/examples/flaskapp/policy.yml +++ b/examples/petstore/policy.yml @@ -1,8 +1,7 @@ --- - !variable &dbpassword dbpassword -- !webservice &petstore_ws petstore_ws -- !host &petstore_host petstore_host +- !host &petstore petstore - !host &inventory_manager inventory_manager - !group &employees_group employees @@ -18,15 +17,15 @@ - !permit role: *employees - resource: *petstore_ws + resource: *petstore privileges: [add_pet] - !permit role: *inventory_manager - resource: *petstore_ws + resource: *petstore privileges: [add_pet, remove_pet] - !permit - role: *petstore_host + role: *petstore resource: *dbpassword privileges: [execute] diff --git a/examples/flaskapp/requirements.txt b/examples/petstore/requirements.txt similarity index 100% rename from examples/flaskapp/requirements.txt rename to examples/petstore/requirements.txt diff --git a/examples/flaskapp/start.sh b/examples/petstore/start.sh similarity index 100% rename from examples/flaskapp/start.sh rename to examples/petstore/start.sh diff --git a/examples/flaskapp/stop.sh b/examples/petstore/stop.sh similarity index 100% rename from examples/flaskapp/stop.sh rename to examples/petstore/stop.sh diff --git a/examples/flaskapp/templates/home.html b/examples/petstore/templates/home.html similarity index 100% rename from examples/flaskapp/templates/home.html rename to examples/petstore/templates/home.html From 7f34d2e20ec1e93a7afd4aaf48cf96b2a99ce04b Mon Sep 17 00:00:00 2001 From: Dustin Collins Date: Fri, 23 Sep 2016 16:31:18 -0500 Subject: [PATCH 49/62] remove unused imports --- examples/petstore/load_secrets.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/examples/petstore/load_secrets.py b/examples/petstore/load_secrets.py index af93053..418da88 100644 --- a/examples/petstore/load_secrets.py +++ b/examples/petstore/load_secrets.py @@ -1,15 +1,9 @@ -import random -import string - import sys sys.path.append('../..') import conjur -def random_value(): - return ''.join(random.choice(string.ascii_lowercase) for _ in range(20)) - conjur.config.url = 'http://localhost:3030' conjur.config.account = 'example' From 076cbfb6a557315481f3a5150ef47aed21dd4425 Mon Sep 17 00:00:00 2001 From: Dustin Collins Date: Fri, 23 Sep 2016 16:33:50 -0500 Subject: [PATCH 50/62] Remove 'update' privilege mention --- examples/petstore/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/petstore/README.md b/examples/petstore/README.md index 2970f67..4d5c544 100644 --- a/examples/petstore/README.md +++ b/examples/petstore/README.md @@ -69,9 +69,8 @@ Pets can be added and removed by using the pet store's API. The add and remove views are protected with the `validate_privilege` decorator in [app.py](app.py). A user or machine must pass an `Authorization` header when calling the `petstore` host. `petstore` consults Possum to ensure -that the caller has `update` privilege on the `petstore` host defined -in [policy.yml](policy.yml). If so, the request proceeds. If not, an error -message is returned. +that the caller has the required privilege on the `petstore` host. +If so, the request proceeds. If not, an error message is returned. ## Demo From e74777d6f975aeac1fa9aa0d4a5b72adfdf41b0f Mon Sep 17 00:00:00 2001 From: Dustin Collins Date: Thu, 29 Sep 2016 11:17:38 -0400 Subject: [PATCH 51/62] Fix import path for conjur --- examples/petstore/app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/petstore/app.py b/examples/petstore/app.py index eea0a27..b253d9a 100644 --- a/examples/petstore/app.py +++ b/examples/petstore/app.py @@ -1,8 +1,10 @@ from functools import wraps +import sys from flask import Flask, render_template, jsonify, request from flask_sqlalchemy import SQLAlchemy +sys.path.append('../..') import conjur app = Flask(__name__) From 8061f8b10e14d5e6af033caacda6b3ef995b3621 Mon Sep 17 00:00:00 2001 From: Kevin Gilpin Date: Thu, 6 Oct 2016 16:28:07 -0400 Subject: [PATCH 52/62] wip --- conjur/cli.py | 139 +++++++++++++++++++++++++++++++++++++++++ conjur/util.py | 14 +++++ dev/Dockerfile | 6 ++ dev/docker-compose.yml | 32 ++++++++++ dev/start.sh | 13 ++++ dev/stop.sh | 4 ++ requirements.txt | 4 +- 7 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 conjur/cli.py create mode 100644 dev/Dockerfile create mode 100644 dev/docker-compose.yml create mode 100755 dev/start.sh create mode 100755 dev/stop.sh diff --git a/conjur/cli.py b/conjur/cli.py new file mode 100644 index 0000000..50eb8e4 --- /dev/null +++ b/conjur/cli.py @@ -0,0 +1,139 @@ +from netrc import netrc +from urlparse import urlparse +import argparse +import sys +import os +import conjur +import inflection +from tabulate import tabulate + +def netrc_path(): + try: + return os.environ['CONJURRC'] + except KeyError: + return os.path.expanduser('~/.netrc') + +def credentials_from_netrc(): + creds = netrc(netrc_path()) + result = creds.authenticators(conjur.config.url) + try: + return ( result[0], result[2] ) + except TypeError: + raise Exception("Conjur URL %s is not in %s, therefore you're not logged in" % ( conjur.config.url, netrc_path() )) + +def credentials(): + try: + ( os.environ['CONJUR_AUTHN_LOGIN'], os.environ['CONJUR_AUTHN_API_KEY'] ) + except KeyError: + return credentials_from_netrc() + +def connect(): + return conjur.new_from_key(*credentials()) + +def find_object(kind, id): + api = connect() + if len(id.split(':')) > 0: + return api.resource_qualified(id) + else: + return api.resource(kind, id) + +def find_variable(id): + return find_object('variable', id) + +def find_policy(id): + return find_object('policy', id) + +def authenticate_handler(args): + import base64 + token = connect().authenticate() + if args.H: + token = base64.b64encode(token) + print token + +def whoami_handler(args): + api = connect() + api.authenticate() + print ':'.join(( conjur.config.account, conjur.util.login_kind(api.login), conjur.util.login_identifier(api.login) )) + +def list_handler(args): + def flatten_record(record): + result = [ record['id'], record['owner'] ] + if 'policy' in record.keys(): + result.append(record['policy']) + return result + + api = connect() + resources = [ flatten_record(resource) for resource in api.get(api._resources_url(kind=args.kind)).json() ] + print tabulate(resources, headers=['Id', 'Owner', 'Policy']) + +def show_handler(args): + api = connect() + resource = api.resource_qualified(args.id) + resource = resource.api.get(resource.url()).json() + keys = resource.keys() + keys.sort() + data =[ ( inflection.camelize(key), resource[key] ) for key in keys ] + print tabulate(data) + +def policy_load_handler(args): + if args.policy == '-': + value = sys.stdin.read() + else: + value = args.policy + policy = find_policy(args.id) + url = [ + policy.api.config.url, + 'policies', + policy.account, + policy.kind, + conjur.util.urlescape(policy.identifier) + ] + + policy.api.post(url, data=value) + +def fetch_handler(args): + print find_variable(args.id).secret(args.version) + +def store_handler(args): + if args.value == '-': + value = sys.stdin.read() + else: + value = args.value + find_variable(args.id).add_secret(value) + print "Value added" + +parser = argparse.ArgumentParser(description='Possum command-line client.') +subparsers = parser.add_subparsers() + +whoami = subparsers.add_parser('whoami') +whoami.set_defaults(func=whoami_handler) + +authenticate = subparsers.add_parser('authenticate') +authenticate.add_argument('-H', action='store_true') +authenticate.set_defaults(func=authenticate_handler) + +list_ = subparsers.add_parser('list') +list_.add_argument('-k', '--kind', help='Resource kind') +list_.set_defaults(func=list_handler) + +show = subparsers.add_parser('show') +show.add_argument('id') +show.set_defaults(func=show_handler) + +policy_load = subparsers.add_parser('policy:load') +policy_load.add_argument('id') +policy_load.add_argument('policy') +policy_load.set_defaults(func=policy_load_handler) + +store = subparsers.add_parser('store') +store.add_argument('id') +store.add_argument('value') +store.set_defaults(func=store_handler) + +fetch = subparsers.add_parser('fetch') +fetch.add_argument('id') +fetch.add_argument('-V', '--version', help='Variable version') +fetch.set_defaults(func=fetch_handler) + +args = parser.parse_args() +args.func(args) diff --git a/conjur/util.py b/conjur/util.py index 77ed75d..589302a 100644 --- a/conjur/util.py +++ b/conjur/util.py @@ -30,6 +30,20 @@ def urlescape(s): return quote(s, '') +def login_kind(login): + tokens = login.split('/', 1) + if len(tokens) == 2: + return tokens[0] + else: + return 'user' + +def login_identifier(login): + tokens = login.split('/', 1) + if len(tokens) == 2: + return tokens[1] + else: + return tokens[0] + def authzid(obj, kind, with_account=True): if isinstance(obj, (str, unicode)): # noqa F821 (flake8 doesn't know about unicode) if not with_account: diff --git a/dev/Dockerfile b/dev/Dockerfile new file mode 100644 index 0000000..a590b53 --- /dev/null +++ b/dev/Dockerfile @@ -0,0 +1,6 @@ +FROM ubuntu:14.04 + +RUN apt-get update -y && apt-get install -y git curl vim python-pip + +WORKDIR /app +ENV PYTHONPATH /app diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml new file mode 100644 index 0000000..866f5ff --- /dev/null +++ b/dev/docker-compose.yml @@ -0,0 +1,32 @@ +pg: + image: postgres:9.3 + +example: + image: conjurinc/possum-example + entrypoint: /bin/sh + +possum: + image: conjurinc/possum + command: server -a example -f /var/lib/possum-example/policy/conjur.yml + environment: + DATABASE_URL: postgres://postgres@pg/postgres + POSSUM_ADMIN_PASSWORD: secret + POSSUM_DATA_KEY: + volumes_from: + - example + links: + - pg:pg + +client: + build: . + entrypoint: bash + environment: + CONJUR_APPLIANCE_URL: http://possum + CONJUR_ACCOUNT: example + POSSUM_LOGIN: admin + POSSUM_PASSWORD: secret + PYTHONPATH: /app + volumes: + - ..:/app + links: + - possum:possum diff --git a/dev/start.sh b/dev/start.sh new file mode 100755 index 0000000..258e6d2 --- /dev/null +++ b/dev/start.sh @@ -0,0 +1,13 @@ +#!/bin/bash -ex + +docker-compose build + +if [ ! -f data_key ]; then + echo "Generating data key" + docker-compose run --no-deps --rm possum data-key generate > data_key +fi + +export POSSUM_DATA_KEY="$(cat data_key)" + +docker-compose up -d pg example possum +docker-compose run --rm client diff --git a/dev/stop.sh b/dev/stop.sh new file mode 100755 index 0000000..f9a246a --- /dev/null +++ b/dev/stop.sh @@ -0,0 +1,4 @@ +#!/bin/bash -e + +docker-compose stop +docker-compose rm -f diff --git a/requirements.txt b/requirements.txt index ea4b3c6..6dbbd40 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ pyyaml -requests >= 2.2.1 \ No newline at end of file +requests >= 2.2.1 +tabulate +inflection From b5f013aae1f33cba0879140e55ce7631a52c4f68 Mon Sep 17 00:00:00 2001 From: Kevin Gilpin Date: Tue, 11 Oct 2016 17:04:43 -0400 Subject: [PATCH 53/62] Updated the CLI and added example yaml --- conjur/cli.py | 114 +++++++++++++++++++++++++++----- examples/policies/bootstrap.yml | 31 +++++++++ examples/policies/database.yml | 7 ++ examples/policies/frontend.yml | 9 +++ examples/policies/people.yml | 16 +++++ 5 files changed, 162 insertions(+), 15 deletions(-) create mode 100644 examples/policies/bootstrap.yml create mode 100644 examples/policies/database.yml create mode 100644 examples/policies/frontend.yml create mode 100644 examples/policies/people.yml diff --git a/conjur/cli.py b/conjur/cli.py index 50eb8e4..7737089 100644 --- a/conjur/cli.py +++ b/conjur/cli.py @@ -1,5 +1,7 @@ +from __future__ import print_function from netrc import netrc from urlparse import urlparse +import getpass import argparse import sys import os @@ -7,14 +9,43 @@ import inflection from tabulate import tabulate +def eprint(*args, **kwargs): + print(*args, file=sys.stderr, **kwargs) + def netrc_path(): try: return os.environ['CONJURRC'] except KeyError: return os.path.expanduser('~/.netrc') +def touch_netrc(): + path = netrc_path() + with open(path, 'a'): + os.utime(path, None) + os.chmod(path, 0o600) + +def netrc_str(netrc): + """Dump the class data in the format of a .netrc file.""" + rep = "" + for host in netrc.hosts.keys(): + attrs = netrc.hosts[host] + rep = rep + "machine "+ host + "\n\tlogin " + str(attrs[0]) + "\n" + if attrs[1]: + rep = rep + "account " + str(attrs[1]) + rep = rep + "\tpassword " + str(attrs[2]) + "\n" + for macro in netrc.macros.keys(): + rep = rep + "macdef " + macro + "\n" + for line in netrc.macros[macro]: + rep = rep + line + rep = rep + "\n" + return rep + def credentials_from_netrc(): - creds = netrc(netrc_path()) + try: + creds = netrc(netrc_path()) + except IOError: + raise Exception("Not logged in. File %s does not exist" % netrc_path()) + result = creds.authenticators(conjur.config.url) try: return ( result[0], result[2] ) @@ -30,9 +61,21 @@ def credentials(): def connect(): return conjur.new_from_key(*credentials()) +def current_roleid(): + # Ensure the login is valid + api = connect() + api.authenticate() + username = api.login + account = conjur.config.account + if username.find('/') != -1: + kind, _, id = username.partition('/') + else: + kind, id = ( 'user', username) + return "%s:%s:%s" % ( account, kind, id ) + def find_object(kind, id): api = connect() - if len(id.split(':')) > 0: + if len(id.split(':')) > 1: return api.resource_qualified(id) else: return api.resource(kind, id) @@ -43,17 +86,49 @@ def find_variable(id): def find_policy(id): return find_object('policy', id) +def interpret_login(role): + tokens = role.split(':', 1) + if len(tokens) == 2: + return tokens + else: + return ( 'user', tokens[0] ) + +def login_handler(args): + role = args.role + if not role: + eprint("Enter your username to log into Possum: ", end="") + role = raw_input() + kind, identifier = interpret_login(role) + password = getpass.getpass("Enter password for %s %s (it will not be echoed): " % ( kind, identifier )) + username = '/'.join((kind, identifier)) + api_key = conjur.new_from_password(username, password).api_key + save_api_key(username, api_key) + print("Logged in") + +def save_api_key(username, api_key): + touch_netrc() + logins = netrc(netrc_path()) + logins.hosts[conjur.config.url] = ( username, None, api_key ) + with open(netrc_path(), 'w') as f: + f.write(netrc_str(logins)) + def authenticate_handler(args): import base64 token = connect().authenticate() if args.H: token = base64.b64encode(token) - print token + print(token) def whoami_handler(args): + print(current_roleid()) + +def rotate_api_key_handler(args): api = connect() - api.authenticate() - print ':'.join(( conjur.config.account, conjur.util.login_kind(api.login), conjur.util.login_identifier(api.login) )) + role = conjur.Role.from_roleid(api, args.role or current_roleid()) + api_key = role.rotate_api_key() + if not args.role: + save_api_key(api.login, api_key) + print(api_key) def list_handler(args): def flatten_record(record): @@ -64,7 +139,7 @@ def flatten_record(record): api = connect() resources = [ flatten_record(resource) for resource in api.get(api._resources_url(kind=args.kind)).json() ] - print tabulate(resources, headers=['Id', 'Owner', 'Policy']) + print(tabulate(resources, headers=['Id', 'Owner', 'Policy'])) def show_handler(args): api = connect() @@ -73,7 +148,7 @@ def show_handler(args): keys = resource.keys() keys.sort() data =[ ( inflection.camelize(key), resource[key] ) for key in keys ] - print tabulate(data) + print(tabulate(data)) def policy_load_handler(args): if args.policy == '-': @@ -81,18 +156,19 @@ def policy_load_handler(args): else: value = args.policy policy = find_policy(args.id) - url = [ + url = '/'.join([ policy.api.config.url, 'policies', - policy.account, - policy.kind, + policy.api.config.account, + 'policy', conjur.util.urlescape(policy.identifier) - ] + ]) - policy.api.post(url, data=value) + response = policy.api.post(url, data=value) + print("Policy updated") def fetch_handler(args): - print find_variable(args.id).secret(args.version) + print(find_variable(args.id).secret(args.version)) def store_handler(args): if args.value == '-': @@ -100,11 +176,15 @@ def store_handler(args): else: value = args.value find_variable(args.id).add_secret(value) - print "Value added" + print("Value added") -parser = argparse.ArgumentParser(description='Possum command-line client.') +parser = argparse.ArgumentParser(description='Possum command-line interface.') subparsers = parser.add_subparsers() +login = subparsers.add_parser('login') +login.add_argument('-r', '--role') +login.set_defaults(func=login_handler) + whoami = subparsers.add_parser('whoami') whoami.set_defaults(func=whoami_handler) @@ -112,6 +192,10 @@ def store_handler(args): authenticate.add_argument('-H', action='store_true') authenticate.set_defaults(func=authenticate_handler) +rotate_api_key = subparsers.add_parser('rotate_api_key') +rotate_api_key.add_argument('-r', '--role') +rotate_api_key.set_defaults(func=rotate_api_key_handler) + list_ = subparsers.add_parser('list') list_.add_argument('-k', '--kind', help='Resource kind') list_.set_defaults(func=list_handler) diff --git a/examples/policies/bootstrap.yml b/examples/policies/bootstrap.yml new file mode 100644 index 0000000..601ba32 --- /dev/null +++ b/examples/policies/bootstrap.yml @@ -0,0 +1,31 @@ +- !group security_admin + +- !policy + id: people + owner: !group security_admin + body: + - !group + id: frontend + + - !group + id: operations + +- !policy + id: prod + owner: !group security_admin + body: + - !policy + id: frontend + + - !policy + id: database + +- !permit + role: !group people/frontend + privilege: [ read, execute ] + resource: !policy prod/frontend + +- !permit + role: !group people/operations + privilege: [ read, execute ] + resource: !policy prod/database diff --git a/examples/policies/database.yml b/examples/policies/database.yml new file mode 100644 index 0000000..96d8896 --- /dev/null +++ b/examples/policies/database.yml @@ -0,0 +1,7 @@ +- &variables + - !variable password + +- !permit + role: !layer ../frontend + privilege: [ read, execute ] + resource: *variables diff --git a/examples/policies/frontend.yml b/examples/policies/frontend.yml new file mode 100644 index 0000000..b56dc62 --- /dev/null +++ b/examples/policies/frontend.yml @@ -0,0 +1,9 @@ +- !layer + +- &hosts + - !host frontend-01 + - !host frontend-02 + +- !grant + role: !layer + members: *hosts diff --git a/examples/policies/people.yml b/examples/policies/people.yml new file mode 100644 index 0000000..9011274 --- /dev/null +++ b/examples/policies/people.yml @@ -0,0 +1,16 @@ +- !group operations + +- !group frontend + +- !user owen + +- !user frank + +- !grant + role: !group operations + member: !user owen + +- !grant + role: !group frontend + member: !user frank + From 676ca8169b4e314f7acc28422bdc0bafc9b48efa Mon Sep 17 00:00:00 2001 From: Kevin Gilpin Date: Wed, 12 Oct 2016 10:46:11 -0400 Subject: [PATCH 54/62] improve error reporting --- conjur/api.py | 6 +++++- {examples/policies => demo}/bootstrap.yml | 0 {examples/policies => demo}/database.yml | 0 {examples/policies => demo}/frontend.yml | 0 {examples/policies => demo}/people.yml | 0 5 files changed, 5 insertions(+), 1 deletion(-) rename {examples/policies => demo}/bootstrap.yml (100%) rename {examples/policies => demo}/database.yml (100%) rename {examples/policies => demo}/frontend.yml (100%) rename {examples/policies => demo}/people.yml (100%) diff --git a/conjur/api.py b/conjur/api.py index c2c0ee3..a9024ec 100644 --- a/conjur/api.py +++ b/conjur/api.py @@ -134,7 +134,11 @@ def _request(self, method, url, *args, **kwargs): response = getattr(requests, method.lower())(url, *args, **kwargs) if check_errors and response.status_code >= 300: - raise ConjurException("Request failed: %d" % response.status_code) + try: + error = response.json()['error'] + except: + raise ConjurException("Request failed: %d" % response.status_code) + raise ConjurException("%s : %s" % ( error['code'], error['message'] )) return response diff --git a/examples/policies/bootstrap.yml b/demo/bootstrap.yml similarity index 100% rename from examples/policies/bootstrap.yml rename to demo/bootstrap.yml diff --git a/examples/policies/database.yml b/demo/database.yml similarity index 100% rename from examples/policies/database.yml rename to demo/database.yml diff --git a/examples/policies/frontend.yml b/demo/frontend.yml similarity index 100% rename from examples/policies/frontend.yml rename to demo/frontend.yml diff --git a/examples/policies/people.yml b/demo/people.yml similarity index 100% rename from examples/policies/people.yml rename to demo/people.yml From 5d41385c99ace98b1b3c9e522bc6925adf4bdbf2 Mon Sep 17 00:00:00 2001 From: Kevin Gilpin Date: Wed, 12 Oct 2016 10:46:33 -0400 Subject: [PATCH 55/62] use 'down' in stop script --- examples/stop.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/stop.sh b/examples/stop.sh index 0ea693e..1e3ef9d 100755 --- a/examples/stop.sh +++ b/examples/stop.sh @@ -1,4 +1,3 @@ #!/bin/bash -ex -docker-compose stop -docker-compose rm -f +docker-compose down From c1dcfb070f5e3430dd11c2fde1a3bba8181dfc5b Mon Sep 17 00:00:00 2001 From: Kevin Gilpin Date: Wed, 12 Oct 2016 10:46:55 -0400 Subject: [PATCH 56/62] create demo directory --- demo/README.md | 28 ++++++++++++++++++++++++++++ demo/docker-compose.yml | 23 +++++++++++++++++++++++ demo/env | 3 +++ demo/initial_bootstrap.yml | 9 +++++++++ demo/start.sh | 13 +++++++++++++ demo/stop.sh | 3 +++ 6 files changed, 79 insertions(+) create mode 100644 demo/README.md create mode 100644 demo/docker-compose.yml create mode 100644 demo/env create mode 100644 demo/initial_bootstrap.yml create mode 100755 demo/start.sh create mode 100755 demo/stop.sh diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..740ccc6 --- /dev/null +++ b/demo/README.md @@ -0,0 +1,28 @@ +# Possum Demo + +## Description + +See [https://conjurinc.github.io/possum/demo.html](https://conjurinc.github.io/possum/demo.html) for a detailed walkthrough. + +The policies used in the demo are available in this directory. + +## Running + +To run a Possum server and command-line client in Docker containers, simply run `./start.sh`: + +```sh-session +$ ./start.sh +pg uses an image, skipping +possum uses an image, skipping +Step 1 : FROM python:2.7-slim + ---> 4947dfe5e830 +... +Creating demo_pg_1 +Creating demo_possum_1 +root@5c2b6208380e:/app# possum -h +usage: possum [-h] + {login,whoami,authenticate,rotate_api_key,list,show,policy:load,store,fetch} + ... + +Possum command-line interface. +``` diff --git a/demo/docker-compose.yml b/demo/docker-compose.yml new file mode 100644 index 0000000..2652eea --- /dev/null +++ b/demo/docker-compose.yml @@ -0,0 +1,23 @@ +pg: + image: postgres:9.3 + +possum: + image: conjurinc/possum + command: server -a demo -f /var/lib/possum/initial_bootstrap.yml + environment: + DATABASE_URL: postgres://postgres@pg/postgres + POSSUM_ADMIN_PASSWORD: secret + POSSUM_DATA_KEY: + volumes: + - .:/var/lib/possum + links: + - pg:pg + +client: + build: .. + entrypoint: bash + env_file: env + volumes: + - ..:/app + links: + - possum:possum diff --git a/demo/env b/demo/env new file mode 100644 index 0000000..2637dd3 --- /dev/null +++ b/demo/env @@ -0,0 +1,3 @@ +CONJUR_URL=http://possum +CONJUR_ACCOUNT=demo +PYTHONPATH=/app diff --git a/demo/initial_bootstrap.yml b/demo/initial_bootstrap.yml new file mode 100644 index 0000000..1070144 --- /dev/null +++ b/demo/initial_bootstrap.yml @@ -0,0 +1,9 @@ +- !group security_admin + +- !policy + id: people + owner: !group security_admin + +- !policy + id: prod + owner: !group security_admin diff --git a/demo/start.sh b/demo/start.sh new file mode 100755 index 0000000..390a609 --- /dev/null +++ b/demo/start.sh @@ -0,0 +1,13 @@ +#!/bin/bash -e + +docker-compose build + +if [ ! -f data_key ]; then + echo "Generating data key" + docker-compose run --no-deps --rm possum data-key generate > data_key +fi + +export POSSUM_DATA_KEY="$(cat data_key)" + +docker-compose up -d pg possum +docker-compose run --rm client diff --git a/demo/stop.sh b/demo/stop.sh new file mode 100755 index 0000000..1e3ef9d --- /dev/null +++ b/demo/stop.sh @@ -0,0 +1,3 @@ +#!/bin/bash -ex + +docker-compose down From 5344fac73073327a6290c2b80001d5b518751232 Mon Sep 17 00:00:00 2001 From: Kevin Gilpin Date: Wed, 12 Oct 2016 16:14:10 -0400 Subject: [PATCH 57/62] add proper ownership of the database --- demo/bootstrap.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/bootstrap.yml b/demo/bootstrap.yml index 601ba32..ff66806 100644 --- a/demo/bootstrap.yml +++ b/demo/bootstrap.yml @@ -19,6 +19,7 @@ - !policy id: database + owner: !group ../people/operations - !permit role: !group people/frontend From e1ece373ae53d085d140bc2e94b2eb4d890c174e Mon Sep 17 00:00:00 2001 From: Kevin Gilpin Date: Wed, 12 Oct 2016 16:14:21 -0400 Subject: [PATCH 58/62] Add --rotate option to 'login' --- conjur/cli.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/conjur/cli.py b/conjur/cli.py index 7737089..76de0bd 100644 --- a/conjur/cli.py +++ b/conjur/cli.py @@ -7,6 +7,7 @@ import os import conjur import inflection +import json from tabulate import tabulate def eprint(*args, **kwargs): @@ -99,9 +100,16 @@ def login_handler(args): eprint("Enter your username to log into Possum: ", end="") role = raw_input() kind, identifier = interpret_login(role) - password = getpass.getpass("Enter password for %s %s (it will not be echoed): " % ( kind, identifier )) username = '/'.join((kind, identifier)) - api_key = conjur.new_from_password(username, password).api_key + + if args.rotate: + api = connect() + role = conjur.Role.from_roleid(api, ":".join((kind, identifier))) + api_key = role.rotate_api_key() + else: + password = getpass.getpass("Enter password for %s %s (it will not be echoed): " % ( kind, identifier )) + api_key = conjur.new_from_password(username, password).api_key + save_api_key(username, api_key) print("Logged in") @@ -147,7 +155,13 @@ def show_handler(args): resource = resource.api.get(resource.url()).json() keys = resource.keys() keys.sort() - data =[ ( inflection.camelize(key), resource[key] ) for key in keys ] + def format_value(value): + if isinstance(value, basestring): + return value + else: + return json.dumps(value) + + data = [ ( inflection.camelize(key), format_value(resource[key]) ) for key in keys ] print(tabulate(data)) def policy_load_handler(args): @@ -183,6 +197,7 @@ def store_handler(args): login = subparsers.add_parser('login') login.add_argument('-r', '--role') +login.add_argument('--rotate', action='store_true') login.set_defaults(func=login_handler) whoami = subparsers.add_parser('whoami') From d9f20ace1eef7c911be04a79928c37898226ce46 Mon Sep 17 00:00:00 2001 From: Kevin Gilpin Date: Thu, 13 Oct 2016 10:12:58 -0400 Subject: [PATCH 59/62] script to generate groups and users --- .gitignore | 1 + demo/generate.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100755 demo/generate.py diff --git a/.gitignore b/.gitignore index a02afe6..8941481 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +generated-policy.yml data_key artifacts pytest.xml diff --git a/demo/generate.py b/demo/generate.py new file mode 100755 index 0000000..878c886 --- /dev/null +++ b/demo/generate.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python + +import sys + +count = int(sys.argv[1]) + +f = open('generated-policy.yml', 'w') + +for i in range(0, count / 10 + 1): + f.write("- !group group-%s\n" % i) + +f.write("\n") + +for i in range(0, count): + f.write("""- !user user-%s +- !grant + role: !group group-%s + member: !user user-%s +\n""" % ( i, i / 10, i)) + +f.close() From b572da8935eafffe5285b1df2abb5c2cfae9899b Mon Sep 17 00:00:00 2001 From: Kevin Gilpin Date: Thu, 13 Oct 2016 10:13:15 -0400 Subject: [PATCH 60/62] print policy version and new API keys after load --- conjur/cli.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/conjur/cli.py b/conjur/cli.py index 76de0bd..fc492d9 100644 --- a/conjur/cli.py +++ b/conjur/cli.py @@ -178,8 +178,15 @@ def policy_load_handler(args): conjur.util.urlescape(policy.identifier) ]) - response = policy.api.post(url, data=value) - print("Policy updated") + response = policy.api.post(url, data=value).json() + created_roles = response['created_roles'] + print("") + print("Loaded policy version %s" % response['version']) + print("Created %s roles" % len(created_roles)) + if len(created_roles) > 0: + print("") + print(tabulate([ ( record['id'], record['api_key'] ) for record in created_roles.values() ], ("Id", "API Key"))) + print("") def fetch_handler(args): print(find_variable(args.id).secret(args.version)) From 15e2353f7d39af3c77622bbeda5806c128f69188 Mon Sep 17 00:00:00 2001 From: Kevin Gilpin Date: Thu, 13 Oct 2016 10:28:08 -0400 Subject: [PATCH 61/62] exit with status 1 if the variable has no value --- conjur/cli.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/conjur/cli.py b/conjur/cli.py index fc492d9..fd149c7 100644 --- a/conjur/cli.py +++ b/conjur/cli.py @@ -182,14 +182,18 @@ def policy_load_handler(args): created_roles = response['created_roles'] print("") print("Loaded policy version %s" % response['version']) - print("Created %s roles" % len(created_roles)) if len(created_roles) > 0: + print("Created %s roles" % len(created_roles)) print("") print(tabulate([ ( record['id'], record['api_key'] ) for record in created_roles.values() ], ("Id", "API Key"))) print("") def fetch_handler(args): - print(find_variable(args.id).secret(args.version)) + value = find_variable(args.id).secret(args.version) + if value: + print(value) + else: + sys.exit(1) def store_handler(args): if args.value == '-': From cba8c934fb0c0b23126926a2dcb632fdcc0541df Mon Sep 17 00:00:00 2001 From: Kevin Gilpin Date: Mon, 24 Oct 2016 18:07:47 +0000 Subject: [PATCH 62/62] add missing return statement --- conjur/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conjur/cli.py b/conjur/cli.py index fd149c7..eb2c053 100644 --- a/conjur/cli.py +++ b/conjur/cli.py @@ -55,7 +55,7 @@ def credentials_from_netrc(): def credentials(): try: - ( os.environ['CONJUR_AUTHN_LOGIN'], os.environ['CONJUR_AUTHN_API_KEY'] ) + return ( os.environ['CONJUR_AUTHN_LOGIN'], os.environ['CONJUR_AUTHN_API_KEY'] ) except KeyError: return credentials_from_netrc()