diff --git a/README.md b/README.md
index b62de8f53..1128031c6 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
# VMware Carbon Black Cloud Python SDK
-**Latest Version:** 1.3.0
+**Latest Version:** 1.3.1
-**Release Date:** 08 June 2021
+**Release Date:** 15 June 2021
[![Coverage Status](https://coveralls.io/repos/github/carbonblack/carbon-black-cloud-sdk-python/badge.svg?t=Id6Baf)](https://coveralls.io/github/carbonblack/carbon-black-cloud-sdk-python)
[![Codeship Status for carbonblack/carbon-black-cloud-sdk-python](https://app.codeship.com/projects/9e55a370-a772-0138-aae4-129773225755/status?branch=develop)](https://app.codeship.com/projects/402767)
diff --git a/VERSION b/VERSION
index f0bb29e76..3a3cd8cc8 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.3.0
+1.3.1
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 0fb8e7671..270ddfd2b 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,5 +1,16 @@
Changelog
================================
+CBC SDK 1.3.1 - Released June 15, 2021
+--------------------------------
+
+New Features:
+
+* Allow the SDK to accept a pre-configured ``Session`` object to be used for access, to get around unusual configuration requirements.
+
+Bug Fixes:
+
+* Fix functions in ``Grant`` object for adding a new access profile to a user access grant.
+
CBC SDK 1.3.0 - Released June 8, 2021
--------------------------------
diff --git a/docs/conf.py b/docs/conf.py
index 84f0b1be9..6adbdcc56 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -23,7 +23,7 @@
author = 'Developer Relations'
# The full version, including alpha/beta/rc tags
-release = '1.3.0'
+release = '1.3.1'
# -- General configuration ---------------------------------------------------
diff --git a/docs/guides-and-resources.rst b/docs/guides-and-resources.rst
index 4bd8d0a7e..a45568573 100755
--- a/docs/guides-and-resources.rst
+++ b/docs/guides-and-resources.rst
@@ -18,7 +18,8 @@ Guides
* :doc:`workload` - Advanced protection purpose-built for securing modern workloads to reduce the attack surface and strengthen security posture.
* :doc:`reputation-override` - Manage reputation overrides for known applications, IT tools or certs.
* :doc:`live-response` - Live Response allows security operators to collect information and take action on remote endpoints in real time.
-* :doc:`unified-binary-store` - The unified binary store (UBS) is responsible for storing all binaries and corresponding metadata for those binaries.
+* :doc:`unified-binary-store` - The unified binary store (UBS) is responsible for storing all binaries and corresponding metadata for those binaries.
+* :doc:`users-grants` - Work with users and access grants.
Examples
--------
diff --git a/docs/users-grants.rst b/docs/users-grants.rst
new file mode 100644
index 000000000..650b4ba94
--- /dev/null
+++ b/docs/users-grants.rst
@@ -0,0 +1,198 @@
+Users and Grants
+================
+
+Using the Carbon Black Cloud SDK, you can work with the users in your organization, as well as their access grants
+and profiles.
+
+Uniform Resource Names (URNs)
+-----------------------------
+
+The various API functions that work with users and grants often make use of *uniform resource names* (URNs) that
+uniquely represent various pieces of the Carbon Black Cloud environment. These pieces include:
+
+* **Organizations,** represented as ``psc:org:ORGKEY``, where ``ORGKEY`` is the organization's alphanumeric key value.
+* The special URN ``psc:org:ORKGEY:CHILDREN``, where ``ORGKEY`` is the organization's alphanumeric key value,
+ refers to all the *child organizations* of that organization, but *not* the organization itself.
+* **Users,** represented as ``psc:user:ORGKEY:USERID``, where ``ORGKEY`` is the organization's alphanumeric key value
+ and ``USERID`` is the user's numeric login ID.
+* **Access roles,** represented as ``psc:role:OPT-ORGKEY:NAME``, where ``OPT-ORGKEY`` is (optionally) the alphanumeric
+ key value of the organization containing that role, and ``NAME`` is the name of the role. A role that does not have
+ an OPT-ORGKEY is a default/global role created for all organizations.
+
+Most of these are dealt with for you by the Carbon Black Cloud SDK.
+
+Getting a List of Users
+-----------------------
+
+We can do a query on the ``User`` object to get a list of users within the organization we're accessing.
+
+::
+
+ >>> from cbc_sdk import CBCloudAPI
+ >>> api = CBCloudAPI(profile='sample')
+ >>> from cbc_sdk.platform import User
+ >>> query = api.select(User)
+ >>> user_list = list(query)
+ >>> for user in user_list:
+ ... print(f"{user.first_name} {user.last_name} (#{user.login_id}) <{user.email}>")
+ ...
+ Lysa Arryn (#2345670)
+ Olenna Redwyne (#2345671)
+ Arianne Martell (#2345672)
+ Jorah Mormont (#2345673)
+
+We can restrict the query by user IDs or E-mail addresses by using the ``user_ids([str])`` or ``email_addresses([str])``
+methods on the query object returned by ``select()`` before enumerating its results.
+
+Modifying a User
+----------------
+
+A ``User`` can be modified by changing one or more of its fields and then calling its ``save()`` method.
+
+::
+
+ >>> from cbc_sdk import CBCloudAPI
+ >>> api = CBCloudAPI(profile='sample')
+ >>> from cbc_sdk.platform import User
+ >>> user = api.select(User, 2345672)
+ >>> print(user.phone)
+ 800-555-0000
+ >>> user.phone = '888-555-9753'
+ >>> user.save()
+ @ https://defense.conferdeploy.net (*)
+ >>> print(user.phone)
+ 888-555-9753
+
+**Note:** A user's *role* can only be modified by updating the user's *access grant,* detailed below.
+
+Creating a New User
+-------------------
+
+Creating a user may be done with the help of a *builder object,* which is returned from the ``User.create()``
+function.
+
+::
+
+ >>> from cbc_sdk import CBCloudAPI
+ >>> api = CBCloudAPI(profile='sample')
+ >>> from cbc_sdk.platform import User
+ >>> builder = User.create(api)
+ >>> builder.set_first_name('Samwell').set_last_name('Tarly')
+
+ >>> builder.set_email('starly@example.com').set_phone('800-555-8008')
+
+ >>> builder.set_role('psc:role::BETA_SYSTEM_ADMIN')
+
+ >>> builder.build()
+
+Alternately, you may construct a *template object* (a Python ``dict``) that contains the user's information and
+create the user directly.
+
+::
+
+ >>> from cbc_sdk import CBCloudAPI
+ >>> api = CBCloudAPI(profile='sample')
+ >>> from cbc_sdk.platform import User
+ >>> user_template = {'first_name': 'Selyse', 'last_name': 'Florent', 'email': 'sflorent@example.com',
+ ... 'phone': '877-555-9099', 'role_urn': 'psc:role::BETA_SYSTEM_ADMIN'}
+ >>> User.create(api, user_template)
+
+**Note:** A user that has just been created will *not* be visible in either the UI or in a ``User`` query as detailed
+above, until the user activates their account through the invitation E-mail message and sets a password.
+
+User Access Grants
+------------------
+
+Every user object has an *access grant* object associated with it, defining the access roles they are permitted to use.
+You can use the ``grant()`` method on a ``User`` to get the grant and inspect or modify it.
+
+::
+
+ >>> from cbc_sdk import CBCloudAPI
+ >>> api = CBCloudAPI(profile='sample')
+ >>> from cbc_sdk.platform import User
+ >>> user = api.select(User, 2345672)
+ >>> print(f"{user.first_name} {user.last_name}")
+ Arianne Martell
+ >>> grant = user.grant()
+ >>> print(grant.roles)
+ ['psc:role::BETA_SYSTEM_ADMIN']
+ >>> grant.roles = ['psc:role::BETA_VIEW_ONLY']
+ >>> grant.save()
+ @ https://defense.conferdeploy.net
+ >>> print(grant.roles)
+ ['psc:role::psc:role::BETA_VIEW_ONLY']
+
+You can see what roles your API key is able to access and assign using the ``get_permitted_role_urns()`` function:
+
+::
+
+ >>> from cbc_sdk import CBCloudAPI
+ >>> api = CBCloudAPI(profile='sample')
+ >>> from cbc_sdk.platform import Grant
+ >>> for index, role_urn in enumerate(Grant.get_permitted_role_urns(api)):
+ ... print(f"{index}. {role_urn}")
+ ...
+ 0. psc:role::BETA_LEVEL_3_ANALYST
+ 1. psc:role::KUBERNETES_SECURITY_DATAPLANE_ONLY
+ 2. psc:role::ALL_AND_LR
+ 3. psc:role::BETA_LEVEL_1_ANALYST
+ 4. psc:role::BETA_SYSTEM_ADMIN
+ 5. psc:role::KUBERNETES_SECURITY_DATAPLANE
+ 6. psc:role::VIEW_ONLY
+ 7. psc:role::ALL
+ 8. psc:role::KUBERNETES_SECURITY_ADMIN_USER
+ 9. psc:role::BETA_SUPER_ADMIN
+ 10. psc:role::KUBERNETES_SECURITY_READ_ONLY_USER
+ 11. psc:role::CONTAINER_IMAGE_CLI_TOOL
+ 12. psc:role::KUBERNETES_SECURITY_DEVOPS
+ 13. psc:role::BETA_VIEW_ALL
+ 14. psc:role::KUBERNETES_SECURITY_DEVOPS_VIEW_ONLY
+ 15. psc:role::BETA_LEVEL_2_ANALYST
+ 16. psc:role::KUBERNETES_SECURITY_DEVELOPER
+
+Users created in the Carbon Black Cloud console employ *access profiles* on the access grants, which allow roles for
+a user to be specified for the organization and/or any child organizations. Access profiles may be accessed and
+manipulated through the access grant object.
+
+::
+
+ >>> from cbc_sdk import CBCloudAPI
+ >>> api = CBCloudAPI(profile='sample')
+ >>> from cbc_sdk.platform import User
+ >>> user = api.select(User, 3456789)
+ >>> grant = user.grant()
+ >>> for profile in grant.profiles_:
+ ... print(f"{profile.allowed_orgs} - {profile.roles}")
+ ...
+ ['psc:org:1A2B3C4DE'] - ['psc:role::BETA_LEVEL_3_ANALYST']
+ ['psc:org:2F3G4H5JK'] - ['psc:role::BETA_LEVEL_1_ANALYST']
+
+Adding an access profile may be done via the ``create_profile()`` method on ``Grant``:
+
+::
+
+ >>> from cbc_sdk import CBCloudAPI
+ >>> api = CBCloudAPI(profile='sample')
+ >>> from cbc_sdk.platform import User
+ >>> user = api.select(User, 3450987)
+ >>> grant = user.grant()
+ >>> builder = grant.create_profile()
+ >>> builder.add_org('psc:org:2F3G4H5JK').add_role('psc:role::BETA_VIEW_ALL')
+
+ >>> profile = builder.build()
+ {'orgs': {'allow': ['psc:org:2F3G4H5JK']}, 'roles': ['psc:role::BETA_VIEW_ALL']}
+
+Or it may be added via a template object (as with ``User``):
+
+::
+
+ >>> from cbc_sdk import CBCloudAPI
+ >>> api = CBCloudAPI(profile='sample')
+ >>> from cbc_sdk.platform import User
+ >>> user = api.select(User, 3450987)
+ >>> grant = user.grant()
+ >>> profile_template = {'orgs': {'allow': ['psc:org:2F3G4H5JK']}, 'roles': ['psc:role::BETA_VIEW_ALL']}
+ >>> profile = grant.create_profile(profile_template)
+ {'orgs': {'allow': ['psc:org:2F3G4H5JK']}, 'roles': ['psc:role::BETA_VIEW_ALL']}
+
diff --git a/src/cbc_sdk/__init__.py b/src/cbc_sdk/__init__.py
index b3fd958d9..7e0c419a6 100644
--- a/src/cbc_sdk/__init__.py
+++ b/src/cbc_sdk/__init__.py
@@ -4,7 +4,7 @@
__author__ = 'Carbon Black Developer Network'
__license__ = 'MIT'
__copyright__ = 'Copyright 2020-2021 VMware Carbon Black'
-__version__ = '1.3.0'
+__version__ = '1.3.1'
from .rest_api import CBCloudAPI
from .cache import lru
diff --git a/src/cbc_sdk/connection.py b/src/cbc_sdk/connection.py
index c20ba3384..2a5d0adb0 100644
--- a/src/cbc_sdk/connection.py
+++ b/src/cbc_sdk/connection.py
@@ -154,7 +154,13 @@ def init_poolmanager(self, connections, maxsize, block=DEFAULT_POOLBLOCK, **pool
class Connection(object):
"""Object that encapsulates the HTTP connection to the CB server."""
- def __init__(self, credentials, integration_name=None, timeout=None, max_retries=None, **pool_kwargs):
+ def __init__(self,
+ credentials,
+ integration_name=None,
+ timeout=None,
+ max_retries=None,
+ proxy_session=None,
+ **pool_kwargs):
"""
Initialize the Connection object.
@@ -163,6 +169,7 @@ def __init__(self, credentials, integration_name=None, timeout=None, max_retries
integration_name (str): The integration name being used.
timeout (int): The timeout value to use for HTTP requests on this connection.
max_retries (int): The maximum number of times to retry a request.
+ proxy_session (requests.Session) custom session to be used
**pool_kwargs: Additional arguments to be used to initialize connection pooling.
Raises:
@@ -195,7 +202,12 @@ def __init__(self, credentials, integration_name=None, timeout=None, max_retries
self.token = credentials.token
self.token_header = {'X-Auth-Token': self.token, 'User-Agent': user_agent}
- self.session = requests.Session()
+ if proxy_session:
+ self.session = proxy_session
+ credentials.use_custom_proxy_session = True
+ else:
+ self.session = requests.Session()
+ credentials.use_custom_proxy_session = False
self._timeout = timeout
@@ -215,7 +227,10 @@ def __init__(self, credentials, integration_name=None, timeout=None, max_retries
self.session.mount(self.server, tls_adapter)
self.proxies = {}
- if credentials.ignore_system_proxy: # see https://github.com/kennethreitz/requests/issues/879
+ if credentials.use_custom_proxy_session:
+ # get the custom session proxies
+ self.proxies = self.session.proxies
+ elif credentials.ignore_system_proxy: # see https://github.com/kennethreitz/requests/issues/879
# Unfortunately, requests will look for any proxy-related environment variables and use those anyway. The
# only way to solve this without side effects, is passing in empty strings for 'http' and 'https':
self.proxies = {
@@ -380,13 +395,19 @@ def __init__(self, *args, **kwargs):
# must be None to use MAX_RETRIES in Connection __init__
max_retries = kwargs.pop("max_retries", None)
+ proxy_session = kwargs.pop("proxy_session", None)
pool_connections = kwargs.pop("pool_connections", 1)
pool_maxsize = kwargs.pop("pool_maxsize", DEFAULT_POOLSIZE)
pool_block = kwargs.pop("pool_block", DEFAULT_POOLBLOCK)
- self.session = Connection(self.credentials, integration_name=integration_name, timeout=timeout,
- max_retries=max_retries, pool_connections=pool_connections,
- pool_maxsize=pool_maxsize, pool_block=pool_block)
+ self.session = Connection(self.credentials,
+ integration_name=integration_name,
+ timeout=timeout,
+ max_retries=max_retries,
+ proxy_session=proxy_session,
+ pool_connections=pool_connections,
+ pool_maxsize=pool_maxsize,
+ pool_block=pool_block)
def raise_unless_json(self, ret, expected):
"""
diff --git a/src/cbc_sdk/platform/grants.py b/src/cbc_sdk/platform/grants.py
index f6124bc59..da0156ecd 100644
--- a/src/cbc_sdk/platform/grants.py
+++ b/src/cbc_sdk/platform/grants.py
@@ -17,7 +17,6 @@
from cbc_sdk.errors import ApiError, NonQueryableModel
import time
import copy
-import uuid
import logging
log = logging.getLogger(__name__)
@@ -114,9 +113,7 @@ def _update_object(self):
str: The UUID of this profile object.
"""
if 'profile_uuid' not in self._info:
- self._info['profile_uuid'] = str(uuid.uuid4())
- log.debug("Creating a Profile object with UUID {0:s} for Grant with ID {1:s}"
- .format(self._model_unique_id, self._grant._model_unique_id))
+ log.debug("Creating a Profile object for Grant with ID {0:s}".format(self._grant._model_unique_id))
url = self.urlobject.format(self._cb.credentials.org_key, self._grant._model_unique_id)
ret = self._cb.post_object(url, self._info)
else:
@@ -198,7 +195,7 @@ def __init__(self, grant):
self._cb = grant._cb
self._orgs = {'allow': []}
self._roles = []
- self._conditions = {'expiration': 0, 'disabled': False}
+ self._conditions = None
self._can_manage = False
def set_orgs(self, orgs_list):
@@ -277,7 +274,10 @@ def set_expiration(self, expiration):
Returns:
ProfileBuilder: This object.
"""
- self._conditions['expiration'] = expiration
+ if self._conditions:
+ self._conditions['expiration'] = expiration
+ else:
+ self._conditions = {'expiration': expiration}
return self
def set_disabled(self, flag):
@@ -290,7 +290,10 @@ def set_disabled(self, flag):
Returns:
ProfileBuilder: This object.
"""
- self._conditions['disabled'] = flag
+ if self._conditions:
+ self._conditions['disabled'] = flag
+ else:
+ self._conditions = {'disabled': flag}
return self
def build(self):
@@ -300,7 +303,9 @@ def build(self):
Returns:
Profile: The new Profile object.
"""
- data = {'orgs': self._orgs, 'roles': self._roles, 'conditions': self._conditions}
+ data = {'orgs': self._orgs, 'roles': self._roles}
+ if self._conditions:
+ data['conditions'] = self._conditions
profile = Grant.Profile(self._cb, self._grant, None, data)
if self._grant:
profile._update_object()
@@ -471,8 +476,6 @@ def _update_object(self):
log.debug("Creating a new Grant object for principal {0:s}".format(save_info['principal']))
save_info['profiles'] = []
for profile in self._profiles:
- # presumed to be all new profiles, so create UUIDs for them
- profile._info['profile_uuid'] = str(uuid.uuid4())
save_info['profiles'].append(copy.deepcopy(profile._info))
url = self.urlobject.format(self._cb.credentials.org_key)
ret = self._cb.post_object(url, save_info)
@@ -571,13 +574,6 @@ def create_profile(self, template=None):
t = copy.deepcopy(template)
if 'profile_uuid' in t:
del t['profile_uuid']
- if 'conditions' not in t:
- t['conditions'] = {}
- if 'expiration' not in t['conditions']:
- t['conditions']['expiration'] = 0
- if 'disabled' not in t['conditions']:
- t['conditions']['disabled'] = False
- t['can_manage'] = False
profile = Grant.Profile(self._cb, self, None, t)
profile._update_object()
self._profiles.append(profile)
diff --git a/src/cbc_sdk/platform/users.py b/src/cbc_sdk/platform/users.py
index caf9f3bcf..b24f2e465 100644
--- a/src/cbc_sdk/platform/users.py
+++ b/src/cbc_sdk/platform/users.py
@@ -445,6 +445,7 @@ def _internal_add_profiles(self, profile_templates):
"""
grant = self.grant()
if grant:
+ create_templates = []
for template in profile_templates:
need_create = True
for profile in grant.profiles_:
@@ -454,8 +455,10 @@ def _internal_add_profiles(self, profile_templates):
need_create = False
break
if need_create:
- grant.create_profile(template)
+ create_templates.append(template)
grant.save()
+ for template in create_templates:
+ grant.create_profile(template)
else:
grant_template = {'principal': self.urn, 'org_ref': self.org_urn, 'roles': [],
'principal_name': f"{self.first_name} {self.last_name}", 'profiles': profile_templates}
diff --git a/src/tests/unit/fixtures/platform/mock_grants.py b/src/tests/unit/fixtures/platform/mock_grants.py
index 21beba321..1717b428c 100644
--- a/src/tests/unit/fixtures/platform/mock_grants.py
+++ b/src/tests/unit/fixtures/platform/mock_grants.py
@@ -669,22 +669,7 @@
"disabled": True
},
"can_manage": False
- },
- {
- "orgs": {
- "allow": [
- "psc:org:test_infinity"
- ],
- },
- "roles": [
- "psc:role::SECOPS_ROLE_MANAGER"
- ],
- "conditions": {
- "expiration": 0,
- "disabled": False
- },
- "can_manage": False
- },
+ }
],
"org_ref": "psc:org:test",
"principal_name": "Daniel Jackson",
diff --git a/src/tests/unit/platform/test_grants.py b/src/tests/unit/platform/test_grants.py
index 3931a7118..2f362ccb2 100644
--- a/src/tests/unit/platform/test_grants.py
+++ b/src/tests/unit/platform/test_grants.py
@@ -260,7 +260,6 @@ def test_create_profile_on_existing_grant(cbcsdk_mock):
"""Test the creation of a new profile within a grant via a builder."""
def respond_to_profile_grant(url, body, **kwargs):
ret = copy.deepcopy(POST_PROFILE_IN_GRANT_RESP)
- ret['profile_uuid'] = body['profile_uuid']
return ret
cbcsdk_mock.mock_request('GET', '/access/v2/orgs/test/grants/psc:user:12345678:ABCDEFGH', GET_GRANT_RESP)
@@ -276,7 +275,6 @@ def test_create_profile_from_template(cbcsdk_mock):
"""Test the creation of a new profile within a grant via a template."""
def respond_to_profile_grant(url, body, **kwargs):
ret = copy.deepcopy(POST_PROFILE_IN_GRANT_RESP)
- ret['profile_uuid'] = body['profile_uuid']
return ret
cbcsdk_mock.mock_request('GET', '/access/v2/orgs/test/grants/psc:user:12345678:ABCDEFGH', GET_GRANT_RESP)
diff --git a/src/tests/unit/platform/test_users.py b/src/tests/unit/platform/test_users.py
index f2a7bc78f..46fb55f76 100644
--- a/src/tests/unit/platform/test_users.py
+++ b/src/tests/unit/platform/test_users.py
@@ -544,18 +544,18 @@ def test_add_profiles(cbcsdk_mock, login_id, grant_get, new_profiles, expect_put
def on_put(url, body, **kwargs):
nonlocal expect_put, put_was_called
assert expect_put is not None
- fixed_expect_put = fixup_profile_uuids(expect_put, body)
- assert body == fixed_expect_put
+ assert body == expect_put
put_was_called = True
- return fixed_expect_put
+ return expect_put
def on_profile_post(url, body, **kwargs):
- nonlocal new_profiles, new_profile_count
+ nonlocal new_profiles, expect_new_profs, new_profile_count
matched = False
for template in new_profiles:
matched = template_matches(body, template) or matched
assert matched
new_profile_count = new_profile_count + 1
+ assert new_profile_count <= expect_new_profs
return body
cbcsdk_mock.mock_request('GET', '/appservices/v6/orgs/test/users', GET_USERS_RESP)
diff --git a/src/tests/unit/test_connection.py b/src/tests/unit/test_connection.py
index 2c90cd277..ffabb033e 100755
--- a/src/tests/unit/test_connection.py
+++ b/src/tests/unit/test_connection.py
@@ -78,6 +78,22 @@ def test_session_cert_file_and_proxies():
assert conn.proxies['https'] == 'foobie.bletch.com'
+def test_session_custom_session():
+ """Test to make sure custom session is passed"""
+ import requests
+ session = requests.Session()
+ creds = Credentials({'url': 'https://example.com', 'token': 'ABCDEFGH', 'ssl_cert_file': 'blort',
+ 'proxy': None})
+ session.proxies = {
+ 'http': 'foobie.bletch.com',
+ 'https': 'foobie.bletch.com'
+ }
+ conn = Connection(creds, proxy_session=session)
+ assert conn.ssl_verify == 'blort'
+ assert conn.proxies['http'] == 'foobie.bletch.com'
+ assert conn.proxies['https'] == 'foobie.bletch.com'
+
+
def test_session_ignore_system_proxy():
"""Test to make sure the ignore system proxy parameter has the right effect."""
creds = Credentials({'url': 'https://example.com', 'token': 'ABCDEFGH', 'ignore_system_proxy': True})