Skip to content
This repository was archived by the owner on Feb 15, 2020. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Requirements
============

* `python-odesk`
* `PyCrypto`


Authentication
Expand Down Expand Up @@ -44,6 +45,10 @@ To do so, add the `django_odesk.auth.backends.ModelBackend` to your
'django_odesk.auth.backends.ModelBackend',
)

.. note::
Please, note that `django_odesk` authentication backend may cause failing
Django tests. Please, read a paragraph below to find out why this happens.

You will also need to enable both `SessionMiddleware` and
`AuthenticationMiddleware`::

Expand Down Expand Up @@ -111,6 +116,77 @@ For simple cases you may just set login page to the

LOGIN_URL = '/odesk_auth/authenticate/'

Groups
------

`django-odesk` has support for Django groups. Groups are direct mapping
of oDesk teams. For each team the user is a member of, the corresponding
`django.contrib.auth.models.Group` is created with the name in the form::

company:team@odesk.com

The synchronyzation between oDesk teams and user groups is happening on
user object's creation by default. You can make it happen each time the
user logs in by setting::

ODESK_SYNC_PERMISSIONS_ON_LOGIN = True

Similar to creation of the unknown user, you can disable automatic creation
of new groups by setting::

ODESK_CREATE_UNKNOWN_GROUP = False

Since Django's authentication system has no support for roles, `django-odeask`
has limited support for "pseudo-roles". If the user has "admin" role in oDesk
team, they become the member of additional group, with the name of the from::

company:team:admins@odesk.com

Currently only admin role is supported.

In order turn on the "pseudo-groups" feature, set the corresponding variable::

ODESK_CREATE_PSEUDO_GROUPS = True

It is sometimes desirable to limit the view only to the members of the
specific oDesk team. `django-odesk` provides the convenient decorator to
check for group membership::

from django_odesk.auth.decorators import group_required

@group_required('company:team@odesk.com')
def my_view(request)
...

You can also give the list of group names. The user passes test if they
belong to at least one of them::

from django_odesk.auth.decorators import group_required

@group_required(['company:team@odesk.com','company:team2@odesk.com'])
def my_view(request)
...

Auth-only mode
--------------

Using oDesk APIs imposes an inherent secuirty risk. The person who has access
to the application server is capable of performing arbitrary actions on oDesk
on behalf of everyone who've been using the application, uless the users have
de-authorized the application expicitly. But sometimes all you need is just
to authenticate oDesk users and not make any other API calls. You can reduce
the mentioned risk, by not storing the API token anywhere after user has
logged in.

Since version 0.0.2 `django-odesk` supports "auth-only" mode that works
exactly like that. To turn it on, set::

ODESK_AUTH_ONLY = True

Please note that if you use `django_odesk.core.clients.RequestClient`, either
directly or with `RequestClientMiddleware` in auth-only mode, the client will
only be capable of calling public API methods.


Authentication without a database
---------------------------------
Expand Down Expand Up @@ -190,6 +266,14 @@ used in conjunction with `django_odesk.auth`::
client.team.get_teamrooms()
# ...

Note that the token is stored in django session encrypted (by default). The
encryption method used is AES. This key is stored in client
browser cookies and has expiration time set to two hours.
You can disable the encryption via specifying the following option
in your settings.py file:

ODESK_ENCRYPT_API_TOKEN = False

If you plan to use odesk API calls extensively in your views, there is
another shortcut, the `django_odesk.core.middleware.RequestClientMiddleware`.
It populates `request` with `odesk_client` attribute, which is an instance
Expand All @@ -209,3 +293,25 @@ Then you may use the client in your views::
request.odesk_client.team.get_teamrooms()
# ...

Django Tests Failure
====================

If your project is using `django_odesk` with it's model authentication backend
`django_odesk.auth.backends.ModelBackend`, you will face problems with running
standard Django's tests (in particular - `django.contrib.auth` tests):

`$ python manage.py test`

