Skip to content

Commit ffef586

Browse files
author
lijianguo
committed
feat(oauth2): add oauth2 support
1 parent 5ea6e2b commit ffef586

7 files changed

Lines changed: 168 additions & 6 deletions

File tree

rootfs/api/authentication.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import logging
2+
from django.conf import settings
23
from django.contrib.auth.models import AnonymousUser
4+
from django.core.cache import cache
5+
from django.utils.translation import gettext_lazy as _
36
from rest_framework import authentication
4-
from rest_framework.authentication import TokenAuthentication
7+
from rest_framework.authentication import TokenAuthentication, \
8+
get_authorization_header
9+
from rest_framework import exceptions
510

11+
from api.oauth import OAuthManager
612

713
logger = logging.getLogger(__name__)
814

@@ -27,3 +33,43 @@ def authenticate(self, request):
2733
except Exception as e:
2834
logger.debug(e)
2935
return AnonymousUser(), None
36+
37+
38+
class DryccTokenAuthentication(TokenAuthentication):
39+
def authenticate(self, request):
40+
if 'manager' in request.META.get('HTTP_USER_AGENT', ''):
41+
auth = get_authorization_header(request).split()
42+
43+
if not auth or auth[0].lower() != self.keyword.lower().encode():
44+
return None
45+
46+
if len(auth) == 1:
47+
msg = _('Invalid token header. No credentials provided.')
48+
raise exceptions.AuthenticationFailed(msg)
49+
elif len(auth) > 2:
50+
msg = _('Invalid token header. Token string should not contain spaces.') # noqa
51+
raise exceptions.AuthenticationFailed(msg)
52+
53+
try:
54+
token = auth[1].decode()
55+
except UnicodeError:
56+
msg = _('Invalid token header. Token string should not contain invalid characters.') # noqa
57+
raise exceptions.AuthenticationFailed(msg)
58+
return self._check_oauth_token(token)
59+
return super(DryccTokenAuthentication, self).authenticate(request) # noqa
60+
61+
def _check_oauth_token(self, key):
62+
user = cache.get(key)
63+
if user:
64+
return user, None
65+
try:
66+
user_info = OAuthManager().get_user_by_token(key)
67+
if not user_info.get('email'):
68+
user_info['email'] = OAuthManager().get_email_by_token(key)
69+
except Exception as e:
70+
logger.info(e)
71+
raise exceptions.AuthenticationFailed(_('Verify token fail.'))
72+
from api import serializers
73+
user = serializers.UserSerializer.update_or_create(user_info)
74+
cache.set(key, user, settings.OAUTH_USER_CACHE_TIME)
75+
return user, None

rootfs/api/backend.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from django.contrib.auth import get_user_model
2+
from django.core.exceptions import ObjectDoesNotExist
3+
4+
from api import serializers
5+
from api.oauth import OAuthManager
6+
7+
8+
class DryccOauthBackend(object):
9+
10+
# The Django auth backend API
11+
def authenticate(self, request, username=None, password=None, **kwargs):
12+
if username is None:
13+
return None
14+
15+
with OAuthManager() as client:
16+
client.fetch_token(username, password)
17+
user_info = client.get_user()
18+
user_info['username'] = username
19+
user_info['password'] = password
20+
if not user_info.get('email'):
21+
user_info['email'] = client.get_email()
22+
user = serializers.UserSerializer.update_or_create(user_info)
23+
return user
24+
25+
def get_user(self, user_id):
26+
user = None
27+
try:
28+
user = get_user_model().objects.get(pk=user_id)
29+
except ObjectDoesNotExist:
30+
pass
31+
return user

rootfs/api/oauth.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from typing import Dict
2+
3+
import requests
4+
from django.conf import settings
5+
from authlib.integrations.requests_client import OAuth2Session
6+
7+
8+
class OAuthManager(object):
9+
def __enter__(self):
10+
return self
11+
12+
def __init__(self):
13+
self.client_id = settings.OAUTH_CLIENT_ID
14+
self.client_secret = settings.OAUTH_CLIENT_SECRET
15+
self.token_url = settings.OAUTH_ACCESS_TOKEN_URL
16+
self.api_url = settings.OAUTH_ACCESS_API_URL
17+
self.client = OAuth2Session(self.client_id, self.client_secret)
18+
19+
def fetch_token(self, username: str, password: str) -> Dict:
20+
response = self.client.fetch_token(self.token_url,
21+
username=username,
22+
password=password)
23+
return response
24+
25+
def get_user(self) -> Dict:
26+
response = self.client.get(f'{self.api_url}/users')
27+
result = response.json()
28+
return result
29+
30+
def get_email(self) -> str:
31+
response = self.client.get(f'{self.api_url}/users/emails')
32+
result = response.json()
33+
return result['email']
34+
35+
def get_user_by_token(self, token: str) -> Dict:
36+
response = requests.get(f'{self.api_url}/users', headers={
37+
'Authorization': f'Bearer {token}'
38+
})
39+
result = response.json()
40+
return result
41+
42+
def get_email_by_token(self, token: str) -> Dict:
43+
response = requests.get(f'{self.api_url}/users/emails', headers={
44+
'Authorization': f'Bearer {token}'
45+
})
46+
result = response.json()
47+
return result
48+
49+
def __exit__(self, exc_type, exc_val, exc_tb):
50+
self.client.close()

rootfs/api/serializers.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,29 @@ def create(self, validated_data):
166166
user.save()
167167
return user
168168

