From 268ca7c78a2f654bd9b6db538bd2454763a1d878 Mon Sep 17 00:00:00 2001 From: "Pablo H." Date: Tue, 26 Nov 2024 18:59:37 +0100 Subject: [PATCH] Remove oauth provider (#15666) * Remove oauth provider This removes the oauth provider functionality from awx. The oauth2_provider app and all references to it have been removed. Migrations to delete the two tables that locally overwrote oauth2_provider tables are included. This change does not include migrations to delete the tables provided by the oauth2_provider app. Also not included here are changes to awxkit, awx_collection or the ui. * Fix linters * Update migrations after rebase * Update collection tests for auth changes The changes in https://github.com/ansible/awx/pull/15554 will cause a few collection tests to fail, depending on what the test configuration is. This changes the tests to look for a specific warning rather than counting the number of warnings emitted. * Update migration * Removed unused oauth_scopes references --------- Co-authored-by: Mike Graves Co-authored-by: Alan Rominger --- awx/__init__.py | 25 -- awx/api/authentication.py | 16 - awx/api/conf.py | 23 -- awx/api/fields.py | 14 - awx/api/generics.py | 6 - awx/api/serializers.py | 208 +----------- .../api/api_o_auth_authorization_root_view.md | 114 ------- awx/api/urls/oauth2.py | 27 -- awx/api/urls/oauth2_root.py | 45 --- awx/api/urls/organization.py | 3 +- awx/api/urls/urls.py | 12 - awx/api/urls/user.py | 8 - awx/api/views/__init__.py | 118 ------- awx/api/views/root.py | 20 +- awx/main/access.py | 94 +----- .../management/commands/cleanup_tokens.py | 26 -- .../commands/create_oauth2_token.py | 34 -- .../commands/regenerate_secret_key.py | 10 +- .../commands/revoke_oauth2_tokens.py | 38 --- ...330_add_oauth_activity_stream_registrar.py | 15 +- .../0031_v330_encrypt_oauth2_secret.py | 5 +- .../migrations/0033_v330_oauth_help_text.py | 3 +- .../0041_v330_update_oauth_refreshtoken.py | 19 +- .../migrations/0183_pre_django_upgrade.py | 11 - ...th2application_unique_together_and_more.py | 39 +++ awx/main/models/__init__.py | 23 -- awx/main/models/activity_stream.py | 2 - awx/main/models/oauth.py | 125 -------- awx/main/signals.py | 18 -- awx/main/tests/functional/api/test_oauth.py | 296 ------------------ .../commands/test_oauth2_token_create.py | 44 --- .../commands/test_oauth2_token_revoke.py | 62 ---- .../commands/test_secret_key_regeneration.py | 16 - awx/main/tests/functional/conftest.py | 6 - awx/main/tests/functional/test_rbac_oauth.py | 247 --------------- .../api/serializers/test_token_serializer.py | 8 - awx/main/tests/unit/api/test_filters.py | 5 - awx/main/utils/common.py | 2 +- awx/settings/defaults.py | 12 - awx_collection/test/awx/test_job_template.py | 2 +- awx_collection/test/awx/test_project.py | 2 +- awx_collection/test/awx/test_user.py | 2 +- docs/docsite/rst/rest_api/authentication.rst | 12 +- docs/docsite/rst/rest_api/awx-manage.rst | 65 +--- 44 files changed, 61 insertions(+), 1821 deletions(-) delete mode 100644 awx/api/templates/api/api_o_auth_authorization_root_view.md delete mode 100644 awx/api/urls/oauth2.py delete mode 100644 awx/api/urls/oauth2_root.py delete mode 100644 awx/main/management/commands/cleanup_tokens.py delete mode 100644 awx/main/management/commands/create_oauth2_token.py delete mode 100644 awx/main/management/commands/revoke_oauth2_tokens.py create mode 100644 awx/main/migrations/0199_alter_oauth2application_unique_together_and_more.py delete mode 100644 awx/main/models/oauth.py delete mode 100644 awx/main/tests/functional/api/test_oauth.py delete mode 100644 awx/main/tests/functional/commands/test_oauth2_token_create.py delete mode 100644 awx/main/tests/functional/commands/test_oauth2_token_revoke.py delete mode 100644 awx/main/tests/functional/test_rbac_oauth.py delete mode 100644 awx/main/tests/unit/api/serializers/test_token_serializer.py diff --git a/awx/__init__.py b/awx/__init__.py index be80b58614..6b2f809c30 100644 --- a/awx/__init__.py +++ b/awx/__init__.py @@ -60,25 +60,6 @@ else: from django.db import connection -def oauth2_getattribute(self, attr): - # Custom method to override - # oauth2_provider.settings.OAuth2ProviderSettings.__getattribute__ - from django.conf import settings - from oauth2_provider.settings import DEFAULTS - - val = None - if (isinstance(attr, str)) and (attr in DEFAULTS) and (not attr.startswith('_')): - # certain Django OAuth Toolkit migrations actually reference - # setting lookups for references to model classes (e.g., - # oauth2_settings.REFRESH_TOKEN_MODEL) - # If we're doing an OAuth2 setting lookup *while running* a migration, - # don't do our usual database settings lookup - val = settings.OAUTH2_PROVIDER.get(attr) - if val is None: - val = object.__getattribute__(self, attr) - return val - - def prepare_env(): # Update the default settings environment variable based on current mode. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'awx.settings.%s' % MODE) @@ -89,12 +70,6 @@ def prepare_env(): if not settings.DEBUG: # pragma: no cover warnings.simplefilter('ignore', DeprecationWarning) - # Monkeypatch Oauth2 toolkit settings class to check for settings - # in django.conf settings each time, not just once during import - import oauth2_provider.settings - - oauth2_provider.settings.OAuth2ProviderSettings.__getattribute__ = oauth2_getattribute - def manage(): # Prepare the AWX environment. diff --git a/awx/api/authentication.py b/awx/api/authentication.py index 48fc00db44..430c8098fd 100644 --- a/awx/api/authentication.py +++ b/awx/api/authentication.py @@ -11,9 +11,6 @@ from django.utils.encoding import smart_str # Django REST Framework from rest_framework import authentication -# Django-OAuth-Toolkit -from oauth2_provider.contrib.rest_framework import OAuth2Authentication - logger = logging.getLogger('awx.api.authentication') @@ -36,16 +33,3 @@ class LoggedBasicAuthentication(authentication.BasicAuthentication): class SessionAuthentication(authentication.SessionAuthentication): def authenticate_header(self, request): return 'Session' - - -class LoggedOAuth2Authentication(OAuth2Authentication): - def authenticate(self, request): - ret = super(LoggedOAuth2Authentication, self).authenticate(request) - if ret: - user, token = ret - username = user.username if user else '' - logger.info( - smart_str(u"User {} performed a {} to {} through the API using OAuth 2 token {}.".format(username, request.method, request.path, token.pk)) - ) - setattr(user, 'oauth_scopes', [x for x in token.scope.split() if x]) - return ret diff --git a/awx/api/conf.py b/awx/api/conf.py index a1ed832ff6..12baf3f3bb 100644 --- a/awx/api/conf.py +++ b/awx/api/conf.py @@ -6,8 +6,6 @@ from rest_framework import serializers # AWX from awx.conf import fields, register, register_validate -from awx.api.fields import OAuth2ProviderField -from oauth2_provider.settings import oauth2_settings register( @@ -46,27 +44,6 @@ register( category=_('Authentication'), category_slug='authentication', ) -register( - 'OAUTH2_PROVIDER', - field_class=OAuth2ProviderField, - default={ - 'ACCESS_TOKEN_EXPIRE_SECONDS': oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS, - 'AUTHORIZATION_CODE_EXPIRE_SECONDS': oauth2_settings.AUTHORIZATION_CODE_EXPIRE_SECONDS, - 'REFRESH_TOKEN_EXPIRE_SECONDS': oauth2_settings.REFRESH_TOKEN_EXPIRE_SECONDS, - }, - label=_('OAuth 2 Timeout Settings'), - help_text=_( - 'Dictionary for customizing OAuth 2 timeouts, available items are ' - '`ACCESS_TOKEN_EXPIRE_SECONDS`, the duration of access tokens in the number ' - 'of seconds, `AUTHORIZATION_CODE_EXPIRE_SECONDS`, the duration of ' - 'authorization codes in the number of seconds, and `REFRESH_TOKEN_EXPIRE_SECONDS`, ' - 'the duration of refresh tokens, after expired access tokens, ' - 'in the number of seconds.' - ), - category=_('Authentication'), - category_slug='authentication', - unit=_('seconds'), -) register( 'LOGIN_REDIRECT_OVERRIDE', field_class=fields.CharField, diff --git a/awx/api/fields.py b/awx/api/fields.py index 1fab90065e..0a85d8f155 100644 --- a/awx/api/fields.py +++ b/awx/api/fields.py @@ -9,7 +9,6 @@ from django.core.exceptions import ObjectDoesNotExist from rest_framework import serializers # AWX -from awx.conf import fields from awx.main.models import Credential __all__ = ['BooleanNullField', 'CharNullField', 'ChoiceNullField', 'VerbatimField'] @@ -79,19 +78,6 @@ class VerbatimField(serializers.Field): return value -class OAuth2ProviderField(fields.DictField): - default_error_messages = {'invalid_key_names': _('Invalid key names: {invalid_key_names}')} - valid_key_names = {'ACCESS_TOKEN_EXPIRE_SECONDS', 'AUTHORIZATION_CODE_EXPIRE_SECONDS', 'REFRESH_TOKEN_EXPIRE_SECONDS'} - child = fields.IntegerField(min_value=1) - - def to_internal_value(self, data): - data = super(OAuth2ProviderField, self).to_internal_value(data) - invalid_flags = set(data.keys()) - self.valid_key_names - if invalid_flags: - self.fail('invalid_key_names', invalid_key_names=', '.join(list(invalid_flags))) - return data - - class DeprecatedCredentialField(serializers.IntegerField): def __init__(self, **kwargs): kwargs['allow_null'] = True diff --git a/awx/api/generics.py b/awx/api/generics.py index 9c400d0b38..ba7ab3cf27 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -374,12 +374,6 @@ class APIView(views.APIView): kwargs.pop('version') return super(APIView, self).dispatch(request, *args, **kwargs) - def check_permissions(self, request): - if request.method not in ('GET', 'OPTIONS', 'HEAD'): - if 'write' not in getattr(request.user, 'oauth_scopes', ['write']): - raise PermissionDenied() - return super(APIView, self).check_permissions(request) - class GenericAPIView(generics.GenericAPIView, APIView): # Base class for all model-based views. diff --git a/awx/api/serializers.py b/awx/api/serializers.py index a5b5f87eab..1a1ee393fa 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -10,10 +10,6 @@ from collections import Counter, OrderedDict from datetime import timedelta from uuid import uuid4 -# OAuth2 -from oauthlib import oauth2 -from oauthlib.common import generate_token - # Jinja from jinja2 import sandbox, StrictUndefined from jinja2.exceptions import TemplateSyntaxError, UndefinedError, SecurityError @@ -50,7 +46,7 @@ from ansible_base.rbac import permission_registry # AWX from awx.main.access import get_user_capabilities -from awx.main.constants import ACTIVE_STATES, CENSOR_VALUE, org_role_to_permission +from awx.main.constants import ACTIVE_STATES, org_role_to_permission from awx.main.models import ( ActivityStream, AdHocCommand, @@ -79,14 +75,11 @@ from awx.main.models import ( Label, Notification, NotificationTemplate, - OAuth2AccessToken, - OAuth2Application, Organization, Project, ProjectUpdate, ProjectUpdateEvent, ReceptorAddress, - RefreshToken, Role, Schedule, SystemJob, @@ -1060,9 +1053,6 @@ class UserSerializer(BaseSerializer): roles=self.reverse('api:user_roles_list', kwargs={'pk': obj.pk}), activity_stream=self.reverse('api:user_activity_stream_list', kwargs={'pk': obj.pk}), access_list=self.reverse('api:user_access_list', kwargs={'pk': obj.pk}), - tokens=self.reverse('api:o_auth2_token_list', kwargs={'pk': obj.pk}), - authorized_tokens=self.reverse('api:user_authorized_token_list', kwargs={'pk': obj.pk}), - personal_tokens=self.reverse('api:user_personal_token_list', kwargs={'pk': obj.pk}), ) ) return res @@ -1079,199 +1069,6 @@ class UserActivityStreamSerializer(UserSerializer): fields = ('*', '-is_system_auditor') -class BaseOAuth2TokenSerializer(BaseSerializer): - refresh_token = serializers.SerializerMethodField() - token = serializers.SerializerMethodField() - ALLOWED_SCOPES = ['read', 'write'] - - class Meta: - model = OAuth2AccessToken - fields = ('*', '-name', 'description', 'user', 'token', 'refresh_token', 'application', 'expires', 'scope') - read_only_fields = ('user', 'token', 'expires', 'refresh_token') - extra_kwargs = {'scope': {'allow_null': False, 'required': False}, 'user': {'allow_null': False, 'required': True}} - - def get_token(self, obj): - request = self.context.get('request', None) - try: - if request.method == 'POST': - return obj.token - else: - return CENSOR_VALUE - except ObjectDoesNotExist: - return '' - - def get_refresh_token(self, obj): - request = self.context.get('request', None) - try: - if not obj.refresh_token: - return None - elif request.method == 'POST': - return getattr(obj.refresh_token, 'token', '') - else: - return CENSOR_VALUE - except ObjectDoesNotExist: - return None - - def get_related(self, obj): - ret = super(BaseOAuth2TokenSerializer, self).get_related(obj) - if obj.user: - ret['user'] = self.reverse('api:user_detail', kwargs={'pk': obj.user.pk}) - if obj.application: - ret['application'] = self.reverse('api:o_auth2_application_detail', kwargs={'pk': obj.application.pk}) - ret['activity_stream'] = self.reverse('api:o_auth2_token_activity_stream_list', kwargs={'pk': obj.pk}) - return ret - - def _is_valid_scope(self, value): - if not value or (not isinstance(value, str)): - return False - words = value.split() - for word in words: - if words.count(word) > 1: - return False # do not allow duplicates - if word not in self.ALLOWED_SCOPES: - return False - return True - - def validate_scope(self, value): - if not self._is_valid_scope(value): - raise serializers.ValidationError(_('Must be a simple space-separated string with allowed scopes {}.').format(self.ALLOWED_SCOPES)) - return value - - def create(self, validated_data): - validated_data['user'] = self.context['request'].user - try: - return super(BaseOAuth2TokenSerializer, self).create(validated_data) - except oauth2.AccessDeniedError as e: - raise PermissionDenied(str(e)) - - -class UserAuthorizedTokenSerializer(BaseOAuth2TokenSerializer): - class Meta: - extra_kwargs = { - 'scope': {'allow_null': False, 'required': False}, - 'user': {'allow_null': False, 'required': True}, - 'application': {'allow_null': False, 'required': True}, - } - - def create(self, validated_data): - current_user = self.context['request'].user - validated_data['token'] = generate_token() - validated_data['expires'] = now() + timedelta(seconds=settings.OAUTH2_PROVIDER['ACCESS_TOKEN_EXPIRE_SECONDS']) - obj = super(UserAuthorizedTokenSerializer, self).create(validated_data) - obj.save() - if obj.application: - RefreshToken.objects.create(user=current_user, token=generate_token(), application=obj.application, access_token=obj) - return obj - - -class OAuth2TokenSerializer(BaseOAuth2TokenSerializer): - def create(self, validated_data): - current_user = self.context['request'].user - validated_data['token'] = generate_token() - validated_data['expires'] = now() + timedelta(seconds=settings.OAUTH2_PROVIDER['ACCESS_TOKEN_EXPIRE_SECONDS']) - obj = super(OAuth2TokenSerializer, self).create(validated_data) - if obj.application and obj.application.user: - obj.user = obj.application.user - obj.save() - if obj.application: - RefreshToken.objects.create(user=current_user, token=generate_token(), application=obj.application, access_token=obj) - return obj - - -class OAuth2TokenDetailSerializer(OAuth2TokenSerializer): - class Meta: - read_only_fields = ('*', 'user', 'application') - - -class UserPersonalTokenSerializer(BaseOAuth2TokenSerializer): - class Meta: - read_only_fields = ('user', 'token', 'expires', 'application') - - def create(self, validated_data): - validated_data['token'] = generate_token() - validated_data['expires'] = now() + timedelta(seconds=settings.OAUTH2_PROVIDER['ACCESS_TOKEN_EXPIRE_SECONDS']) - validated_data['application'] = None - obj = super(UserPersonalTokenSerializer, self).create(validated_data) - obj.save() - return obj - - -class OAuth2ApplicationSerializer(BaseSerializer): - show_capabilities = ['edit', 'delete'] - - class Meta: - model = OAuth2Application - fields = ( - '*', - 'description', - '-user', - 'client_id', - 'client_secret', - 'client_type', - 'redirect_uris', - 'authorization_grant_type', - 'skip_authorization', - 'organization', - ) - read_only_fields = ('client_id', 'client_secret') - read_only_on_update_fields = ('user', 'authorization_grant_type') - extra_kwargs = { - 'user': {'allow_null': True, 'required': False}, - 'organization': {'allow_null': False}, - 'authorization_grant_type': {'allow_null': False, 'label': _('Authorization Grant Type')}, - 'client_secret': {'label': _('Client Secret')}, - 'client_type': {'label': _('Client Type')}, - 'redirect_uris': {'label': _('Redirect URIs')}, - 'skip_authorization': {'label': _('Skip Authorization')}, - } - - def to_representation(self, obj): - ret = super(OAuth2ApplicationSerializer, self).to_representation(obj) - request = self.context.get('request', None) - if request.method != 'POST' and obj.client_type == 'confidential': - ret['client_secret'] = CENSOR_VALUE - if obj.client_type == 'public': - ret.pop('client_secret', None) - return ret - - def get_related(self, obj): - res = super(OAuth2ApplicationSerializer, self).get_related(obj) - res.update( - dict( - tokens=self.reverse('api:o_auth2_application_token_list', kwargs={'pk': obj.pk}), - activity_stream=self.reverse('api:o_auth2_application_activity_stream_list', kwargs={'pk': obj.pk}), - ) - ) - if obj.organization_id: - res.update( - dict( - organization=self.reverse('api:organization_detail', kwargs={'pk': obj.organization_id}), - ) - ) - return res - - def get_modified(self, obj): - if obj is None: - return None - return obj.updated - - def _summary_field_tokens(self, obj): - token_list = [{'id': x.pk, 'token': CENSOR_VALUE, 'scope': x.scope} for x in obj.oauth2accesstoken_set.all()[:10]] - if has_model_field_prefetched(obj, 'oauth2accesstoken_set'): - token_count = len(obj.oauth2accesstoken_set.all()) - else: - if len(token_list) < 10: - token_count = len(token_list) - else: - token_count = obj.oauth2accesstoken_set.count() - return {'count': token_count, 'results': token_list} - - def get_summary_fields(self, obj): - ret = super(OAuth2ApplicationSerializer, self).get_summary_fields(obj) - ret['tokens'] = self._summary_field_tokens(obj) - return ret - - class OrganizationSerializer(BaseSerializer): show_capabilities = ['edit', 'delete'] @@ -1292,7 +1089,6 @@ class OrganizationSerializer(BaseSerializer): admins=self.reverse('api:organization_admins_list', kwargs={'pk': obj.pk}), teams=self.reverse('api:organization_teams_list', kwargs={'pk': obj.pk}), credentials=self.reverse('api:organization_credential_list', kwargs={'pk': obj.pk}), - applications=self.reverse('api:organization_applications_list', kwargs={'pk': obj.pk}), activity_stream=self.reverse('api:organization_activity_stream_list', kwargs={'pk': obj.pk}), notification_templates=self.reverse('api:organization_notification_templates_list', kwargs={'pk': obj.pk}), notification_templates_started=self.reverse('api:organization_notification_templates_started_list', kwargs={'pk': obj.pk}), @@ -6084,8 +5880,6 @@ class ActivityStreamSerializer(BaseSerializer): ('workflow_job_template_node', ('id', 'unified_job_template_id')), ('label', ('id', 'name', 'organization_id')), ('notification', ('id', 'status', 'notification_type', 'notification_template_id')), - ('o_auth2_access_token', ('id', 'user_id', 'description', 'application_id', 'scope')), - ('o_auth2_application', ('id', 'name', 'description')), ('credential_type', ('id', 'name', 'description', 'kind', 'managed')), ('ad_hoc_command', ('id', 'name', 'status', 'limit')), ('workflow_approval', ('id', 'name', 'unified_job_id')), diff --git a/awx/api/templates/api/api_o_auth_authorization_root_view.md b/awx/api/templates/api/api_o_auth_authorization_root_view.md deleted file mode 100644 index 328daaab7a..0000000000 --- a/awx/api/templates/api/api_o_auth_authorization_root_view.md +++ /dev/null @@ -1,114 +0,0 @@ -# Token Handling using OAuth2 - -This page lists OAuth 2 utility endpoints used for authorization, token refresh and revoke. -Note endpoints other than `/api/o/authorize/` are not meant to be used in browsers and do not -support HTTP GET. The endpoints here strictly follow -[RFC specs for OAuth2](https://tools.ietf.org/html/rfc6749), so please use that for detailed -reference. Note AWX net location default to `http://localhost:8013` in examples: - - -## Create Token for an Application using Authorization code grant type -Given an application "AuthCodeApp" of grant type `authorization-code`, -from the client app, the user makes a GET to the Authorize endpoint with - -* `response_type` -* `client_id` -* `redirect_uris` -* `scope` - -AWX will respond with the authorization `code` and `state` -to the redirect_uri specified in the application. The client application will then make a POST to the -`api/o/token/` endpoint on AWX with - -* `code` -* `client_id` -* `client_secret` -* `grant_type` -* `redirect_uri` - -AWX will respond with the `access_token`, `token_type`, `refresh_token`, and `expires_in`. For more -information on testing this flow, refer to [django-oauth-toolkit](http://django-oauth-toolkit.readthedocs.io/en/latest/tutorial/tutorial_01.html#test-your-authorization-server). - - -## Create Token for an Application using Password grant type - -Log in is not required for `password` grant type, so a simple `curl` can be used to acquire a personal access token -via `/api/o/token/` with - -* `grant_type`: Required to be "password" -* `username` -* `password` -* `client_id`: Associated application must have grant_type "password" -* `client_secret` - -For example: - -```bash -curl -X POST \ - -H "Content-Type: application/x-www-form-urlencoded" \ - -d "grant_type=password&username=&password=&scope=read" \ - -u "gwSPoasWSdNkMDtBN3Hu2WYQpPWCO9SwUEsKK22l:fI6ZpfocHYBGfm1tP92r0yIgCyfRdDQt0Tos9L8a4fNsJjQQMwp9569e -IaUBsaVDgt2eiwOGe0bg5m5vCSstClZmtdy359RVx2rQK5YlIWyPlrolpt2LEpVeKXWaiybo" \ - http://localhost:8013/api/o/token/ -i -``` -In the above post request, parameters `username` and `password` are username and password of the related -AWX user of the underlying application, and the authentication information is of format -`:`, where `client_id` and `client_secret` are the corresponding fields of -underlying application. - -Upon success, access token, refresh token and other information are given in the response body in JSON -format: - -```text -{ -"access_token": "9epHOqHhnXUcgYK8QanOmUQPSgX92g", -"token_type": "Bearer", -"expires_in": 31536000000, -"refresh_token": "jMRX6QvzOTf046KHee3TU5mT3nyXsz", -"scope": "read" -} -``` - - -## Refresh an existing access token - -The `/api/o/token/` endpoint is used for refreshing access token: -```bash -curl -X POST \ - -H "Content-Type: application/x-www-form-urlencoded" \ - -d "grant_type=refresh_token&refresh_token=AL0NK9TTpv0qp54dGbC4VUZtsZ9r8z" \ - -u "gwSPoasWSdNkMDtBN3Hu2WYQpPWCO9SwUEsKK22l:fI6ZpfocHYBGfm1tP92r0yIgCyfRdDQt0Tos9L8a4fNsJjQQMwp9569eIaUBsaVDgt2eiwOGe0bg5m5vCSstClZmtdy359RVx2rQK5YlIWyPlrolpt2LEpVeKXWaiybo" \ - http://localhost:8013/api/o/token/ -i -``` -In the above post request, `refresh_token` is provided by `refresh_token` field of the access token -above. The authentication information is of format `:`, where `client_id` -and `client_secret` are the corresponding fields of underlying related application of the access token. - -Upon success, the new (refreshed) access token with the same scope information as the previous one is -given in the response body in JSON format: -```text -{ -"access_token": "NDInWxGJI4iZgqpsreujjbvzCfJqgR", -"token_type": "Bearer", -"expires_in": 31536000000, -"refresh_token": "DqOrmz8bx3srlHkZNKmDpqA86bnQkT", -"scope": "read write" -} -``` -Internally, the refresh operation deletes the existing token and a new token is created immediately -after, with information like scope and related application identical to the original one. We can -verify by checking the new token is present at the `api/v2/tokens` endpoint. - -## Revoke an access token -Revoking an access token is the same as deleting the token resource object. -Revoking is done by POSTing to `/api/o/revoke_token/` with the token to revoke as parameter: - -```bash -curl -X POST -d "token=rQONsve372fQwuc2pn76k3IHDCYpi7" \ - -H "Content-Type: application/x-www-form-urlencoded" \ - -u "gwSPoasWSdNkMDtBN3Hu2WYQpPWCO9SwUEsKK22l:fI6ZpfocHYBGfm1tP92r0yIgCyfRdDQt0Tos9L8a4fNsJjQQMwp9569eIaUBsaVDgt2eiwOGe0bg5m5vCSstClZmtdy359RVx2rQK5YlIWyPlrolpt2LEpVeKXWaiybo" \ - http://localhost:8013/api/o/revoke_token/ -i -``` -`200 OK` means a successful delete. - - diff --git a/awx/api/urls/oauth2.py b/awx/api/urls/oauth2.py deleted file mode 100644 index f613b34a0b..0000000000 --- a/awx/api/urls/oauth2.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (c) 2017 Ansible, Inc. -# All Rights Reserved. - -from django.urls import re_path - -from awx.api.views import ( - OAuth2ApplicationList, - OAuth2ApplicationDetail, - ApplicationOAuth2TokenList, - OAuth2ApplicationActivityStreamList, - OAuth2TokenList, - OAuth2TokenDetail, - OAuth2TokenActivityStreamList, -) - - -urls = [ - re_path(r'^applications/$', OAuth2ApplicationList.as_view(), name='o_auth2_application_list'), - re_path(r'^applications/(?P[0-9]+)/$', OAuth2ApplicationDetail.as_view(), name='o_auth2_application_detail'), - re_path(r'^applications/(?P[0-9]+)/tokens/$', ApplicationOAuth2TokenList.as_view(), name='o_auth2_application_token_list'), - re_path(r'^applications/(?P[0-9]+)/activity_stream/$', OAuth2ApplicationActivityStreamList.as_view(), name='o_auth2_application_activity_stream_list'), - re_path(r'^tokens/$', OAuth2TokenList.as_view(), name='o_auth2_token_list'), - re_path(r'^tokens/(?P[0-9]+)/$', OAuth2TokenDetail.as_view(), name='o_auth2_token_detail'), - re_path(r'^tokens/(?P[0-9]+)/activity_stream/$', OAuth2TokenActivityStreamList.as_view(), name='o_auth2_token_activity_stream_list'), -] - -__all__ = ['urls'] diff --git a/awx/api/urls/oauth2_root.py b/awx/api/urls/oauth2_root.py deleted file mode 100644 index 1a5a444bc6..0000000000 --- a/awx/api/urls/oauth2_root.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) 2017 Ansible, Inc. -# All Rights Reserved. -from datetime import timedelta - -from django.utils.timezone import now -from django.conf import settings -from django.urls import re_path - -from oauthlib import oauth2 -from oauth2_provider import views - -from awx.main.models import RefreshToken -from awx.api.views.root import ApiOAuthAuthorizationRootView - - -class TokenView(views.TokenView): - def create_token_response(self, request): - # Django OAuth2 Toolkit has a bug whereby refresh tokens are *never* - # properly expired (ugh): - # - # https://github.com/jazzband/django-oauth-toolkit/issues/746 - # - # This code detects and auto-expires them on refresh grant - # requests. - if request.POST.get('grant_type') == 'refresh_token' and 'refresh_token' in request.POST: - refresh_token = RefreshToken.objects.filter(token=request.POST['refresh_token']).first() - if refresh_token: - expire_seconds = settings.OAUTH2_PROVIDER.get('REFRESH_TOKEN_EXPIRE_SECONDS', 0) - if refresh_token.created + timedelta(seconds=expire_seconds) < now(): - return request.build_absolute_uri(), {}, 'The refresh token has expired.', '403' - try: - return super(TokenView, self).create_token_response(request) - except oauth2.AccessDeniedError as e: - return request.build_absolute_uri(), {}, str(e), '403' - - -urls = [ - re_path(r'^$', ApiOAuthAuthorizationRootView.as_view(), name='oauth_authorization_root_view'), - re_path(r"^authorize/$", views.AuthorizationView.as_view(), name="authorize"), - re_path(r"^token/$", TokenView.as_view(), name="token"), - re_path(r"^revoke_token/$", views.RevokeTokenView.as_view(), name="revoke-token"), -] - - -__all__ = ['urls'] diff --git a/awx/api/urls/organization.py b/awx/api/urls/organization.py index a75ee9d3cc..bbfe98af4a 100644 --- a/awx/api/urls/organization.py +++ b/awx/api/urls/organization.py @@ -25,7 +25,7 @@ from awx.api.views.organization import ( OrganizationObjectRolesList, OrganizationAccessList, ) -from awx.api.views import OrganizationCredentialList, OrganizationApplicationList +from awx.api.views import OrganizationCredentialList urls = [ @@ -66,7 +66,6 @@ urls = [ re_path(r'^(?P[0-9]+)/galaxy_credentials/$', OrganizationGalaxyCredentialsList.as_view(), name='organization_galaxy_credentials_list'), re_path(r'^(?P[0-9]+)/object_roles/$', OrganizationObjectRolesList.as_view(), name='organization_object_roles_list'), re_path(r'^(?P[0-9]+)/access_list/$', OrganizationAccessList.as_view(), name='organization_access_list'), - re_path(r'^(?P[0-9]+)/applications/$', OrganizationApplicationList.as_view(), name='organization_applications_list'), ] __all__ = ['urls'] diff --git a/awx/api/urls/urls.py b/awx/api/urls/urls.py index 3f257da956..909a7f0287 100644 --- a/awx/api/urls/urls.py +++ b/awx/api/urls/urls.py @@ -25,10 +25,6 @@ from awx.api.views import ( JobTemplateCredentialsList, SchedulePreview, ScheduleZoneInfo, - OAuth2ApplicationList, - OAuth2TokenList, - ApplicationOAuth2TokenList, - OAuth2ApplicationDetail, HostMetricSummaryMonthlyList, ) @@ -79,8 +75,6 @@ from .schedule import urls as schedule_urls from .activity_stream import urls as activity_stream_urls from .instance import urls as instance_urls from .instance_group import urls as instance_group_urls -from .oauth2 import urls as oauth2_urls -from .oauth2_root import urls as oauth2_root_urls from .workflow_approval_template import urls as workflow_approval_template_urls from .workflow_approval import urls as workflow_approval_urls from .analytics import urls as analytics_urls @@ -95,11 +89,6 @@ v2_urls = [ re_path(r'^job_templates/(?P[0-9]+)/credentials/$', JobTemplateCredentialsList.as_view(), name='job_template_credentials_list'), re_path(r'^schedules/preview/$', SchedulePreview.as_view(), name='schedule_rrule'), re_path(r'^schedules/zoneinfo/$', ScheduleZoneInfo.as_view(), name='schedule_zoneinfo'), - re_path(r'^applications/$', OAuth2ApplicationList.as_view(), name='o_auth2_application_list'), - re_path(r'^applications/(?P[0-9]+)/$', OAuth2ApplicationDetail.as_view(), name='o_auth2_application_detail'), - re_path(r'^applications/(?P[0-9]+)/tokens/$', ApplicationOAuth2TokenList.as_view(), name='application_o_auth2_token_list'), - re_path(r'^tokens/$', OAuth2TokenList.as_view(), name='o_auth2_token_list'), - re_path(r'^', include(oauth2_urls)), re_path(r'^metrics/$', MetricsView.as_view(), name='metrics_view'), re_path(r'^ping/$', ApiV2PingView.as_view(), name='api_v2_ping_view'), re_path(r'^config/$', ApiV2ConfigView.as_view(), name='api_v2_config_view'), @@ -164,7 +153,6 @@ urlpatterns = [ re_path(r'^(?P(v2))/', include(v2_urls)), re_path(r'^login/$', LoggedLoginView.as_view(template_name='rest_framework/login.html', extra_context={'inside_login_context': True}), name='login'), re_path(r'^logout/$', LoggedLogoutView.as_view(next_page='/api/', redirect_field_name='next'), name='logout'), - re_path(r'^o/', include(oauth2_root_urls)), ] if MODE == 'development': # Only include these if we are in the development environment diff --git a/awx/api/urls/user.py b/awx/api/urls/user.py index 39bc07aec4..ffdb03453a 100644 --- a/awx/api/urls/user.py +++ b/awx/api/urls/user.py @@ -14,10 +14,6 @@ from awx.api.views import ( UserRolesList, UserActivityStreamList, UserAccessList, - OAuth2ApplicationList, - OAuth2UserTokenList, - UserPersonalTokenList, - UserAuthorizedTokenList, ) urls = [ @@ -31,10 +27,6 @@ urls = [ re_path(r'^(?P[0-9]+)/roles/$', UserRolesList.as_view(), name='user_roles_list'), re_path(r'^(?P[0-9]+)/activity_stream/$', UserActivityStreamList.as_view(), name='user_activity_stream_list'), re_path(r'^(?P[0-9]+)/access_list/$', UserAccessList.as_view(), name='user_access_list'), - re_path(r'^(?P[0-9]+)/applications/$', OAuth2ApplicationList.as_view(), name='o_auth2_application_list'), - re_path(r'^(?P[0-9]+)/tokens/$', OAuth2UserTokenList.as_view(), name='o_auth2_token_list'), - re_path(r'^(?P[0-9]+)/authorized_tokens/$', UserAuthorizedTokenList.as_view(), name='user_authorized_token_list'), - re_path(r'^(?P[0-9]+)/personal_tokens/$', UserPersonalTokenList.as_view(), name='user_personal_token_list'), ] __all__ = ['urls'] diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 6619047863..e769b49ee3 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -50,9 +50,6 @@ from rest_framework_yaml.renderers import YAMLRenderer # ansi2html from ansi2html import Ansi2HTMLConverter -# Django OAuth Toolkit -from oauth2_provider.models import get_access_token_model - import pytz from wsgiref.util import FileWrapper @@ -1148,121 +1145,6 @@ class UserMeList(ListAPIView): return self.model.objects.filter(pk=self.request.user.pk) -class OAuth2ApplicationList(ListCreateAPIView): - name = _("OAuth 2 Applications") - - model = models.OAuth2Application - serializer_class = serializers.OAuth2ApplicationSerializer - swagger_topic = 'Authentication' - - -class OAuth2ApplicationDetail(RetrieveUpdateDestroyAPIView): - name = _("OAuth 2 Application Detail") - - model = models.OAuth2Application - serializer_class = serializers.OAuth2ApplicationSerializer - swagger_topic = 'Authentication' - - def update_raw_data(self, data): - data.pop('client_secret', None) - return super(OAuth2ApplicationDetail, self).update_raw_data(data) - - -class ApplicationOAuth2TokenList(SubListCreateAPIView): - name = _("OAuth 2 Application Tokens") - - model = models.OAuth2AccessToken - serializer_class = serializers.OAuth2TokenSerializer - parent_model = models.OAuth2Application - relationship = 'oauth2accesstoken_set' - parent_key = 'application' - swagger_topic = 'Authentication' - - -class OAuth2ApplicationActivityStreamList(SubListAPIView): - model = models.ActivityStream - serializer_class = serializers.ActivityStreamSerializer - parent_model = models.OAuth2Application - relationship = 'activitystream_set' - swagger_topic = 'Authentication' - search_fields = ('changes',) - - -class OAuth2TokenList(ListCreateAPIView): - name = _("OAuth2 Tokens") - - model = models.OAuth2AccessToken - serializer_class = serializers.OAuth2TokenSerializer - swagger_topic = 'Authentication' - - -class OAuth2UserTokenList(SubListCreateAPIView): - name = _("OAuth2 User Tokens") - - model = models.OAuth2AccessToken - serializer_class = serializers.OAuth2TokenSerializer - parent_model = models.User - relationship = 'main_oauth2accesstoken' - parent_key = 'user' - swagger_topic = 'Authentication' - - -class UserAuthorizedTokenList(SubListCreateAPIView): - name = _("OAuth2 User Authorized Access Tokens") - - model = models.OAuth2AccessToken - serializer_class = serializers.UserAuthorizedTokenSerializer - parent_model = models.User - relationship = 'oauth2accesstoken_set' - parent_key = 'user' - swagger_topic = 'Authentication' - - def get_queryset(self): - return get_access_token_model().objects.filter(application__isnull=False, user=self.request.user) - - -class OrganizationApplicationList(SubListCreateAPIView): - name = _("Organization OAuth2 Applications") - - model = models.OAuth2Application - serializer_class = serializers.OAuth2ApplicationSerializer - parent_model = models.Organization - relationship = 'applications' - parent_key = 'organization' - swagger_topic = 'Authentication' - - -class UserPersonalTokenList(SubListCreateAPIView): - name = _("OAuth2 Personal Access Tokens") - - model = models.OAuth2AccessToken - serializer_class = serializers.UserPersonalTokenSerializer - parent_model = models.User - relationship = 'main_oauth2accesstoken' - parent_key = 'user' - swagger_topic = 'Authentication' - - def get_queryset(self): - return get_access_token_model().objects.filter(application__isnull=True, user=self.request.user) - - -class OAuth2TokenDetail(RetrieveUpdateDestroyAPIView): - name = _("OAuth Token Detail") - - model = models.OAuth2AccessToken - serializer_class = serializers.OAuth2TokenDetailSerializer - swagger_topic = 'Authentication' - - -class OAuth2TokenActivityStreamList(SubListAPIView): - model = models.ActivityStream - serializer_class = serializers.ActivityStreamSerializer - parent_model = models.OAuth2AccessToken - relationship = 'activitystream_set' - swagger_topic = 'Authentication' - search_fields = ('changes',) - - class UserTeamsList(SubListAPIView): model = models.Team serializer_class = serializers.TeamSerializer diff --git a/awx/api/views/root.py b/awx/api/views/root.py index d0f3a88e4c..af0a9c5b7b 100644 --- a/awx/api/views/root.py +++ b/awx/api/views/root.py @@ -28,7 +28,7 @@ from awx.main.analytics import all_collectors from awx.main.ha import is_ha_environment from awx.main.utils import get_awx_version, get_custom_venv_choices from awx.main.utils.licensing import validate_entitlement_manifest -from awx.api.versioning import URLPathVersioning, is_optional_api_urlpattern_prefix_request, reverse, drf_reverse +from awx.api.versioning import URLPathVersioning, reverse, drf_reverse from awx.main.constants import PRIVILEGE_ESCALATION_METHODS from awx.main.models import Project, Organization, Instance, InstanceGroup, JobTemplate from awx.main.utils import set_environ @@ -51,8 +51,6 @@ class ApiRootView(APIView): data['description'] = _('AWX REST API') data['current_version'] = v2 data['available_versions'] = dict(v2=v2) - if not is_optional_api_urlpattern_prefix_request(request): - data['oauth2'] = drf_reverse('api:oauth_authorization_root_view') data['custom_logo'] = settings.CUSTOM_LOGO data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO data['login_redirect_override'] = settings.LOGIN_REDIRECT_OVERRIDE @@ -61,20 +59,6 @@ class ApiRootView(APIView): return Response(data) -class ApiOAuthAuthorizationRootView(APIView): - permission_classes = (AllowAny,) - name = _("API OAuth 2 Authorization Root") - versioning_class = None - swagger_topic = 'Authentication' - - def get(self, request, format=None): - data = OrderedDict() - data['authorize'] = drf_reverse('api:authorize') - data['token'] = drf_reverse('api:token') - data['revoke_token'] = drf_reverse('api:revoke-token') - return Response(data) - - class ApiVersionRootView(APIView): permission_classes = (AllowAny,) swagger_topic = 'Versioning' @@ -99,8 +83,6 @@ class ApiVersionRootView(APIView): data['credentials'] = reverse('api:credential_list', request=request) data['credential_types'] = reverse('api:credential_type_list', request=request) data['credential_input_sources'] = reverse('api:credential_input_source_list', request=request) - data['applications'] = reverse('api:o_auth2_application_list', request=request) - data['tokens'] = reverse('api:o_auth2_token_list', request=request) data['metrics'] = reverse('api:metrics_view', request=request) data['inventory'] = reverse('api:inventory_list', request=request) data['constructed_inventory'] = reverse('api:constructed_inventory_list', request=request) diff --git a/awx/main/access.py b/awx/main/access.py index 308d262b23..02a8ef0775 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -17,9 +17,6 @@ from django.core.exceptions import ObjectDoesNotExist, FieldDoesNotExist # Django REST Framework from rest_framework.exceptions import ParseError, PermissionDenied -# Django OAuth Toolkit -from awx.main.models.oauth import OAuth2Application, OAuth2AccessToken - # django-ansible-base from ansible_base.lib.utils.validation import to_python_boolean from ansible_base.rbac.models import RoleEvaluation @@ -441,10 +438,7 @@ class BaseAccess(object): # Actions not possible for reason unrelated to RBAC # Cannot copy with validation errors, or update a manual group/project - if 'write' not in getattr(self.user, 'oauth_scopes', ['write']): - user_capabilities[display_method] = False # Read tokens cannot take any actions - continue - elif display_method in ['copy', 'start', 'schedule'] and isinstance(obj, JobTemplate): + if display_method in ['copy', 'start', 'schedule'] and isinstance(obj, JobTemplate): if obj.validation_errors: user_capabilities[display_method] = False continue @@ -753,82 +747,6 @@ class UserAccess(BaseAccess): return False -class OAuth2ApplicationAccess(BaseAccess): - """ - I can read, change or delete OAuth 2 applications when: - - I am a superuser. - - I am the admin of the organization of the user of the application. - - I am a user in the organization of the application. - I can create OAuth 2 applications when: - - I am a superuser. - - I am the admin of the organization of the application. - """ - - model = OAuth2Application - select_related = ('user',) - prefetch_related = ('organization', 'oauth2accesstoken_set') - - def filtered_queryset(self): - org_access_qs = Organization.access_qs(self.user, 'member') - return self.model.objects.filter(organization__in=org_access_qs) - - def can_change(self, obj, data): - return self.user.is_superuser or self.check_related('organization', Organization, data, obj=obj, role_field='admin_role', mandatory=True) - - def can_delete(self, obj): - return self.user.is_superuser or obj.organization in self.user.admin_of_organizations - - def can_add(self, data): - if self.user.is_superuser: - return True - if not data: - return Organization.access_qs(self.user, 'change').exists() - return self.check_related('organization', Organization, data, role_field='admin_role', mandatory=True) - - -class OAuth2TokenAccess(BaseAccess): - """ - I can read, change or delete an app token when: - - I am a superuser. - - I am the admin of the organization of the application of the token. - - I am the user of the token. - I can create an OAuth2 app token when: - - I have the read permission of the related application. - I can read, change or delete a personal token when: - - I am the user of the token - - I am the superuser - I can create an OAuth2 Personal Access Token when: - - I am a user. But I can only create a PAT for myself. - """ - - model = OAuth2AccessToken - - select_related = ('user', 'application') - prefetch_related = ('refresh_token',) - - def filtered_queryset(self): - org_access_qs = Organization.objects.filter(Q(admin_role__members=self.user) | Q(auditor_role__members=self.user)) - return self.model.objects.filter(application__organization__in=org_access_qs) | self.model.objects.filter(user__id=self.user.pk) - - def can_delete(self, obj): - if (self.user.is_superuser) | (obj.user == self.user): - return True - elif not obj.application: - return False - return self.user in obj.application.organization.admin_role - - def can_change(self, obj, data): - return self.can_delete(obj) - - def can_add(self, data): - if 'application' in data: - app = get_object_from_data('application', OAuth2Application, data) - if app is None: - return True - return OAuth2ApplicationAccess(self.user).can_read(app) - return True - - class OrganizationAccess(NotificationAttachMixin, BaseAccess): """ I can see organizations when: @@ -2741,8 +2659,6 @@ class ActivityStreamAccess(BaseAccess): 'credential_type', 'team', 'ad_hoc_command', - 'o_auth2_application', - 'o_auth2_access_token', 'notification_template', 'notification', 'label', @@ -2828,14 +2744,6 @@ class ActivityStreamAccess(BaseAccess): if team_set: q |= Q(team__in=team_set) - app_set = OAuth2ApplicationAccess(self.user).filtered_queryset() - if app_set: - q |= Q(o_auth2_application__in=app_set) - - token_set = OAuth2TokenAccess(self.user).filtered_queryset() - if token_set: - q |= Q(o_auth2_access_token__in=token_set) - return qs.filter(q).distinct() def can_add(self, data): diff --git a/awx/main/management/commands/cleanup_tokens.py b/awx/main/management/commands/cleanup_tokens.py deleted file mode 100644 index 2deefd3790..0000000000 --- a/awx/main/management/commands/cleanup_tokens.py +++ /dev/null @@ -1,26 +0,0 @@ -import logging -from django.core import management -from django.core.management.base import BaseCommand - -from awx.main.models import OAuth2AccessToken -from oauth2_provider.models import RefreshToken - - -class Command(BaseCommand): - def init_logging(self): - log_levels = dict(enumerate([logging.ERROR, logging.INFO, logging.DEBUG, 0])) - self.logger = logging.getLogger('awx.main.commands.cleanup_tokens') - self.logger.setLevel(log_levels.get(self.verbosity, 0)) - handler = logging.StreamHandler() - handler.setFormatter(logging.Formatter('%(message)s')) - self.logger.addHandler(handler) - self.logger.propagate = False - - def execute(self, *args, **options): - self.verbosity = int(options.get('verbosity', 1)) - self.init_logging() - total_accesstokens = OAuth2AccessToken.objects.all().count() - total_refreshtokens = RefreshToken.objects.all().count() - management.call_command('cleartokens') - self.logger.info("Expired OAuth 2 Access Tokens deleted: {}".format(total_accesstokens - OAuth2AccessToken.objects.all().count())) - self.logger.info("Expired OAuth 2 Refresh Tokens deleted: {}".format(total_refreshtokens - RefreshToken.objects.all().count())) diff --git a/awx/main/management/commands/create_oauth2_token.py b/awx/main/management/commands/create_oauth2_token.py deleted file mode 100644 index 7df9b49f9c..0000000000 --- a/awx/main/management/commands/create_oauth2_token.py +++ /dev/null @@ -1,34 +0,0 @@ -# Django -from django.core.management.base import BaseCommand, CommandError -from django.contrib.auth.models import User -from django.core.exceptions import ObjectDoesNotExist - -# AWX -from awx.api.serializers import OAuth2TokenSerializer - - -class Command(BaseCommand): - """Command that creates an OAuth2 token for a certain user. Returns the value of created token.""" - - help = 'Creates an OAuth2 token for a user.' - - def add_arguments(self, parser): - parser.add_argument('--user', dest='user', type=str) - - def handle(self, *args, **options): - if not options['user']: - raise CommandError('Username not supplied. Usage: awx-manage create_oauth2_token --user=username.') - try: - user = User.objects.get(username=options['user']) - except ObjectDoesNotExist: - raise CommandError('The user does not exist.') - config = {'user': user, 'scope': 'write'} - serializer_obj = OAuth2TokenSerializer() - - class FakeRequest(object): - def __init__(self): - self.user = user - - serializer_obj.context['request'] = FakeRequest() - token_record = serializer_obj.create(config) - self.stdout.write(token_record.token) diff --git a/awx/main/management/commands/regenerate_secret_key.py b/awx/main/management/commands/regenerate_secret_key.py index 248256eb7e..e1dbf42439 100644 --- a/awx/main/management/commands/regenerate_secret_key.py +++ b/awx/main/management/commands/regenerate_secret_key.py @@ -10,7 +10,7 @@ from django.db.models.signals import post_save from awx.conf import settings_registry from awx.conf.models import Setting from awx.conf.signals import on_post_save_setting -from awx.main.models import UnifiedJob, Credential, NotificationTemplate, Job, JobTemplate, WorkflowJob, WorkflowJobTemplate, OAuth2Application +from awx.main.models import UnifiedJob, Credential, NotificationTemplate, Job, JobTemplate, WorkflowJob, WorkflowJobTemplate from awx.main.utils.encryption import encrypt_field, decrypt_field, encrypt_value, decrypt_value, get_encryption_key @@ -45,7 +45,6 @@ class Command(BaseCommand): self._notification_templates() self._credentials() self._unified_jobs() - self._oauth2_app_secrets() self._settings() self._survey_passwords() return self.new_key @@ -74,13 +73,6 @@ class Command(BaseCommand): uj.start_args = encrypt_field(uj, 'start_args', secret_key=self.new_key) uj.save() - def _oauth2_app_secrets(self): - for app in OAuth2Application.objects.iterator(): - raw = app.client_secret - app.client_secret = raw - encrypted = encrypt_value(raw, secret_key=self.new_key) - OAuth2Application.objects.filter(pk=app.pk).update(client_secret=encrypted) - def _settings(self): # don't update the cache, the *actual* value isn't changing post_save.disconnect(on_post_save_setting, sender=Setting) diff --git a/awx/main/management/commands/revoke_oauth2_tokens.py b/awx/main/management/commands/revoke_oauth2_tokens.py deleted file mode 100644 index 1cc128afdf..0000000000 --- a/awx/main/management/commands/revoke_oauth2_tokens.py +++ /dev/null @@ -1,38 +0,0 @@ -# Django -from django.core.management.base import BaseCommand, CommandError -from django.contrib.auth.models import User -from django.core.exceptions import ObjectDoesNotExist - -# AWX -from awx.main.models.oauth import OAuth2AccessToken -from oauth2_provider.models import RefreshToken - - -def revoke_tokens(token_list): - for token in token_list: - token.revoke() - print('revoked {} {}'.format(token.__class__.__name__, token.token)) - - -class Command(BaseCommand): - """Command that revokes OAuth2 access tokens.""" - - help = 'Revokes OAuth2 access tokens. Use --all to revoke access and refresh tokens.' - - def add_arguments(self, parser): - parser.add_argument('--user', dest='user', type=str, help='revoke OAuth2 tokens for a specific username') - parser.add_argument('--all', dest='all', action='store_true', help='revoke OAuth2 access tokens and refresh tokens') - - def handle(self, *args, **options): - if not options['user']: - if options['all']: - revoke_tokens(RefreshToken.objects.filter(revoked=None)) - revoke_tokens(OAuth2AccessToken.objects.all()) - else: - try: - user = User.objects.get(username=options['user']) - except ObjectDoesNotExist: - raise CommandError('A user with that username does not exist.') - if options['all']: - revoke_tokens(RefreshToken.objects.filter(revoked=None).filter(user=user)) - revoke_tokens(user.main_oauth2accesstoken.filter(user=user)) diff --git a/awx/main/migrations/0025_v330_add_oauth_activity_stream_registrar.py b/awx/main/migrations/0025_v330_add_oauth_activity_stream_registrar.py index 88afcd7dff..fd93014daa 100644 --- a/awx/main/migrations/0025_v330_add_oauth_activity_stream_registrar.py +++ b/awx/main/migrations/0025_v330_add_oauth_activity_stream_registrar.py @@ -5,7 +5,6 @@ from __future__ import unicode_literals from django.conf import settings from django.db import migrations, models import django.db.models.deletion -import oauth2_provider import re @@ -13,19 +12,13 @@ class Migration(migrations.Migration): dependencies = [ ('main', '0024_v330_create_user_session_membership'), ] - run_before = [ - # As of this migration, OAuth2Application and OAuth2AccessToken are models in main app - # Grant and RefreshToken models are still in the oauth2_provider app and reference - # the app and token models, so these must be created before the oauth2_provider models - ('oauth2_provider', '0001_initial') - ] operations = [ migrations.CreateModel( name='OAuth2Application', fields=[ ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('client_id', models.CharField(db_index=True, default=oauth2_provider.generators.generate_client_id, max_length=100, unique=True)), + ('client_id', models.CharField(db_index=True, default=lambda: "", max_length=100, unique=True)), ( 'redirect_uris', models.TextField(blank=True, help_text='Allowed URIs list, space separated'), @@ -43,7 +36,7 @@ class Migration(migrations.Migration): max_length=32, ), ), - ('client_secret', models.CharField(blank=True, db_index=True, default=oauth2_provider.generators.generate_client_secret, max_length=255)), + ('client_secret', models.CharField(blank=True, db_index=True, default=lambda: "", max_length=255)), ('name', models.CharField(blank=True, max_length=255)), ('skip_authorization', models.BooleanField(default=False)), ('created', models.DateTimeField(auto_now_add=True)), @@ -72,10 +65,6 @@ class Migration(migrations.Migration): ('updated', models.DateTimeField(auto_now=True)), ('description', models.CharField(blank=True, default='', max_length=200)), ('last_used', models.DateTimeField(default=None, editable=False, null=True)), - ( - 'application', - models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL), - ), ( 'user', models.ForeignKey( diff --git a/awx/main/migrations/0031_v330_encrypt_oauth2_secret.py b/awx/main/migrations/0031_v330_encrypt_oauth2_secret.py index 96d9067058..ce14d415c8 100644 --- a/awx/main/migrations/0031_v330_encrypt_oauth2_secret.py +++ b/awx/main/migrations/0031_v330_encrypt_oauth2_secret.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import awx.main.fields from django.db import migrations -import oauth2_provider.generators class Migration(migrations.Migration): @@ -16,8 +15,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='oauth2application', name='client_secret', - field=awx.main.fields.OAuth2ClientSecretField( - blank=True, db_index=True, default=oauth2_provider.generators.generate_client_secret, max_length=1024 - ), + field=awx.main.fields.OAuth2ClientSecretField(blank=True, db_index=True, default=lambda: "", max_length=1024), ), ] diff --git a/awx/main/migrations/0033_v330_oauth_help_text.py b/awx/main/migrations/0033_v330_oauth_help_text.py index ec5867b2f5..75164236fd 100644 --- a/awx/main/migrations/0033_v330_oauth_help_text.py +++ b/awx/main/migrations/0033_v330_oauth_help_text.py @@ -6,7 +6,6 @@ import awx.main.fields from django.conf import settings from django.db import migrations, models import django.db.models.deletion -import oauth2_provider.generators # TODO: Squash all of these migrations with '0024_v330_add_oauth_activity_stream_registrar' @@ -54,7 +53,7 @@ class Migration(migrations.Migration): field=awx.main.fields.OAuth2ClientSecretField( blank=True, db_index=True, - default=oauth2_provider.generators.generate_client_secret, + default=lambda: "", help_text='Used for more stringent verification of access to an application when creating a token.', max_length=1024, ), diff --git a/awx/main/migrations/0041_v330_update_oauth_refreshtoken.py b/awx/main/migrations/0041_v330_update_oauth_refreshtoken.py index 7ff23582a7..0264957c26 100644 --- a/awx/main/migrations/0041_v330_update_oauth_refreshtoken.py +++ b/awx/main/migrations/0041_v330_update_oauth_refreshtoken.py @@ -2,27 +2,12 @@ # Generated by Django 1.11.11 on 2018-06-14 21:03 from __future__ import unicode_literals -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion +from django.db import migrations class Migration(migrations.Migration): dependencies = [ - migrations.swappable_dependency(settings.OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL), ('main', '0040_v330_unifiedjob_controller_node'), ] - operations = [ - migrations.AddField( - model_name='oauth2accesstoken', - name='source_refresh_token', - field=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, - ), - ), - ] + operations = [] diff --git a/awx/main/migrations/0183_pre_django_upgrade.py b/awx/main/migrations/0183_pre_django_upgrade.py index ec31b9e0a3..e2b844ab99 100644 --- a/awx/main/migrations/0183_pre_django_upgrade.py +++ b/awx/main/migrations/0183_pre_django_upgrade.py @@ -1,25 +1,14 @@ # Generated by Django 3.2.16 on 2023-04-21 14:15 -from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - migrations.swappable_dependency(settings.OAUTH2_PROVIDER_ID_TOKEN_MODEL), ('main', '0182_constructed_inventory'), - ('oauth2_provider', '0005_auto_20211222_2352'), ] operations = [ - migrations.AddField( - model_name='oauth2accesstoken', - name='id_token', - field=models.OneToOneField( - blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='access_token', to=settings.OAUTH2_PROVIDER_ID_TOKEN_MODEL - ), - ), migrations.AddField( model_name='oauth2application', name='algorithm', diff --git a/awx/main/migrations/0199_alter_oauth2application_unique_together_and_more.py b/awx/main/migrations/0199_alter_oauth2application_unique_together_and_more.py new file mode 100644 index 0000000000..a157880386 --- /dev/null +++ b/awx/main/migrations/0199_alter_oauth2application_unique_together_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.10 on 2024-10-24 14:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0198_alter_inventorysource_source_and_more'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='oauth2application', + unique_together=None, + ), + migrations.RemoveField( + model_name='oauth2application', + name='organization', + ), + migrations.RemoveField( + model_name='oauth2application', + name='user', + ), + migrations.RemoveField( + model_name='activitystream', + name='o_auth2_access_token', + ), + migrations.RemoveField( + model_name='activitystream', + name='o_auth2_application', + ), + migrations.DeleteModel( + name='OAuth2AccessToken', + ), + migrations.DeleteModel( + name='OAuth2Application', + ), + ] diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index f8dbc2d50f..5cf8b5fc35 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -93,9 +93,6 @@ from awx.main.models.workflow import ( # noqa WorkflowApproval, WorkflowApprovalTemplate, ) -from awx.api.versioning import reverse -from awx.main.models.oauth import OAuth2AccessToken, OAuth2Application # noqa -from oauth2_provider.models import Grant, RefreshToken # noqa -- needed django-oauth-toolkit model migrations # Add custom methods to User model for permissions checks. @@ -244,19 +241,6 @@ def user_is_system_auditor(user, tf): User.add_to_class('is_system_auditor', user_is_system_auditor) -def o_auth2_application_get_absolute_url(self, request=None): - return reverse('api:o_auth2_application_detail', kwargs={'pk': self.pk}, request=request) - - -OAuth2Application.add_to_class('get_absolute_url', o_auth2_application_get_absolute_url) - - -def o_auth2_token_get_absolute_url(self, request=None): - return reverse('api:o_auth2_token_detail', kwargs={'pk': self.pk}, request=request) - - -OAuth2AccessToken.add_to_class('get_absolute_url', o_auth2_token_get_absolute_url) - from awx.main.registrar import activity_stream_registrar # noqa activity_stream_registrar.connect(Organization) @@ -288,8 +272,6 @@ activity_stream_registrar.connect(WorkflowJobTemplateNode) activity_stream_registrar.connect(WorkflowJob) activity_stream_registrar.connect(WorkflowApproval) activity_stream_registrar.connect(WorkflowApprovalTemplate) -activity_stream_registrar.connect(OAuth2Application) -activity_stream_registrar.connect(OAuth2AccessToken) # Register models permission_registry.register(Project, Team, WorkflowJobTemplate, JobTemplate, Inventory, Organization, Credential, NotificationTemplate, ExecutionEnvironment) @@ -297,8 +279,3 @@ permission_registry.register(InstanceGroup, parent_field_name=None) # Not part # prevent API filtering on certain Django-supplied sensitive fields prevent_search(User._meta.get_field('password')) -prevent_search(OAuth2AccessToken._meta.get_field('token')) -prevent_search(RefreshToken._meta.get_field('token')) -prevent_search(OAuth2Application._meta.get_field('client_secret')) -prevent_search(OAuth2Application._meta.get_field('client_id')) -prevent_search(Grant._meta.get_field('code')) diff --git a/awx/main/models/activity_stream.py b/awx/main/models/activity_stream.py index 2dccf3158f..62b1970b5a 100644 --- a/awx/main/models/activity_stream.py +++ b/awx/main/models/activity_stream.py @@ -81,8 +81,6 @@ class ActivityStream(models.Model): role = models.ManyToManyField("Role", blank=True) instance = models.ManyToManyField("Instance", blank=True) instance_group = models.ManyToManyField("InstanceGroup", blank=True) - o_auth2_application = models.ManyToManyField("OAuth2Application", blank=True) - o_auth2_access_token = models.ManyToManyField("OAuth2AccessToken", blank=True) setting = models.JSONField(default=dict, blank=True) diff --git a/awx/main/models/oauth.py b/awx/main/models/oauth.py deleted file mode 100644 index adda62d574..0000000000 --- a/awx/main/models/oauth.py +++ /dev/null @@ -1,125 +0,0 @@ -# Python -import logging -import re - -# Django -from django.core.validators import RegexValidator -from django.db import models, connection -from django.utils.timezone import now -from django.utils.translation import gettext_lazy as _ -from django.conf import settings - -# Django OAuth Toolkit -from oauth2_provider.models import AbstractApplication, AbstractAccessToken -from oauth2_provider.generators import generate_client_secret - -from awx.main.fields import OAuth2ClientSecretField - - -DATA_URI_RE = re.compile(r'.*') # FIXME - -__all__ = ['OAuth2AccessToken', 'OAuth2Application'] - - -logger = logging.getLogger('awx.main.models.oauth') - - -class OAuth2Application(AbstractApplication): - class Meta: - app_label = 'main' - verbose_name = _('application') - unique_together = (("name", "organization"),) - ordering = ('organization', 'name') - - CLIENT_CONFIDENTIAL = "confidential" - CLIENT_PUBLIC = "public" - CLIENT_TYPES = ( - (CLIENT_CONFIDENTIAL, _("Confidential")), - (CLIENT_PUBLIC, _("Public")), - ) - - GRANT_AUTHORIZATION_CODE = "authorization-code" - GRANT_PASSWORD = "password" - GRANT_TYPES = ( - (GRANT_AUTHORIZATION_CODE, _("Authorization code")), - (GRANT_PASSWORD, _("Resource owner password-based")), - ) - - description = models.TextField( - default='', - blank=True, - ) - logo_data = models.TextField( - default='', - editable=False, - validators=[RegexValidator(DATA_URI_RE)], - ) - organization = models.ForeignKey( - 'Organization', - related_name='applications', - help_text=_('Organization containing this application.'), - on_delete=models.CASCADE, - null=True, - ) - client_secret = OAuth2ClientSecretField( - max_length=1024, - blank=True, - default=generate_client_secret, - db_index=True, - help_text=_('Used for more stringent verification of access to an application when creating a token.'), - ) - client_type = models.CharField( - max_length=32, choices=CLIENT_TYPES, help_text=_('Set to Public or Confidential depending on how secure the client device is.') - ) - skip_authorization = models.BooleanField(default=False, help_text=_('Set True to skip authorization step for completely trusted applications.')) - authorization_grant_type = models.CharField( - max_length=32, choices=GRANT_TYPES, help_text=_('The Grant type the user must use for acquire tokens for this application.') - ) - - -class OAuth2AccessToken(AbstractAccessToken): - class Meta: - app_label = 'main' - verbose_name = _('access token') - ordering = ('id',) - - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - blank=True, - null=True, - related_name="%(app_label)s_%(class)s", - help_text=_('The user representing the token owner'), - ) - description = models.TextField( - default='', - blank=True, - ) - last_used = models.DateTimeField( - null=True, - default=None, - editable=False, - ) - scope = models.TextField( - blank=True, - default='write', - help_text=_( - 'Allowed scopes, further restricts user\'s permissions. Must be a simple space-separated string with allowed scopes [\'read\', \'write\'].' - ), - ) - modified = models.DateTimeField(editable=False, auto_now=True) - - def is_valid(self, scopes=None): - valid = super(OAuth2AccessToken, self).is_valid(scopes) - if valid: - self.last_used = now() - - def _update_last_used(): - if OAuth2AccessToken.objects.filter(pk=self.pk).exists(): - self.save(update_fields=['last_used']) - - connection.on_commit(_update_last_used) - return valid - - def save(self, *args, **kwargs): - super(OAuth2AccessToken, self).save(*args, **kwargs) diff --git a/awx/main/signals.py b/awx/main/signals.py index e2fb00a907..8d2d77be5e 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -39,7 +39,6 @@ from awx.main.models import ( Job, JobHostSummary, JobTemplate, - OAuth2AccessToken, Organization, Project, Role, @@ -54,7 +53,6 @@ from awx.main.models import ( WorkflowApprovalTemplate, ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ) -from awx.main.constants import CENSOR_VALUE from awx.main.utils import model_instance_diff, model_to_dict, camelcase_to_underscore, get_current_apps from awx.main.utils import ignore_inventory_computed_fields, ignore_inventory_group_removal, _inventory_updates from awx.main.tasks.system import update_inventory_computed_fields, handle_removed_image @@ -400,8 +398,6 @@ def model_serializer_mapping(): models.WorkflowApproval: serializers.WorkflowApprovalActivityStreamSerializer, models.WorkflowApprovalTemplate: serializers.WorkflowApprovalTemplateSerializer, models.WorkflowJob: serializers.WorkflowJobSerializer, - models.OAuth2AccessToken: serializers.OAuth2TokenSerializer, - models.OAuth2Application: serializers.OAuth2ApplicationSerializer, } @@ -443,8 +439,6 @@ def activity_stream_create(sender, instance, created, **kwargs): changes['labels'] = [label.name for label in instance.labels.iterator()] if 'extra_vars' in changes: changes['extra_vars'] = instance.display_extra_vars() - if type(instance) == OAuth2AccessToken: - changes['token'] = CENSOR_VALUE activity_entry = get_activity_stream_class()(operation='create', object1=object1, changes=json.dumps(changes), actor=get_current_user_or_none()) # TODO: Weird situation where cascade SETNULL doesn't work # it might actually be a good idea to remove all of these FK references since @@ -506,8 +500,6 @@ def activity_stream_delete(sender, instance, **kwargs): return changes.update(model_to_dict(instance, model_serializer_mapping())) object1 = camelcase_to_underscore(instance.__class__.__name__) - if type(instance) == OAuth2AccessToken: - changes['token'] = CENSOR_VALUE activity_entry = get_activity_stream_class()(operation='delete', changes=json.dumps(changes), object1=object1, actor=get_current_user_or_none()) activity_entry.save() connection.on_commit(lambda: emit_activity_stream_change(activity_entry)) @@ -669,13 +661,3 @@ def save_user_session_membership(sender, **kwargs): membership.delete() if len(expired): consumers.emit_channel_notification('control-limit_reached_{}'.format(user_id), dict(group_name='control', reason='limit_reached')) - - -@receiver(post_save, sender=OAuth2AccessToken) -def create_access_token_user_if_missing(sender, **kwargs): - obj = kwargs['instance'] - if obj.application and obj.application.user: - obj.user = obj.application.user - post_save.disconnect(create_access_token_user_if_missing, sender=OAuth2AccessToken) - obj.save() - post_save.connect(create_access_token_user_if_missing, sender=OAuth2AccessToken) diff --git a/awx/main/tests/functional/api/test_oauth.py b/awx/main/tests/functional/api/test_oauth.py deleted file mode 100644 index b4b0b49b6e..0000000000 --- a/awx/main/tests/functional/api/test_oauth.py +++ /dev/null @@ -1,296 +0,0 @@ -import base64 -import json -import time - -import pytest - -from django.db import connection -from django.test.utils import override_settings -from django.utils.encoding import smart_str, smart_bytes - -from rest_framework.reverse import reverse as drf_reverse - -from awx.main.utils.encryption import decrypt_value, get_encryption_key -from awx.api.versioning import reverse -from awx.main.models.oauth import OAuth2Application as Application, OAuth2AccessToken as AccessToken -from oauth2_provider.models import RefreshToken - - -@pytest.mark.django_db -def test_personal_access_token_creation(oauth_application, post, alice): - url = drf_reverse('api:oauth_authorization_root_view') + 'token/' - resp = post( - url, - data='grant_type=password&username=alice&password=alice&scope=read', - content_type='application/x-www-form-urlencoded', - HTTP_AUTHORIZATION='Basic ' + smart_str(base64.b64encode(smart_bytes(':'.join([oauth_application.client_id, oauth_application.client_secret])))), - ) - resp_json = smart_str(resp._container[0]) - assert 'access_token' in resp_json - assert 'scope' in resp_json - assert 'refresh_token' in resp_json - - -@pytest.mark.django_db -def test_pat_creation_no_default_scope(oauth_application, post, admin): - # tests that the default scope is overriden - url = reverse('api:o_auth2_token_list') - response = post( - url, - { - 'description': 'test token', - 'scope': 'read', - 'application': oauth_application.pk, - }, - admin, - ) - assert response.data['scope'] == 'read' - - -@pytest.mark.django_db -def test_pat_creation_no_scope(oauth_application, post, admin): - url = reverse('api:o_auth2_token_list') - response = post( - url, - { - 'description': 'test token', - 'application': oauth_application.pk, - }, - admin, - ) - assert response.data['scope'] == 'write' - - -@pytest.mark.django_db -def test_oauth2_application_create(admin, organization, post): - response = post( - reverse('api:o_auth2_application_list'), - { - 'name': 'test app', - 'organization': organization.pk, - 'client_type': 'confidential', - 'authorization_grant_type': 'password', - }, - admin, - expect=201, - ) - assert 'modified' in response.data - assert 'updated' not in response.data - created_app = Application.objects.get(client_id=response.data['client_id']) - assert created_app.name == 'test app' - assert created_app.skip_authorization is False - assert created_app.redirect_uris == '' - assert created_app.client_type == 'confidential' - assert created_app.authorization_grant_type == 'password' - assert created_app.organization == organization - - -@pytest.mark.django_db -def test_oauth2_validator(admin, oauth_application, post): - post( - reverse('api:o_auth2_application_list'), - { - 'name': 'Write App Token', - 'application': oauth_application.pk, - 'scope': 'Write', - }, - admin, - expect=400, - ) - - -@pytest.mark.django_db -def test_oauth_application_update(oauth_application, organization, patch, admin, alice): - patch( - reverse('api:o_auth2_application_detail', kwargs={'pk': oauth_application.pk}), - { - 'name': 'Test app with immutable grant type and user', - 'organization': organization.pk, - 'redirect_uris': 'http://localhost/api/', - 'authorization_grant_type': 'password', - 'skip_authorization': True, - }, - admin, - expect=200, - ) - updated_app = Application.objects.get(client_id=oauth_application.client_id) - assert updated_app.name == 'Test app with immutable grant type and user' - assert updated_app.redirect_uris == 'http://localhost/api/' - assert updated_app.skip_authorization is True - assert updated_app.authorization_grant_type == 'password' - assert updated_app.organization == organization - - -@pytest.mark.django_db -def test_oauth_application_encryption(admin, organization, post): - response = post( - reverse('api:o_auth2_application_list'), - { - 'name': 'test app', - 'organization': organization.pk, - 'client_type': 'confidential', - 'authorization_grant_type': 'password', - }, - admin, - expect=201, - ) - pk = response.data.get('id') - secret = response.data.get('client_secret') - with connection.cursor() as cursor: - encrypted = cursor.execute('SELECT client_secret FROM main_oauth2application WHERE id={}'.format(pk)).fetchone()[0] - assert encrypted.startswith('$encrypted$') - assert decrypt_value(get_encryption_key('value', pk=None), encrypted) == secret - - -@pytest.mark.django_db -def test_oauth_token_create(oauth_application, get, post, admin): - response = post(reverse('api:o_auth2_application_token_list', kwargs={'pk': oauth_application.pk}), {'scope': 'read'}, admin, expect=201) - assert 'modified' in response.data and response.data['modified'] is not None - assert 'updated' not in response.data - token = AccessToken.objects.get(token=response.data['token']) - refresh_token = RefreshToken.objects.get(token=response.data['refresh_token']) - assert token.application == oauth_application - assert refresh_token.application == oauth_application - assert token.user == admin - assert refresh_token.user == admin - assert refresh_token.access_token == token - assert token.scope == 'read' - response = get(reverse('api:o_auth2_application_token_list', kwargs={'pk': oauth_application.pk}), admin, expect=200) - assert response.data['count'] == 1 - response = get(reverse('api:o_auth2_application_detail', kwargs={'pk': oauth_application.pk}), admin, expect=200) - assert response.data['summary_fields']['tokens']['count'] == 1 - assert response.data['summary_fields']['tokens']['results'][0] == {'id': token.pk, 'scope': token.scope, 'token': '************'} - - response = post(reverse('api:o_auth2_token_list'), {'scope': 'read', 'application': oauth_application.pk}, admin, expect=201) - assert response.data['refresh_token'] - response = post( - reverse('api:user_authorized_token_list', kwargs={'pk': admin.pk}), {'scope': 'read', 'application': oauth_application.pk}, admin, expect=201 - ) - assert response.data['refresh_token'] - response = post(reverse('api:application_o_auth2_token_list', kwargs={'pk': oauth_application.pk}), {'scope': 'read'}, admin, expect=201) - assert response.data['refresh_token'] - - -@pytest.mark.django_db -def test_oauth_token_update(oauth_application, post, patch, admin): - response = post(reverse('api:o_auth2_application_token_list', kwargs={'pk': oauth_application.pk}), {'scope': 'read'}, admin, expect=201) - token = AccessToken.objects.get(token=response.data['token']) - patch(reverse('api:o_auth2_token_detail', kwargs={'pk': token.pk}), {'scope': 'write'}, admin, expect=200) - token = AccessToken.objects.get(token=token.token) - assert token.scope == 'write' - - -@pytest.mark.django_db -def test_oauth_token_delete(oauth_application, post, delete, get, admin): - response = post(reverse('api:o_auth2_application_token_list', kwargs={'pk': oauth_application.pk}), {'scope': 'read'}, admin, expect=201) - token = AccessToken.objects.get(token=response.data['token']) - delete(reverse('api:o_auth2_token_detail', kwargs={'pk': token.pk}), admin, expect=204) - assert AccessToken.objects.count() == 0 - assert RefreshToken.objects.count() == 1 - response = get(reverse('api:o_auth2_application_token_list', kwargs={'pk': oauth_application.pk}), admin, expect=200) - assert response.data['count'] == 0 - response = get(reverse('api:o_auth2_application_detail', kwargs={'pk': oauth_application.pk}), admin, expect=200) - assert response.data['summary_fields']['tokens']['count'] == 0 - - -@pytest.mark.django_db -def test_oauth_application_delete(oauth_application, post, delete, admin): - post(reverse('api:o_auth2_application_token_list', kwargs={'pk': oauth_application.pk}), {'scope': 'read'}, admin, expect=201) - delete(reverse('api:o_auth2_application_detail', kwargs={'pk': oauth_application.pk}), admin, expect=204) - assert Application.objects.filter(client_id=oauth_application.client_id).count() == 0 - assert RefreshToken.objects.filter(application=oauth_application).count() == 0 - assert AccessToken.objects.filter(application=oauth_application).count() == 0 - - -@pytest.mark.django_db -def test_oauth_list_user_tokens(oauth_application, post, get, admin, alice): - for user in (admin, alice): - url = reverse('api:o_auth2_token_list', kwargs={'pk': user.pk}) - post(url, {'scope': 'read'}, user, expect=201) - response = get(url, admin, expect=200) - assert response.data['count'] == 1 - - -@pytest.mark.django_db -def test_refresh_accesstoken(oauth_application, post, get, delete, admin): - response = post(reverse('api:o_auth2_application_token_list', kwargs={'pk': oauth_application.pk}), {'scope': 'read'}, admin, expect=201) - assert AccessToken.objects.count() == 1 - assert RefreshToken.objects.count() == 1 - token = AccessToken.objects.get(token=response.data['token']) - refresh_token = RefreshToken.objects.get(token=response.data['refresh_token']) - - refresh_url = drf_reverse('api:oauth_authorization_root_view') + 'token/' - response = post( - refresh_url, - data='grant_type=refresh_token&refresh_token=' + refresh_token.token, - content_type='application/x-www-form-urlencoded', - HTTP_AUTHORIZATION='Basic ' + smart_str(base64.b64encode(smart_bytes(':'.join([oauth_application.client_id, oauth_application.client_secret])))), - ) - assert RefreshToken.objects.filter(token=refresh_token).exists() - original_refresh_token = RefreshToken.objects.get(token=refresh_token) - assert token not in AccessToken.objects.all() - assert AccessToken.objects.count() == 1 - # the same RefreshToken remains but is marked revoked - assert RefreshToken.objects.count() == 2 - new_token = json.loads(response._container[0])['access_token'] - new_refresh_token = json.loads(response._container[0])['refresh_token'] - assert AccessToken.objects.filter(token=new_token).count() == 1 - # checks that RefreshTokens are rotated (new RefreshToken issued) - assert RefreshToken.objects.filter(token=new_refresh_token).count() == 1 - assert original_refresh_token.revoked # is not None - - -@pytest.mark.django_db -def test_refresh_token_expiration_is_respected(oauth_application, post, get, delete, admin): - response = post(reverse('api:o_auth2_application_token_list', kwargs={'pk': oauth_application.pk}), {'scope': 'read'}, admin, expect=201) - assert AccessToken.objects.count() == 1 - assert RefreshToken.objects.count() == 1 - refresh_token = RefreshToken.objects.get(token=response.data['refresh_token']) - refresh_url = drf_reverse('api:oauth_authorization_root_view') + 'token/' - short_lived = {'ACCESS_TOKEN_EXPIRE_SECONDS': 1, 'AUTHORIZATION_CODE_EXPIRE_SECONDS': 1, 'REFRESH_TOKEN_EXPIRE_SECONDS': 1} - time.sleep(1) - with override_settings(OAUTH2_PROVIDER=short_lived): - response = post( - refresh_url, - data='grant_type=refresh_token&refresh_token=' + refresh_token.token, - content_type='application/x-www-form-urlencoded', - HTTP_AUTHORIZATION='Basic ' + smart_str(base64.b64encode(smart_bytes(':'.join([oauth_application.client_id, oauth_application.client_secret])))), - ) - assert response.status_code == 403 - assert b'The refresh token has expired.' in response.content - assert RefreshToken.objects.filter(token=refresh_token).exists() - assert AccessToken.objects.count() == 1 - assert RefreshToken.objects.count() == 1 - - -@pytest.mark.django_db -def test_revoke_access_then_refreshtoken(oauth_application, post, get, delete, admin): - response = post(reverse('api:o_auth2_application_token_list', kwargs={'pk': oauth_application.pk}), {'scope': 'read'}, admin, expect=201) - token = AccessToken.objects.get(token=response.data['token']) - refresh_token = RefreshToken.objects.get(token=response.data['refresh_token']) - assert AccessToken.objects.count() == 1 - assert RefreshToken.objects.count() == 1 - - token.revoke() - assert AccessToken.objects.count() == 0 - assert RefreshToken.objects.count() == 1 - assert not refresh_token.revoked - - refresh_token.revoke() - assert AccessToken.objects.count() == 0 - assert RefreshToken.objects.count() == 1 - - -@pytest.mark.django_db -def test_revoke_refreshtoken(oauth_application, post, get, delete, admin): - response = post(reverse('api:o_auth2_application_token_list', kwargs={'pk': oauth_application.pk}), {'scope': 'read'}, admin, expect=201) - refresh_token = RefreshToken.objects.get(token=response.data['refresh_token']) - assert AccessToken.objects.count() == 1 - assert RefreshToken.objects.count() == 1 - - refresh_token.revoke() - assert AccessToken.objects.count() == 0 - # the same RefreshToken is recycled - new_refresh_token = RefreshToken.objects.all().first() - assert refresh_token == new_refresh_token - assert new_refresh_token.revoked diff --git a/awx/main/tests/functional/commands/test_oauth2_token_create.py b/awx/main/tests/functional/commands/test_oauth2_token_create.py deleted file mode 100644 index 5c7a138137..0000000000 --- a/awx/main/tests/functional/commands/test_oauth2_token_create.py +++ /dev/null @@ -1,44 +0,0 @@ -# Python -import pytest -import string -import random -from io import StringIO - -# Django -from django.contrib.auth.models import User -from django.core.management import call_command -from django.core.management.base import CommandError - -# AWX -from awx.main.models.oauth import OAuth2AccessToken - - -@pytest.mark.django_db -@pytest.mark.inventory_import -class TestOAuth2CreateCommand: - def test_no_user_option(self): - out = StringIO() - with pytest.raises(CommandError) as excinfo: - call_command('create_oauth2_token', stdout=out) - assert 'Username not supplied.' in str(excinfo.value) - out.close() - - def test_non_existing_user(self): - out = StringIO() - fake_username = '' - while fake_username == '' or User.objects.filter(username=fake_username).exists(): - fake_username = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(6)) - arg = '--user=' + fake_username - with pytest.raises(CommandError) as excinfo: - call_command('create_oauth2_token', arg, stdout=out) - assert 'The user does not exist.' in str(excinfo.value) - out.close() - - def test_correct_user(self, alice): - out = StringIO() - arg = '--user=' + 'alice' - call_command('create_oauth2_token', arg, stdout=out) - generated_token = out.getvalue().strip() - assert OAuth2AccessToken.objects.filter(user=alice, token=generated_token).count() == 1 - assert OAuth2AccessToken.objects.get(user=alice, token=generated_token).scope == 'write' - out.close() diff --git a/awx/main/tests/functional/commands/test_oauth2_token_revoke.py b/awx/main/tests/functional/commands/test_oauth2_token_revoke.py deleted file mode 100644 index 69b25fd0a8..0000000000 --- a/awx/main/tests/functional/commands/test_oauth2_token_revoke.py +++ /dev/null @@ -1,62 +0,0 @@ -# Python -import datetime -import pytest -import string -import random -from io import StringIO - -# Django -from django.core.management import call_command -from django.core.management.base import CommandError - -# AWX -from awx.main.models import RefreshToken -from awx.main.models.oauth import OAuth2AccessToken -from awx.api.versioning import reverse - - -@pytest.mark.django_db -class TestOAuth2RevokeCommand: - def test_non_existing_user(self): - out = StringIO() - fake_username = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(6)) - arg = '--user=' + fake_username - with pytest.raises(CommandError) as excinfo: - call_command('revoke_oauth2_tokens', arg, stdout=out) - assert 'A user with that username does not exist' in str(excinfo.value) - out.close() - - def test_revoke_all_access_tokens(self, post, admin, alice): - url = reverse('api:o_auth2_token_list') - for user in (admin, alice): - post(url, {'description': 'test token', 'scope': 'read'}, user) - assert OAuth2AccessToken.objects.count() == 2 - call_command('revoke_oauth2_tokens') - assert OAuth2AccessToken.objects.count() == 0 - - def test_revoke_access_token_for_user(self, post, admin, alice): - url = reverse('api:o_auth2_token_list') - post(url, {'description': 'test token', 'scope': 'read'}, alice) - assert OAuth2AccessToken.objects.count() == 1 - call_command('revoke_oauth2_tokens', '--user=admin') - assert OAuth2AccessToken.objects.count() == 1 - call_command('revoke_oauth2_tokens', '--user=alice') - assert OAuth2AccessToken.objects.count() == 0 - - def test_revoke_all_refresh_tokens(self, post, admin, oauth_application): - url = reverse('api:o_auth2_token_list') - post(url, {'description': 'test token for', 'scope': 'read', 'application': oauth_application.pk}, admin) - assert OAuth2AccessToken.objects.count() == 1 - assert RefreshToken.objects.count() == 1 - - call_command('revoke_oauth2_tokens') - assert OAuth2AccessToken.objects.count() == 0 - assert RefreshToken.objects.count() == 1 - for r in RefreshToken.objects.all(): - assert r.revoked is None - - call_command('revoke_oauth2_tokens', '--all') - assert RefreshToken.objects.count() == 1 - for r in RefreshToken.objects.all(): - assert r.revoked is not None - assert isinstance(r.revoked, datetime.datetime) diff --git a/awx/main/tests/functional/commands/test_secret_key_regeneration.py b/awx/main/tests/functional/commands/test_secret_key_regeneration.py index 808fefbdc9..4084baea08 100644 --- a/awx/main/tests/functional/commands/test_secret_key_regeneration.py +++ b/awx/main/tests/functional/commands/test_secret_key_regeneration.py @@ -147,22 +147,6 @@ class TestKeyRegeneration: with override_settings(SECRET_KEY=new_key): assert json.loads(new_job.decrypted_extra_vars())['secret_key'] == 'donttell' - def test_oauth2_application_client_secret(self, oauth_application): - # test basic decryption - secret = oauth_application.client_secret - assert len(secret) == 128 - - # re-key the client_secret - new_key = regenerate_secret_key.Command().handle() - - # verify that the old SECRET_KEY doesn't work - with pytest.raises(InvalidToken): - models.OAuth2Application.objects.get(pk=oauth_application.pk).client_secret - - # verify that the new SECRET_KEY *does* work - with override_settings(SECRET_KEY=new_key): - assert models.OAuth2Application.objects.get(pk=oauth_application.pk).client_secret == secret - def test_use_custom_key_with_tower_secret_key_env_var(self): custom_key = 'MXSq9uqcwezBOChl/UfmbW1k4op+bC+FQtwPqgJ1u9XV' os.environ['TOWER_SECRET_KEY'] = custom_key diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index 8a100b86d0..288e5264c8 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -44,7 +44,6 @@ from awx.main.models.events import ( ) from awx.main.models.workflow import WorkflowJobTemplate from awx.main.models.ad_hoc_commands import AdHocCommand -from awx.main.models.oauth import OAuth2Application as Application from awx.main.models.execution_environments import ExecutionEnvironment from awx.main.utils import is_testing @@ -812,11 +811,6 @@ def get_db_prep_save(self, value, connection, **kwargs): return value -@pytest.fixture -def oauth_application(admin): - return Application.objects.create(name='test app', user=admin, client_type='confidential', authorization_grant_type='password') - - class MockCopy: events = [] index = -1 diff --git a/awx/main/tests/functional/test_rbac_oauth.py b/awx/main/tests/functional/test_rbac_oauth.py deleted file mode 100644 index c55943adeb..0000000000 --- a/awx/main/tests/functional/test_rbac_oauth.py +++ /dev/null @@ -1,247 +0,0 @@ -import pytest - -from awx.main.access import ( - OAuth2ApplicationAccess, - OAuth2TokenAccess, - ActivityStreamAccess, -) -from awx.main.models.oauth import ( - OAuth2Application as Application, - OAuth2AccessToken as AccessToken, -) -from awx.main.models import ActivityStream -from awx.api.versioning import reverse - - -@pytest.mark.django_db -class TestOAuth2Application: - @pytest.mark.parametrize( - "user_for_access, can_access_list", - [ - (0, [True, True]), - (1, [True, True]), - (2, [True, True]), - (3, [False, False]), - ], - ) - def test_can_read(self, admin, org_admin, org_member, alice, user_for_access, can_access_list, organization): - user_list = [admin, org_admin, org_member, alice] - access = OAuth2ApplicationAccess(user_list[user_for_access]) - app_creation_user_list = [admin, org_admin] - for user, can_access in zip(app_creation_user_list, can_access_list): - app = Application.objects.create( - name='test app for {}'.format(user.username), - user=user, - client_type='confidential', - authorization_grant_type='password', - organization=organization, - ) - assert access.can_read(app) is can_access - - def test_admin_only_can_read(self, user, organization): - user = user('org-admin', False) - organization.admin_role.members.add(user) - access = OAuth2ApplicationAccess(user) - app = Application.objects.create( - name='test app for {}'.format(user.username), user=user, client_type='confidential', authorization_grant_type='password', organization=organization - ) - assert access.can_read(app) is True - - def test_app_activity_stream(self, org_admin, alice, organization): - app = Application.objects.create( - name='test app for {}'.format(org_admin.username), - user=org_admin, - client_type='confidential', - authorization_grant_type='password', - organization=organization, - ) - access = OAuth2ApplicationAccess(org_admin) - assert access.can_read(app) is True - access = ActivityStreamAccess(org_admin) - activity_stream = ActivityStream.objects.filter(o_auth2_application=app).latest('pk') - assert access.can_read(activity_stream) is True - access = ActivityStreamAccess(alice) - assert access.can_read(app) is False - assert access.can_read(activity_stream) is False - - def test_token_activity_stream(self, org_admin, alice, organization, post): - app = Application.objects.create( - name='test app for {}'.format(org_admin.username), - user=org_admin, - client_type='confidential', - authorization_grant_type='password', - organization=organization, - ) - response = post(reverse('api:o_auth2_application_token_list', kwargs={'pk': app.pk}), {'scope': 'read'}, org_admin, expect=201) - token = AccessToken.objects.get(token=response.data['token']) - access = OAuth2ApplicationAccess(org_admin) - assert access.can_read(app) is True - access = ActivityStreamAccess(org_admin) - activity_stream = ActivityStream.objects.filter(o_auth2_access_token=token).latest('pk') - assert access.can_read(activity_stream) is True - access = ActivityStreamAccess(alice) - assert access.can_read(token) is False - assert access.can_read(activity_stream) is False - - def test_can_edit_delete_app_org_admin(self, admin, org_admin, org_member, alice, organization): - user_list = [admin, org_admin, org_member, alice] - can_access_list = [True, True, False, False] - for user, can_access in zip(user_list, can_access_list): - app = Application.objects.create( - name='test app for {}'.format(user.username), - user=org_admin, - client_type='confidential', - authorization_grant_type='password', - organization=organization, - ) - access = OAuth2ApplicationAccess(user) - assert access.can_change(app, {}) is can_access - assert access.can_delete(app) is can_access - - def test_can_edit_delete_app_admin(self, admin, org_admin, org_member, alice, organization): - user_list = [admin, org_admin, org_member, alice] - can_access_list = [True, True, False, False] - for user, can_access in zip(user_list, can_access_list): - app = Application.objects.create( - name='test app for {}'.format(user.username), - user=admin, - client_type='confidential', - authorization_grant_type='password', - organization=organization, - ) - access = OAuth2ApplicationAccess(user) - assert access.can_change(app, {}) is can_access - assert access.can_delete(app) is can_access - - def test_superuser_can_always_create(self, admin, org_admin, org_member, alice, organization): - access = OAuth2ApplicationAccess(admin) - for user in [admin, org_admin, org_member, alice]: - assert access.can_add( - {'name': 'test app', 'user': user.pk, 'client_type': 'confidential', 'authorization_grant_type': 'password', 'organization': organization.id} - ) - - def test_normal_user_cannot_create(self, admin, org_admin, org_member, alice, organization): - for access_user in [org_member, alice]: - access = OAuth2ApplicationAccess(access_user) - for user in [admin, org_admin, org_member, alice]: - assert not access.can_add( - { - 'name': 'test app', - 'user': user.pk, - 'client_type': 'confidential', - 'authorization_grant_type': 'password', - 'organization': organization.id, - } - ) - - -@pytest.mark.django_db -class TestOAuth2Token: - def test_can_read_change_delete_app_token(self, post, admin, org_admin, org_member, alice, organization): - user_list = [admin, org_admin, org_member, alice] - can_access_list = [True, True, False, False] - app = Application.objects.create( - name='test app for {}'.format(admin.username), - user=admin, - client_type='confidential', - authorization_grant_type='password', - organization=organization, - ) - response = post(reverse('api:o_auth2_application_token_list', kwargs={'pk': app.pk}), {'scope': 'read'}, admin, expect=201) - for user, can_access in zip(user_list, can_access_list): - token = AccessToken.objects.get(token=response.data['token']) - access = OAuth2TokenAccess(user) - assert access.can_read(token) is can_access - assert access.can_change(token, {}) is can_access - assert access.can_delete(token) is can_access - - def test_auditor_can_read(self, post, admin, org_admin, org_member, alice, system_auditor, organization): - user_list = [admin, org_admin, org_member] - can_access_list = [True, True, True] - cannot_access_list = [False, False, False] - app = Application.objects.create( - name='test app for {}'.format(admin.username), - user=admin, - client_type='confidential', - authorization_grant_type='password', - organization=organization, - ) - for user, can_access, cannot_access in zip(user_list, can_access_list, cannot_access_list): - response = post(reverse('api:o_auth2_application_token_list', kwargs={'pk': app.pk}), {'scope': 'read'}, user, expect=201) - token = AccessToken.objects.get(token=response.data['token']) - access = OAuth2TokenAccess(system_auditor) - assert access.can_read(token) is can_access - assert access.can_change(token, {}) is cannot_access - assert access.can_delete(token) is cannot_access - - def test_user_auditor_can_change(self, post, org_member, org_admin, system_auditor, organization): - app = Application.objects.create( - name='test app for {}'.format(org_admin.username), - user=org_admin, - client_type='confidential', - authorization_grant_type='password', - organization=organization, - ) - response = post(reverse('api:o_auth2_application_token_list', kwargs={'pk': app.pk}), {'scope': 'read'}, org_member, expect=201) - token = AccessToken.objects.get(token=response.data['token']) - access = OAuth2TokenAccess(system_auditor) - assert access.can_read(token) is True - assert access.can_change(token, {}) is False - assert access.can_delete(token) is False - dual_user = system_auditor - organization.admin_role.members.add(dual_user) - access = OAuth2TokenAccess(dual_user) - assert access.can_read(token) is True - assert access.can_change(token, {}) is True - assert access.can_delete(token) is True - - def test_can_read_change_delete_personal_token_org_member(self, post, admin, org_admin, org_member, alice): - # Tests who can read a token created by an org-member - user_list = [admin, org_admin, org_member, alice] - can_access_list = [True, False, True, False] - response = post(reverse('api:user_personal_token_list', kwargs={'pk': org_member.pk}), {'scope': 'read'}, org_member, expect=201) - token = AccessToken.objects.get(token=response.data['token']) - for user, can_access in zip(user_list, can_access_list): - access = OAuth2TokenAccess(user) - assert access.can_read(token) is can_access - assert access.can_change(token, {}) is can_access - assert access.can_delete(token) is can_access - - def test_can_read_personal_token_creator(self, post, admin, org_admin, org_member, alice): - # Tests the token's creator can read their tokens - user_list = [admin, org_admin, org_member, alice] - can_access_list = [True, True, True, True] - - for user, can_access in zip(user_list, can_access_list): - response = post(reverse('api:user_personal_token_list', kwargs={'pk': user.pk}), {'scope': 'read', 'application': None}, user, expect=201) - token = AccessToken.objects.get(token=response.data['token']) - access = OAuth2TokenAccess(user) - assert access.can_read(token) is can_access - assert access.can_change(token, {}) is can_access - assert access.can_delete(token) is can_access - - @pytest.mark.parametrize( - "user_for_access, can_access_list", - [ - (0, [True, True]), - (1, [True, True]), - (2, [True, True]), - (3, [False, False]), - ], - ) - def test_can_create(self, post, admin, org_admin, org_member, alice, user_for_access, can_access_list, organization): - user_list = [admin, org_admin, org_member, alice] - for user, can_access in zip(user_list, can_access_list): - app = Application.objects.create( - name='test app for {}'.format(user.username), - user=user, - client_type='confidential', - authorization_grant_type='password', - organization=organization, - ) - post( - reverse('api:o_auth2_application_token_list', kwargs={'pk': app.pk}), - {'scope': 'read'}, - user_list[user_for_access], - expect=201 if can_access else 403, - ) diff --git a/awx/main/tests/unit/api/serializers/test_token_serializer.py b/awx/main/tests/unit/api/serializers/test_token_serializer.py deleted file mode 100644 index aa6363d47a..0000000000 --- a/awx/main/tests/unit/api/serializers/test_token_serializer.py +++ /dev/null @@ -1,8 +0,0 @@ -import pytest - -from awx.api.serializers import OAuth2TokenSerializer - - -@pytest.mark.parametrize('scope, expect', [('', False), ('read', True), ('read read', False), ('write read', True), ('read rainbow', False)]) -def test_invalid_scopes(scope, expect): - assert OAuth2TokenSerializer()._is_valid_scope(scope) is expect diff --git a/awx/main/tests/unit/api/test_filters.py b/awx/main/tests/unit/api/test_filters.py index 29c2e3a93d..78cc7401cf 100644 --- a/awx/main/tests/unit/api/test_filters.py +++ b/awx/main/tests/unit/api/test_filters.py @@ -9,7 +9,6 @@ from ansible_base.rest_filters.rest_framework.field_lookup_backend import FieldL from awx.main.models import ( AdHocCommand, - ActivityStream, Job, JobTemplate, SystemJob, @@ -19,7 +18,6 @@ from awx.main.models import ( WorkflowJobTemplate, WorkflowJobOptions, ) -from awx.main.models.oauth import OAuth2Application from awx.main.models.jobs import JobOptions @@ -28,7 +26,6 @@ from awx.main.models.jobs import JobOptions [ (User, 'password__icontains'), (User, 'settings__value__icontains'), - (User, 'main_oauth2accesstoken__token__gt'), (UnifiedJob, 'job_args__icontains'), (UnifiedJob, 'job_env__icontains'), (UnifiedJob, 'start_args__icontains'), @@ -40,8 +37,6 @@ from awx.main.models.jobs import JobOptions (WorkflowJob, 'survey_passwords__icontains'), (JobTemplate, 'survey_spec__icontains'), (WorkflowJobTemplate, 'survey_spec__icontains'), - (ActivityStream, 'o_auth2_application__client_secret__gt'), - (OAuth2Application, 'grant__code__gt'), ], ) def test_filter_sensitive_fields_and_relations(model, query): diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index 066a365f0f..7d4439f51a 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -361,7 +361,7 @@ def get_allowed_fields(obj, serializer_mapping): else: allowed_fields = [x.name for x in obj._meta.fields] - ACTIVITY_STREAM_FIELD_EXCLUSIONS = {'user': ['last_login'], 'oauth2accesstoken': ['last_used'], 'oauth2application': ['client_secret']} + ACTIVITY_STREAM_FIELD_EXCLUSIONS = {'user': ['last_login']} model_name = obj._meta.model_name fields_excluded = ACTIVITY_STREAM_FIELD_EXCLUSIONS.get(model_name, []) # see definition of from_db for CredentialType diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 1edcc11edb..1bcfdf0826 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -343,7 +343,6 @@ INSTALLED_APPS = [ # According to channels 4.0 docs you install daphne instead of channels now 'daphne', 'django.contrib.staticfiles', - 'oauth2_provider', 'rest_framework', 'django_extensions', 'polymorphic', @@ -369,7 +368,6 @@ REST_FRAMEWORK = { 'PAGE_SIZE': 25, 'DEFAULT_AUTHENTICATION_CLASSES': ( 'ansible_base.jwt_consumer.awx.auth.AwxJWTAuthentication', - 'awx.api.authentication.LoggedOAuth2Authentication', 'awx.api.authentication.SessionAuthentication', 'awx.api.authentication.LoggedBasicAuthentication', ), @@ -389,16 +387,6 @@ REST_FRAMEWORK = { AUTHENTICATION_BACKENDS = ('awx.main.backends.AWXModelBackend',) - -# Django OAuth Toolkit settings -OAUTH2_PROVIDER_APPLICATION_MODEL = 'main.OAuth2Application' -OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = 'main.OAuth2AccessToken' -OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = 'oauth2_provider.RefreshToken' -OAUTH2_PROVIDER_ID_TOKEN_MODEL = "oauth2_provider.IDToken" - -OAUTH2_PROVIDER = {'ACCESS_TOKEN_EXPIRE_SECONDS': 31536000000, 'AUTHORIZATION_CODE_EXPIRE_SECONDS': 600, 'REFRESH_TOKEN_EXPIRE_SECONDS': 2628000} - - # Enable / Disable HTTP Basic Authentication used in the API browser # Note: Session limits are not enforced when using HTTP Basic Authentication. # Note: This setting may be overridden by database settings. diff --git a/awx_collection/test/awx/test_job_template.py b/awx_collection/test/awx/test_job_template.py index 5ee114359b..16ac36f82f 100644 --- a/awx_collection/test/awx/test_job_template.py +++ b/awx_collection/test/awx/test_job_template.py @@ -240,7 +240,7 @@ def test_job_template_with_survey_encrypted_default(run_module, admin_user, proj assert result.get('changed', False), result # not actually desired, but assert for sanity - silence_warning.assert_called_once_with( + silence_warning.assert_any_call( "The field survey_spec of job_template {0} has encrypted data and " "may inaccurately report task is changed.".format(result['id']) ) diff --git a/awx_collection/test/awx/test_project.py b/awx_collection/test/awx/test_project.py index 04ec05bbf7..27b7b96e05 100644 --- a/awx_collection/test/awx/test_project.py +++ b/awx_collection/test/awx/test_project.py @@ -14,7 +14,7 @@ def test_create_project(run_module, admin_user, organization, silence_warning): dict(name='foo', organization=organization.name, scm_type='git', scm_url='https://foo.invalid', wait=False, scm_update_cache_timeout=5), admin_user, ) - silence_warning.assert_called_once_with('scm_update_cache_timeout will be ignored since scm_update_on_launch was not set to true') + silence_warning.assert_any_call('scm_update_cache_timeout will be ignored since scm_update_on_launch was not set to true') assert result.pop('changed', None), result diff --git a/awx_collection/test/awx/test_user.py b/awx_collection/test/awx/test_user.py index 1513b05a91..a183c7a555 100644 --- a/awx_collection/test/awx/test_user.py +++ b/awx_collection/test/awx/test_user.py @@ -36,7 +36,7 @@ def test_password_no_op_warning(run_module, admin_user, mock_auth_stuff, silence assert result.get('changed') # not actually desired, but assert for sanity - silence_warning.assert_called_once_with( + silence_warning.assert_any_call( "The field password of user {0} has encrypted data and " "may inaccurately report task is changed.".format(result['id']) ) diff --git a/docs/docsite/rst/rest_api/authentication.rst b/docs/docsite/rst/rest_api/authentication.rst index f8d8cb31ad..d438b75c70 100644 --- a/docs/docsite/rst/rest_api/authentication.rst +++ b/docs/docsite/rst/rest_api/authentication.rst @@ -11,7 +11,7 @@ This chapter describes basic and session authentication methods, the best use ca .. contents:: :local: -AWX is designed for organizations to centralize and control their automation with a visual dashboard for out-of-the box control while providing a REST API to integrate with your other tooling on a deeper level. AWX supports a number of authentication methods to make it easy to embed AWX into existing tools and processes to help ensure the right people can access AWX resources. +AWX is designed for organizations to centralize and control their automation with a visual dashboard for out-of-the box control while providing a REST API to integrate with your other tooling on a deeper level. AWX supports a number of authentication methods to make it easy to embed AWX into existing tools and processes to help ensure the right people can access AWX resources. .. _api_session_auth: @@ -44,7 +44,7 @@ Using the curl tool, you can see the activity that occurs when you log into AWX. --cookie 'csrftoken=K580zVVm0rWX8pmNylz5ygTPamgUJxifrdJY0UDtMMoOis5Q1UOxRmV9918BUBIN' \ https:///api/login/ -k -D - -o /dev/null -All of this is done by the AWX when you log in to the UI or API in the browser, and should only be used when authenticating in the browser. +All of this is done by the AWX when you log in to the UI or API in the browser, and should only be used when authenticating in the browser. A typical response might look like: @@ -92,11 +92,11 @@ Setting a session limit allows administrators to limit the number of simultaneou A session is created for each browser that a user uses to log in, which forces the user to log out any extra sessions after they exceed the administrator-defined maximum. -Session limits may be important, depending on your particular setup. For example, perhaps you only want a single user on your system with a single login per device (where the user could log in on his work laptop, phone, or home computer). In such a case, you would want to create a session limit equal to 1 (one). If the user logs in on his laptop, for example, then logs in using his phone, his laptop session expires (times out) and only the login on the phone persists. Proactive session limits will kick the user out when the session is idle. The default value is **-1**, which disables the maximum sessions allowed altogether, meaning you can have as many sessions without an imposed limit. +Session limits may be important, depending on your particular setup. For example, perhaps you only want a single user on your system with a single login per device (where the user could log in on his work laptop, phone, or home computer). In such a case, you would want to create a session limit equal to 1 (one). If the user logs in on his laptop, for example, then logs in using his phone, his laptop session expires (times out) and only the login on the phone persists. Proactive session limits will kick the user out when the session is idle. The default value is **-1**, which disables the maximum sessions allowed altogether, meaning you can have as many sessions without an imposed limit. -While session counts can be very limited, they can also be expanded to cover as many session logins as are needed by your organization. +While session counts can be very limited, they can also be expanded to cover as many session logins as are needed by your organization. -When a user logs in and their login results in other users being logged out, the session limit has been reached and those users who are logged out are notified as to why the logout occurred. +When a user logs in and their login results in other users being logged out, the session limit has been reached and those users who are logged out are notified as to why the logout occurred. .. note:: To make the best use of session limits, disable ``AUTH_BASIC_ENABLED`` by changing the value to ``False``, as it falls outside of the scope of session limit enforcement. @@ -105,7 +105,7 @@ When a user logs in and their login results in other users being logged out, the Basic Authentication ==================== -Basic Authentication (Basic Auth) is stateless, thus the base64-encoded ``username`` and ``password`` must be sent along with each request via the Authorization header. This can be used for API calls from curl requests, python scripts, or individual requests to the API. +Basic Authentication (Basic Auth) is stateless, thus the base64-encoded ``username`` and ``password`` must be sent along with each request via the Authorization header. This can be used for API calls from curl requests, python scripts, or individual requests to the API. Example with curl: .. code-block:: text diff --git a/docs/docsite/rst/rest_api/awx-manage.rst b/docs/docsite/rst/rest_api/awx-manage.rst index 9d42b5419f..bb52052d3b 100644 --- a/docs/docsite/rst/rest_api/awx-manage.rst +++ b/docs/docsite/rst/rest_api/awx-manage.rst @@ -70,75 +70,12 @@ Cluster management .. _ag_token_utility: -Token and session management +Session management ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. index:: - single: awx-manage; token management single: awx-manage; session management -AWX supports the following commands for OAuth2 token management: - -.. contents:: - :local: - - -``create_oauth2_token`` -^^^^^^^^^^^^^^^^^^^^^^^^ - -Use this command to create OAuth2 tokens (specify actual username for ``example_user`` below): - -:: - - $ awx-manage create_oauth2_token --user example_user - - New OAuth2 token for example_user: j89ia8OO79te6IAZ97L7E8bMgXCON2 - -Make sure you provide a valid user when creating tokens. Otherwise, you will get an error message that you tried to issue the command without specifying a user, or supplying a username that does not exist. - - -.. _ag_manage_utility_revoke_tokens: - - -``revoke_oauth2_tokens`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Use this command to revoke OAuth2 tokens (both application tokens and personal access tokens (PAT)). By default, it revokes all application tokens (but not their associated refresh tokens), and revokes all personal access tokens. However, you can also specify a user for whom to revoke all tokens. - -To revoke all existing OAuth2 tokens: - -:: - - $ awx-manage revoke_oauth2_tokens - -To revoke all OAuth2 tokens & their refresh tokens: - -:: - - $ awx-manage revoke_oauth2_tokens --revoke_refresh - -To revoke all OAuth2 tokens for the user with ``id=example_user`` (specify actual username for ``example_user`` below): - -:: - - $ awx-manage revoke_oauth2_tokens --user example_user - -To revoke all OAuth2 tokens and refresh token for the user with ``id=example_user``: - -:: - - $ awx-manage revoke_oauth2_tokens --user example_user --revoke_refresh - - - -``cleartokens`` -^^^^^^^^^^^^^^^^^^^ - -Use this command to clear tokens which have already been revoked. Refer to `Django's Oauth Toolkit documentation on cleartokens`_ for more detail. - - .. _`Django's Oauth Toolkit documentation on cleartokens`: https://django-oauth-toolkit.readthedocs.io/en/latest/management_commands.html - - ``expire_sessions`` ^^^^^^^^^^^^^^^^^^^^^^^^