will give you lot's of errors.
This happens due to the nature of `django.contrib.auth` tests. While officially
Django's auth system supports third-party backends, its tests are intended to
check only standard (or very close to standard) backend. Really, `here <https://code.djangoproject.com/browser/django/trunk/django/contrib/auth/tests/views.py#L271>`_
you can see intension to authenticate user via username/password pair which
is of course incorrect in our case.

There is no way to prevent this by changing `django_odesk` package. Thus one who uses
`django_odesk` has two possible choices:

- ignore tests failure
- add Django's standard `django.contrib.auth.backends.ModelBackend` to the end of
`AUTHENTICATION_BACKENDS` tuple in your `settings.py` file.
2 changes: 1 addition & 1 deletion django_odesk/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VERSION = (0, 0, 3, 'alpha', 1)
VERSION = (0, 0, 2, 'final', 0)

def get_version():
version = '%s.%s' % (VERSION[0], VERSION[1])
Expand Down
122 changes: 25 additions & 97 deletions django_odesk/auth/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.contrib.auth.models import Group
from django.db import transaction
from django_odesk.auth.models import get_user_model
from django_odesk.auth.utils import sync_odesk_permissions
from django_odesk.conf import settings
from django_odesk.core.clients import DefaultClient

Expand Down Expand Up @@ -107,12 +108,12 @@ def has_module_perms(self, user_obj, app_label):
class BaseModelBackend(ModelBackend):

create_unknown_user = True
create_unknown_group = True
sync_permissions_on_login = True

def __init__(self, *args, **kwargs):
super(BaseModelBackend, self).__init__(*args, **kwargs)
self.api_token = None
self.auth_user = None
self.client = None
pass

def authenticate(self, token=None):
client = DefaultClient(token)
Expand All @@ -130,18 +131,22 @@ def authenticate(self, token=None):
if self.create_unknown_user:
user, created = model.objects.get_or_create(username=username)
if created:
user = self.configure_user(user, auth_user)
user = self.configure_user(user, auth_user, token)
else:
try:
user = model.objects.get(username=username)
except model.DoesNotExist:
pass

if self.sync_permissions_on_login:
sync_odesk_permissions(user, token, self.create_unknown_group)

return user

def clean_username(self, auth_user):
return auth_user['mail']
return auth_user['uid']+'@odesk.com'

def configure_user(self, user, auth_user):
def configure_user(self, user, auth_user, token):
return user

def get_user(self, user_id):
Expand All @@ -153,109 +158,32 @@ def get_user(self, user_id):


class ModelBackend(BaseModelBackend):


supports_object_permissions = False

@property
def create_unknown_user(self):
return settings.ODESK_CREATE_UNKNOWN_USER

def set_user_info(self, user, auth_user):
@property
def create_unknown_group(self):
return settings.ODESK_CREATE_UNKNOWN_GROUP

@property
def sync_permissions_on_login(self):
return settings.ODESK_SYNC_PERMISSIONS_ON_LOGIN

def configure_user(self, user, auth_user, token):
user.first_name = auth_user['first_name']
user.last_name = auth_user['last_name']
user.email = auth_user['mail']
user.set_unusable_password()
return user

def set_user_status(self, user, auth_user):
admins = settings.ODESK_ADMINS
superusers = settings.ODESK_SUPERUSERS
if user.username in admins:
user.is_staff = True
if user.username in superusers:
user.is_superuser = True
return user

def configure_user(self, user, auth_user):
self.set_user_info(user, auth_user)
self.set_user_status(user, auth_user)
user.set_unusable_password()
user.save()
return user


class TeamAuthBackend(ModelBackend):

def sync_django_groups(self, user, userteams):
"""
Assign existing `Group`s with userteam names to `user.groups`
"""
from django.db import connection

def clear_groups(user):
cursor = connection.cursor()
cursor.execute("DELETE FROM auth_user_groups WHERE user_id=%s", (user.id,))