169+
@staticmethod
170+
def update_or_create(data):
171+
now = timezone.now()
172+
user, created = User.objects.update_or_create(
173+
username=data['username'],
174+
defaults={
175+
'email': data['email'],
176+
'first_name': data.get('first_name', ''),
177+
'last_name': data.get('first_name', ''),
178+
'last_login': now})
179+
if created:
180+
user.date_joined = now
181+
user.is_active = True
182+
if data.get('password'):
183+
user.set_password(data['password'])
184+
elif created and not data.get('password'):
185+
user.set_unusable_password()
186+
# Make the first signup an admin / superuser
187+
if not User.objects.filter(is_superuser=True).exists():
188+
user.is_superuser = user.is_staff = True
189+
user.save()
190+
return user
191+
169192

170193
class AdminUserSerializer(serializers.ModelSerializer):
171194
"""Serialize admin status for a User model."""

rootfs/api/settings/production.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@
155155
'rest_framework.permissions.IsAuthenticated',
156156
),
157157
'DEFAULT_AUTHENTICATION_CLASSES': (
158-
'rest_framework.authentication.TokenAuthentication',
158+
'api.authentication.DryccTokenAuthentication',
159159
),
160160
'DEFAULT_RENDERER_CLASSES': (
161161
'rest_framework.renderers.JSONRenderer',
@@ -434,14 +434,22 @@
434434
# check if we can register users with `drycc register`
435435
REGISTRATION_MODE = os.environ.get('REGISTRATION_MODE', 'enabled')
436436

437-
438437
DRYCC_DATABASE_URL = os.environ.get('DRYCC_DATABASE_URL', 'postgres://:@:5432/drycc')
439438
DATABASES = {
440439
'default': dj_database_url.config(default=DRYCC_DATABASE_URL, conn_max_age=600)
441440
}
442441

443442
APP_URL_REGEX = '[a-z0-9-]+'
444443

444+
# Oauth settings
445+
OAUTH_ACCESS_TOKEN_URL = os.environ.get('OAUTH2_ACCESS_TOKEN_URL', '')
446+
OAUTH_ACCESS_API_URL = os.environ.get('OAUTH2_ACCESS_API_URL', '')
447+
OAUTH_CLIENT_ID = os.environ.get('OAUTH2_CLIENT_ID', '')
448+
OAUTH_CLIENT_SECRET = os.environ.get('OAUTH2_CLIENT_SECRET', '')
449+
OAUTH_CACHE_USER_TIME = int(os.environ.get('OAUTH_CACHE_USER_TIME', 30 * 60))
450+
if OAUTH_ACCESS_TOKEN_URL:
451+
AUTHENTICATION_BACKENDS = ("api.backend.DryccOauthBackend",) + AUTHENTICATION_BACKENDS
452+
445453
# LDAP settings taken from environment variables.
446454
LDAP_ENDPOINT = os.environ.get('LDAP_ENDPOINT', '')
447455
LDAP_BIND_DN = os.environ.get('LDAP_BIND_DN', '')
@@ -537,7 +545,6 @@
537545
DRYCC_INFLUXDB_ORG = os.environ.get('DRYCC_INFLUXDB_ORG', 'root')
538546
DRYCC_INFLUXDB_TOKEN = os.environ.get('DRYCC_INFLUXDB_TOKEN', 'root')
539547

540-
541548
# Workflow-manager Configuration Options
542549
WORKFLOW_MANAGER_URL = os.environ.get('DRYCC_WORKFLOW_MANAGER_URL', None)
543550
WORKFLOW_MANAGER_TOKEN = os.environ.get('DRYCC_WORKFLOW_MANAGER_TOKEN', None)

rootfs/api/views.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
from api.models import AlreadyExists, ServiceUnavailable, DryccException, \
2323
UnprocessableEntity
2424

25-
2625
logger = logging.getLogger(__name__)
2726

2827

@@ -64,6 +63,8 @@ class UserRegistrationViewSet(GenericViewSet,
6463
def create(self, request, *args, **kwargs):
6564
if settings.LDAP_ENDPOINT:
6665
raise DryccException("You cannot register user when ldap is enabled.")
66+
if settings.OAUTH_ACCESS_TOKEN_URL:
67+
raise DryccException("You cannot register user when oauth2 is enabled.")
6768
return super(UserRegistrationViewSet, self).create(request, *args, **kwargs)
6869

6970

@@ -84,6 +85,8 @@ def list(self, request, **kwargs):
8485
def destroy(self, request, **kwargs):
8586
if settings.LDAP_ENDPOINT:
8687
raise DryccException("You cannot destroy user when ldap is enabled.")
88+
if settings.OAUTH_ACCESS_TOKEN_URL:
89+
raise DryccException("You cannot destroy user when oauth2 is enabled.")
8790
calling_obj = self.get_object()
8891
target_obj = calling_obj
8992

@@ -110,7 +113,8 @@ def passwd(self, request, **kwargs):
110113
raise DryccException("new_password is a required field")
111114
if settings.LDAP_ENDPOINT:
112115
raise DryccException("You cannot change password when ldap is enabled.")
113-
116+
if settings.OAUTH_ACCESS_TOKEN_URL:
117+
raise DryccException("You cannot change user when oauth2 is enabled.")
114118
caller_obj = self.get_object()
115119
target_obj = self.get_object()
116120
if request.data.get('username'):

rootfs/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ celery==5.0.2
2424
django_redis==4.12.1
2525
influxdb-client==1.13.0
2626
dj-database-url
27+
Authlib

0 commit comments

Comments
 (0)