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",
]