def bulk_groups_insert(user, groups_query):
group_ids = filter(lambda gid: gid is not None,
(grp.get('id') for grp in groups_query.values('id')))
values = zip([user.id]*len(group_ids), group_ids)
if len(values) > 0:
sql_values = ','.join("(%i,%i)" % v for v in values)
cursor = connection.cursor()
cursor.execute("INSERT INTO auth_user_groups (user_id, group_id) VALUES %s" % sql_values)

@transaction.commit_on_success
def run_in_tx():
clear_groups(user)
bulk_groups_insert(user, Group.objects.filter(name__in=userteams))

run_in_tx()


def authenticate(self, token=None):
client = DefaultClient(token)
self.client = client
try:
api_token, auth_user = client.auth.check_token()
self.api_token, self.auth_user = api_token, auth_user
except HTTPError:
return None

user = None
username = self.clean_username(auth_user)
model = get_user_model()

userteams = set(team[u'id'] for team in client.hr.get_teams())
# TODO authorize subteams of parents in ODESK_AUTH_TEAMS
auth_teams = userteams.intersection(set(settings.ODESK_AUTH_TEAMS))

if auth_teams or username in settings.ODESK_AUTH_USERS:

if self.create_unknown_user:
user, created = model.objects.get_or_create(username=username)
user = self.set_user_info(user, auth_user)
else:
try:
user = model.objects.get(username=username)
except model.DoesNotExist:
pass

if user is not None:

self.sync_django_groups(user, auth_teams)

if userteams.intersection(set(settings.ODESK_AUTH_ADMIN_TEAMS)) or \
username in settings.ODESK_ADMINS:
user.is_staff=True
else:
user.is_staff=False

if userteams.intersection(set(settings.ODESK_AUTH_SUPERUSER_TEAMS)) or \
username in settings.ODESK_SUPERUSERS:
user.is_superuser=True
else:
user.is_superuser=False

# attach api_token to user instance, so it can be passed to post_save signal handler
user.odesk_api_token = api_token
user.save()

sync_odesk_permissions(user, token, self.create_unknown_group)
return user
12 changes: 12 additions & 0 deletions django_odesk/auth/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django.contrib.auth.decorators import user_passes_test
from django.contrib.auth.models import Group

def group_required(names, login_url=None):
"""
Checks if the user is a member of a particular group (or at least one
group from the list)
"""
if not hasattr(names,'__iter__'):
names = [names]
return user_passes_test(lambda u: u.groups.filter(name__in=names),
login_url=login_url)
61 changes: 61 additions & 0 deletions django_odesk/auth/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from django.db import IntegrityError
from django.contrib.auth.models import Group

from django_odesk.core.clients import DefaultClient
from django_odesk.conf import settings

def get_odesk_permissions(auth_token):
"""
Gets oDesk team roles/permissions for the authenticated user
"""
client = DefaultClient(auth_token)
response = client.hr.get_user_role()
permissions = []
# oDesk API returns empty string if user has no membership in any group
if not response:
return []
roles = response['userrole']
# oDesk API returns dict if there is single role
if isinstance(roles, dict):
roles = [roles]
for record in roles:
data = dict(team_id = record['team__id'], role=record['role'])
if record['permissions']:
data['permissions'] = record['permissions']['permission']
permissions.append(data)
return permissions



def sync_odesk_permissions(user, auth_token, create_groups=True):
"""
Syncs oDesk team roles/permissions with Django groups
"""
permissions = get_odesk_permissions(auth_token)
user.groups.through.objects.filter(user=user,
group__name__contains = '@odesk.com').delete()

def get_or_create_group(name):
created = False
group = None
try:
group = Group.objects.get(name=name)
except Group.DoesNotExist:
if create_groups:
group = Group(name=name)
group.save()
if group:
user.groups.add(group)
return group, created

for perm in permissions:
name = '%s@odesk.com' % perm['team_id']
group, c = get_or_create_group(name=name)
if settings.ODESK_CREATE_PSEUDO_GROUPS:
#Adding special groups for admins
#May later add other pseudo-roles
if perm['role'] == 'admin':
name = '%s:%s@odesk.com' % (perm['team_id'], 'admins')
group, c = get_or_create_group(name=name)
return True

Loading