Skip to content

Commit

Permalink
Oauth2 provider (ansible#133)
Browse files Browse the repository at this point in the history
Replaces ansible#44
- [x] Update requirements_all.txt
- [x] Add common model changes to prevent time changing from oauth2
system changes
- [x] Add tests
- [x] Fix linting

---------

Signed-off-by: Rick Elrod <[email protected]>
Co-authored-by: Rick Elrod <[email protected]>
  • Loading branch information
2 people authored and thedoubl3j committed Jun 17, 2024
1 parent b660136 commit a855798
Show file tree
Hide file tree
Showing 45 changed files with 2,145 additions and 18 deletions.
2 changes: 1 addition & 1 deletion ansible_base/authentication/utils/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def determine_username_from_uid(uid: str = None, authenticator: Authenticator =
new_username = get_local_username({'username': uid})
logger.info(
f'Authenticator {authenticator.name} wants to authenticate {uid} but that'
f'username is already in use by another authenticator,'
f' username is already in use by another authenticator,'
f' the user from this authenticator will be {new_username}'
)
return new_username
Expand Down
34 changes: 34 additions & 0 deletions ansible_base/lib/dynamic_config/dynamic_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,37 @@
ORG_ADMINS_CAN_SEE_ALL_USERS
except NameError:
ORG_ADMINS_CAN_SEE_ALL_USERS = True


if 'ansible_base.oauth2_provider' in INSTALLED_APPS: # noqa: F821
if 'oauth2_provider' not in INSTALLED_APPS: # noqa: F821
INSTALLED_APPS.append('oauth2_provider') # noqa: F821

try:
OAUTH2_PROVIDER # noqa: F821
except NameError:
OAUTH2_PROVIDER = {}

if 'ACCESS_TOKEN_EXPIRE_SECONDS' not in OAUTH2_PROVIDER:
OAUTH2_PROVIDER['ACCESS_TOKEN_EXPIRE_SECONDS'] = 31536000000
if 'AUTHORIZATION_CODE_EXPIRE_SECONDS' not in OAUTH2_PROVIDER:
OAUTH2_PROVIDER['AUTHORIZATION_CODE_EXPIRE_SECONDS'] = 600
if 'REFRESH_TOKEN_EXPIRE_SECONDS' not in OAUTH2_PROVIDER:
OAUTH2_PROVIDER['REFRESH_TOKEN_EXPIRE_SECONDS'] = 2628000

OAUTH2_PROVIDER['APPLICATION_MODEL'] = 'dab_oauth2_provider.OAuth2Application'
OAUTH2_PROVIDER['ACCESS_TOKEN_MODEL'] = 'dab_oauth2_provider.OAuth2AccessToken'

oauth2_authentication_class = 'ansible_base.oauth2_provider.authentication.LoggedOAuth2Authentication'
if 'DEFAULT_AUTHENTICATION_CLASSES' not in REST_FRAMEWORK: # noqa: F821
REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] = [] # noqa: F821
if oauth2_authentication_class not in REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES']: # noqa: F821
REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'].insert(0, oauth2_authentication_class) # noqa: F821

# These have to be defined for the migration to function
OAUTH2_PROVIDER_APPLICATION_MODEL = 'dab_oauth2_provider.OAuth2Application'
OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = 'dab_oauth2_provider.OAuth2AccessToken'
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "dab_oauth2_provider.OAuth2RefreshToken"
OAUTH2_PROVIDER_ID_TOKEN_MODEL = "dab_oauth2_provider.OAuth2IDToken"

ALLOW_OAUTH2_FOR_EXTERNAL_USERS = False
10 changes: 8 additions & 2 deletions ansible_base/lib/serializers/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,16 @@ def is_list_view(self) -> bool:
def _get_related(self, obj) -> dict[str, str]:
if obj is None:
return {}
related_fields = {}
view = self.context.get('view')
if view is not None and hasattr(view, 'extra_related_fields'):
related_fields.update(view.extra_related_fields(obj))
if not hasattr(obj, 'related_fields'):
logger.warning(f"Object {obj.__class__} has no related_fields method")
return {}
return obj.related_fields(self.context.get('request'))
else:
related_fields.update(obj.related_fields(self.context.get('request')))

return related_fields

def _get_summary_fields(self, obj) -> dict[str, dict]:
if obj is None:
Expand Down
10 changes: 10 additions & 0 deletions ansible_base/lib/utils/views/ansible_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,13 @@ def finalize_response(self, request, response, *args, **kwargs):
response['Warning'] = _('This resource has been deprecated and will be removed in a future release.')

return response

def extra_related_fields(self, obj):
"""
A hook for adding extra related fields to serializers which
make use of this view/viewset.
This is particularly useful for mixins which want to extend a viewset
with additional actions and provide those actions as related fields.
"""
return {}
Empty file.
3 changes: 3 additions & 0 deletions ansible_base/oauth2_provider/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.contrib import admin # noqa: F401

# Register your models here.
7 changes: 7 additions & 0 deletions ansible_base/oauth2_provider/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.apps import AppConfig


class Oauth2ProviderConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'ansible_base.oauth2_provider'
label = 'dab_oauth2_provider'
20 changes: 20 additions & 0 deletions ansible_base/oauth2_provider/authentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import logging

from django.utils.encoding import smart_str
from oauth2_provider.contrib.rest_framework import OAuth2Authentication

logger = logging.getLogger('ansible_base.oauth2_provider.authentication')


class LoggedOAuth2Authentication(OAuth2Authentication):
def authenticate(self, request):
ret = super().authenticate(request)
if ret:
user, token = ret
username = user.username if user else '<none>'
logger.info(
smart_str(u"User {} performed a {} to {} through the API using OAuth 2 token {}.".format(username, request.method, request.path, token.pk))
)
# TODO: check oauth_scopes when we have RBAC in Gateway
setattr(user, 'oauth_scopes', [x for x in token.scope.split() if x])
return ret
130 changes: 130 additions & 0 deletions ansible_base/oauth2_provider/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Generated by Django 4.2.8 on 2024-02-11 20:16

import re
import uuid

import django.core.validators
import django.db.models.deletion
import oauth2_provider.generators
from django.conf import settings
from django.db import migrations, models

import ansible_base.oauth2_provider.models.application


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
migrations.swappable_dependency(settings.ANSIBLE_BASE_ORGANIZATION_MODEL),
]

