diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..14bd311 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: +- package-ecosystem: pip + directory: "/" + schedule: + interval: daily + time: "11:00" + open-pull-requests-limit: 1 diff --git a/.gitignore b/.gitignore index 0d20b64..a17907e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *.pyc +dist/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index c513760..64e95dd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,11 @@ language: python python: - - "3.3" + - "3.8" + - "3.7" + - "3.6" + - "3.5" - "2.7" install: - - pip install . --use-mirrors + - pip install . - pip install nose script: python tests/unittests.py diff --git a/README.rst b/README.rst old mode 100644 new mode 100755 index ef59aba..c3b4fb5 --- a/README.rst +++ b/README.rst @@ -1,6 +1,26 @@ python-crowd ============ +.. image:: https://img.shields.io/pypi/pyversions/crowd.svg + :target: https://pypi.python.org/pypi/jira/ + +.. image:: https://img.shields.io/pypi/l/crowd.svg + :target: https://pypi.python.org/pypi/crowd/ + +.. image:: https://img.shields.io/pypi/dm/crowd.svg + :target: https://pypi.python.org/pypi/crowd/ + +------------ + +.. image:: https://api.travis-ci.org/pycontribs/python-crowd.svg?branch=master + :target: https://travis-ci.org/pycontribs/python-crowd + +.. image:: https://img.shields.io/pypi/status/crowd.svg + :target: https://pypi.python.org/pypi/crowd/ + +.. image:: https://img.shields.io/coveralls/pycontribs/crowd.svg + :target: https://coveralls.io/r/pycontribs/crowd + python-crowd is a python client library to the Atlassian Crowd REST API. This library may be useful to you if you wish to create an application @@ -14,7 +34,7 @@ Documentation Docs are built automatically by sphinx. You can build them yourself in the doc directory or access them at -. +http://python-crowd.readthedocs.io/en/latest/ Examples ======== @@ -74,3 +94,7 @@ to the end of this list and include the change in your pull request. * Christian Schläppi (@nevious) * Yuichi Tokutomi (@Tommy1969) * Attila Bogár (@attilabogar) +* Jascha Geerds (@jgeerds) +* John Christian (@potus98) +* Maiko Bossuyt (@maiko) +* Oleg Sinilo (@GoRoSfan) diff --git a/crowd.py b/crowd.py old mode 100644 new mode 100755 index 258e4ec..fa05387 --- a/crowd.py +++ b/crowd.py @@ -31,14 +31,18 @@ class CrowdServer(object): The ``ssl_verify`` parameter controls how and if certificates are verified. If ``True``, the SSL certificate will be verified. A CA_BUNDLE path can also be provided. + + The ``client_cert`` tuple (cert,key) specifies a SSL client certificate and key files. """ - def __init__(self, crowd_url, app_name, app_pass, ssl_verify=True, timeout=None): + def __init__(self, crowd_url, app_name, app_pass, ssl_verify=True, + timeout=None, client_cert=None): self.crowd_url = crowd_url self.app_name = app_name self.app_pass = app_pass self.rest_url = crowd_url.rstrip("/") + "/rest/usermanagement/1" self.ssl_verify = ssl_verify + self.client_cert = client_cert self.timeout = timeout self.session = self._build_session(content_type='json') @@ -58,6 +62,7 @@ def _build_session(self, content_type='json'): } session = requests.Session() session.verify = self.ssl_verify + session.cert = self.client_cert session.auth = requests.auth.HTTPBasicAuth(self.app_name, self.app_pass) session.headers.update(headers) return session @@ -276,7 +281,7 @@ def validate_session(self, token, remote="127.0.0.1", proxy=None): } if proxy: - params["validation-factors"]["validationFactors"].append({"name": "X-Forwarded-For", "value": proxy, }) + params["validationFactors"].append({"name": "X-Forwarded-For", "value": proxy, }) url = self.rest_url + "/session/%s" % token response = self._post(url, data=json.dumps(params), params={"expand": "user"}) @@ -313,6 +318,21 @@ def terminate_session(self, token): # Otherwise return True return True + def get_cookie_conf(self): + """Retrieve cookie configuration + + Returns: + dict: conf information + None: if failure occurred + + """ + response = self._get(self.rest_url + "/config/cookie") + + if not response.ok: + return None + + return response.json() + def add_user(self, username, raise_on_error=False, **kwargs): """Add a user to the directory @@ -425,6 +445,223 @@ def set_active(self, username, active_state): return None + def set_user_attribute(self, username, attribute, value, raise_on_error=False): + """Set an attribute on a user + :param username: The username on which to set the attribute + :param attribute: The name of the attribute to set + :param value: The value of the attribute to set + :return: True on success, False on failure. + """ + data = { + 'attributes': [ + { + 'name': attribute, + 'values': [ + value + ] + }, + ] + } + response = self._post(self.rest_url + "/user/attribute", + params={"username": username,}, + data=json.dumps(data)) + + if response.status_code == 204: + return True + + if raise_on_error: + raise RuntimeError(response.json()['message']) + + return False + + def create_group(self, name, description='', raise_on_error=False): + """Create a new group with the . + + Args: + name (str): the group name of a new group + + Returns: + None: if succeeded + error msg (str): If unsuccessful + + """ + data = { + "name": name, + "description": description, + "type": "GROUP", + "active": True + } + response = self._post(self.rest_url + "/group", data=json.dumps(data)) + if response.status_code == 201: + return + + if raise_on_error: + raise RuntimeError(response.json()['message']) + + return response.json()['message'] + + def remove_group(self, name, raise_on_error=False): + """Remove a group with the . + + Args: + name (str): the group name of a removed group + + Returns: + None: If succeeded + error msg (str): If unsuccessful + + """ + data = { + "groupname": name + } + response = self._delete(self.rest_url + "/group", params=data) + if response.status_code == 204: + return + + if raise_on_error: + raise RuntimeError(response.json()['message']) + + return response.json()['message'] + + def update_group(self, name, data, raise_on_error=False): + """Update a group with the . + + Args: + name (str): the group name of a removed group + data (dict): new group attrs + + Returns: + None: if succeeded + error msg (str): If unsuccessful + + """ + params = { + 'groupname': name + } + data["name"] = name + data["type"] = "GROUP" + + response = self._put( + self.rest_url + "/group", params=params, data=json.dumps(data) + ) + if response.status_code == 200: + return + + if raise_on_error: + raise RuntimeError(response.json()['message']) + + return response.json()['message'] + + def add_child_group(self, name, parent, raise_on_error=False): + """Add a child group with the to group. + + Args: + name (str): the name of a child group + parent (str): the name of a parent group + + Returns: + None: if succeeded + error msg (str): If unsuccessful + + """ + params = { + 'groupname': parent + } + data = { + "name": name + } + response = self._post( + self.rest_url + "/group/child-group/direct", + data=json.dumps(data), + params=params + ) + if response.status_code == 201: + return + + if raise_on_error: + raise RuntimeError(response.json()['message']) + + return response.json()['message'] + + def remove_child_group(self, name, parent, raise_on_error=False): + """Add a child group with the to group. + + Args: + name (str): the name of a child group + parent (str): the name of a parent group + + Returns: + None: if succeeded + error msg (str): If unsuccessful + + """ + params = { + 'groupname': parent, + 'child-groupname': name + } + response = self._delete( + self.rest_url + "/group/child-group/direct", + params=params + ) + if response.status_code == 204: + return + + if raise_on_error: + raise RuntimeError(response.json()['message']) + + return response.json()['message'] + + def add_user_to_group(self, username, groupname, raise_on_error=False): + """Add a user to a group + :param username: The username to assign to the group + :param groupname: The group name into which to assign the user + :return: True on success, False on failure. + """ + data = { + 'name': groupname, + } + response = self._post(self.rest_url + "/user/group/direct", + params={"username": username,}, + data=json.dumps(data)) + + if response.status_code == 201: + return True + + if raise_on_error: + raise RuntimeError(response.json()['message']) + + return False + + def remove_user_from_group(self, username, groupname, raise_on_error=False): + """Remove a user from a group + + Attempts to remove a user from a group + + Args: + username: The username to remove from the group. + groupname: The group name to be removed from the user. + + Returns: + True: Succeeded + False: If unsuccessful + """ + + response = self._delete( + self.rest_url + "/group/user/direct", + params={ + "username": username, + "groupname": groupname + } + ) + + if response.status_code == 204: + return True + + if raise_on_error: + raise RuntimeError(response.json()['message']) + + return False + def change_password(self, username, newpassword, raise_on_error=False): """Change new password for a user @@ -488,7 +725,8 @@ def get_groups(self, username): return [g['name'] for g in response.json()['groups']] def get_nested_groups(self, username): - """Retrieve a list of all group names that have as a direct or indirect member. + """Retrieve a list of all group names that have as a direct + or indirect member. Args: username: The account username. @@ -508,7 +746,8 @@ def get_nested_groups(self, username): return [g['name'] for g in response.json()['groups']] def get_nested_group_users(self, groupname): - """Retrieves a list of all users that directly or indirectly belong to the given groupname. + """Retrieves a list of all users that directly or indirectly belong to + the given groupname. Args: groupname: The group name. @@ -573,7 +812,7 @@ def get_memberships(self): memberships[group] = {u'users': users, u'groups': groups} return memberships - def search(self, entity_type, property_name, search_string): + def search(self, entity_type, property_name, search_string, start_index=0, max_results=99999): """Performs a user search using the Crowd search API. https://developer.atlassian.com/display/CROWDDEV/Crowd+REST+Resources#CrowdRESTResources-SearchResource @@ -582,6 +821,8 @@ def search(self, entity_type, property_name, search_string): entity_type: 'user' or 'group' property_name: eg. 'email', 'name' search_string: the string to search for. + start_index: starting index of the results (default: 0) + max_results: maximum number of results returned (default: 99999) Returns: json results: @@ -601,6 +842,8 @@ def search(self, entity_type, property_name, search_string): params = { 'entity-type': entity_type, 'expand': entity_type, + 'start-index': start_index, + 'max-results': max_results } # Construct XML payload of the form: # diff --git a/doc/conf.py b/doc/conf.py index 6c5c851..30dcbc9 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = '0.9' +version = '1.0' # The full version, including alpha/beta/rc tags. -release = '0.9.0' +release = '1.0.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index 6b8b450..5e311e0 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ from setuptools import setup +import codecs import os.path __dir__ = os.path.dirname(os.path.abspath(__file__)) @@ -6,15 +7,16 @@ name='Crowd', license='BSD', py_modules=['crowd'], - version='0.9.1', + version='3.1.0', install_requires=['requests', 'lxml'], description='A python client to the Atlassian Crowd REST API', - long_description=open(os.path.join(__dir__, 'README.rst')).read(), + long_description=codecs.open(os.path.join(__dir__, 'README.rst'), + encoding='utf-8').read(), author='Alexander Else', author_email='aelse@else.id.au', - url='https://github.com/aelse/python-crowd', + url='https://github.com/pycontribs/python-crowd', classifiers=[ "Development Status :: 4 - Beta", @@ -22,7 +24,7 @@ "License :: OSI Approved :: BSD License", "Programming Language :: Python", "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.5", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Systems Administration :: Authentication/Directory", ]