run_before = [
('oauth2_provider', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='OAuth2Application',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, editable=False, help_text='The date/time this resource was created')),
('modified', models.DateTimeField(default=None, editable=False, help_text='The date/time this resource was created')),
('name', models.CharField(blank=True, help_text='The name of this resource', max_length=255)),
('client_id', models.CharField(db_index=True, default=oauth2_provider.generators.generate_client_id, max_length=100, unique=True)),
('description', models.TextField(blank=True, default='')),
('logo_data', models.TextField(default='', editable=False, validators=[django.core.validators.RegexValidator(re.compile('.*'))])),
('client_secret', models.CharField(blank=True, db_index=True, default=oauth2_provider.generators.generate_client_secret, help_text='Used for more stringent verification of access to an application when creating a token.', max_length=1024)),
('client_type', models.CharField(choices=[('confidential', 'Confidential'), ('public', 'Public')], help_text='Set to Public or Confidential depending on how secure the client device is.', max_length=32)),
('skip_authorization', models.BooleanField(default=False, help_text='Set True to skip authorization step for completely trusted applications.')),
('authorization_grant_type', models.CharField(choices=[('authorization-code', 'Authorization code'), ('password', 'Resource owner password-based')], help_text='The Grant type the user must use for acquire tokens for this application.', max_length=32)),
('created_by', models.ForeignKey(default=None, editable=False, help_text='The user who created this resource', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='%(app_label)s_%(class)s_created+', to=settings.AUTH_USER_MODEL)),
('modified_by', models.ForeignKey(default=None, editable=False, help_text='The user who last modified this resource', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='%(app_label)s_%(class)s_modified+', to=settings.AUTH_USER_MODEL)),
('organization', models.ForeignKey(help_text='Organization containing this application.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='applications', to=settings.ANSIBLE_BASE_ORGANIZATION_MODEL)),
('algorithm', models.CharField(blank=True, choices=[('', 'No OIDC support'), ('RS256', 'RSA with SHA-2 256'), ('HS256', 'HMAC with SHA-2 256')], default='', max_length=5)),
('post_logout_redirect_uris', models.TextField(blank=True, help_text='Allowed Post Logout URIs list, space separated')),
('redirect_uris', models.TextField(blank=True, help_text='Allowed URIs list, space separated')),
('updated', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'application',
'ordering': ('organization', 'name'),
'swappable': 'OAUTH2_PROVIDER_APPLICATION_MODEL',
'unique_together': {('name', 'organization')},
},
),
migrations.CreateModel(
name='OAuth2IDToken',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, editable=False, help_text='The date/time this resource was created')),
('modified', models.DateTimeField(default=None, editable=False, help_text='The date/time this resource was created')),
('created_by', models.ForeignKey(default=None, editable=False, help_text='The user who created this resource', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='%(app_label)s_%(class)s_created+', to=settings.AUTH_USER_MODEL)),
('modified_by', models.ForeignKey(default=None, editable=False, help_text='The user who last modified this resource', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='%(app_label)s_%(class)s_modified+', to=settings.AUTH_USER_MODEL)),
('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
('expires', models.DateTimeField(default=None)),
('jti', models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='JWT Token ID')),
('scope', models.TextField(blank=True)),
('updated', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'id token',
'swappable': 'OAUTH2_PROVIDER_ID_TOKEN_MODEL',
},
),
migrations.CreateModel(
name='OAuth2RefreshToken',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(default=None, editable=False, help_text='The date/time this resource was created')),
('modified', models.DateTimeField(default=None, editable=False, help_text='The date/time this resource was created')),
('created_by', models.ForeignKey(default=None, editable=False, help_text='The user who created this resource', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='%(app_label)s_%(class)s_created+', to=settings.AUTH_USER_MODEL)),
('modified_by', models.ForeignKey(default=None, editable=False, help_text='The user who last modified this resource', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='%(app_label)s_%(class)s_modified+', to=settings.AUTH_USER_MODEL)),
('application', models.ForeignKey(default='', on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
('revoked', models.DateTimeField(null=True)),
('token', models.CharField(default='', max_length=255)),
('updated', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(default='', on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'access token',
'ordering': ('id',),
'swappable': 'OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL',
'unique_together': {('token', 'revoked')},
},
),
migrations.CreateModel(
name='OAuth2AccessToken',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, editable=False, help_text='The date/time this resource was created')),
('modified', models.DateTimeField(default=None, editable=False, help_text='The date/time this resource was created')),
('description', models.TextField(blank=True, default='')),
('last_used', models.DateTimeField(default=None, editable=False, null=True)),
('scope', models.CharField(blank=True, choices=[('read', 'read'), ('write', 'write')], default='write', help_text="Allowed scopes, further restricts user's permissions. Must be a simple space-separated string with allowed scopes ['read', 'write'].", max_length=32)),
('created_by', models.ForeignKey(default=None, editable=False, help_text='The user who created this resource', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='%(app_label)s_%(class)s_created+', to=settings.AUTH_USER_MODEL)),
('modified_by', models.ForeignKey(default=None, editable=False, help_text='The user who last modified this resource', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='%(app_label)s_%(class)s_modified+', to=settings.AUTH_USER_MODEL)),
('user', models.ForeignKey(blank=True, help_text='The user representing the token owner', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s', to=settings.AUTH_USER_MODEL)),
('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
('expires', models.DateTimeField(default=None)),
('token', models.CharField(default='', max_length=255, unique=True)),
('updated', models.DateTimeField(auto_now=True)),
('id_token', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='access_token', to=settings.OAUTH2_PROVIDER_ID_TOKEN_MODEL)),
('source_refresh_token', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='refreshed_access_token', to=settings.OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL)),
],
options={
'verbose_name': 'access token',
'ordering': ('id',),
'swappable': 'OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL',
},
),
migrations.AddField(
model_name='oauth2refreshtoken',
name='access_token',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='refresh_token', to=settings.OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL),
),
]
Loading

0 comments on commit a855798

Please sign in to comment.