From 7dba65b0488352d3684135cab309f4284a286d50 Mon Sep 17 00:00:00 2001 From: Mike Graves Date: Mon, 5 Aug 2024 13:37:09 -0400 Subject: [PATCH 1/4] 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. --- awx/__init__.py | 25 - awx/api/authentication.py | 16 - awx/api/conf.py | 23 - awx/api/fields.py | 13 - awx/api/serializers.py | 206 -------- .../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 | 18 - awx/main/access.py | 89 ---- .../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 | 15 +- .../migrations/0183_pre_django_upgrade.py | 9 - ...itystream_o_auth2_access_token_and_more.py | 29 ++ .../0197_delete_oauth2application.py | 16 + awx/main/models/__init__.py | 22 - awx/main/models/activity_stream.py | 2 - awx/main/models/oauth.py | 125 ----- awx/main/signals.py | 17 - 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 | 4 - awx/main/utils/common.py | 2 +- awx/settings/defaults.py | 12 - .../docsite/rst/administration/awx-manage.rst | 68 +-- .../rst/administration/configure_awx.rst | 4 - docs/docsite/rst/administration/index.rst | 1 - .../rst/administration/management_jobs.rst | 15 - docs/docsite/rst/administration/metrics.rst | 6 +- .../rst/administration/oauth2_token_auth.rst | 460 ------------------ .../rst/administration/performance.rst | 2 - docs/docsite/rst/rest_api/authentication.rst | 91 +--- .../rst/userguide/applications_auth.rst | 114 ----- docs/docsite/rst/userguide/credentials.rst | 1 - docs/docsite/rst/userguide/main_menu.rst | 1 - docs/docsite/rst/userguide/overview.rst | 2 - docs/docsite/rst/userguide/users.rst | 16 +- 52 files changed, 58 insertions(+), 2503 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/0196_remove_activitystream_o_auth2_access_token_and_more.py create mode 100644 awx/main/migrations/0197_delete_oauth2application.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 delete mode 100644 docs/docsite/rst/administration/oauth2_token_auth.rst delete mode 100644 docs/docsite/rst/userguide/applications_auth.rst diff --git a/awx/__init__.py b/awx/__init__.py index be80b5861499..6b2f809c3027 100644 --- a/awx/__init__.py +++ b/awx/__init__.py @@ -60,25 +60,6 @@ def version_file(): 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 48fc00db44dc..430c8098fd30 100644 --- a/awx/api/authentication.py +++ b/awx/api/authentication.py @@ -11,9 +11,6 @@ # 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 @@ def authenticate_header(self, request): 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 a1ed832ff662..12baf3f3bba6 100644 --- a/awx/api/conf.py +++ b/awx/api/conf.py @@ -6,8 +6,6 @@ # 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 @@ 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 1fab90065e10..ecdd65881108 100644 --- a/awx/api/fields.py +++ b/awx/api/fields.py @@ -79,19 +79,6 @@ def to_representation(self, value): 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/serializers.py b/awx/api/serializers.py index acc13acbc963..c720633a7ca8 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -10,10 +10,6 @@ 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 @@ -79,14 +75,11 @@ Label, Notification, NotificationTemplate, - OAuth2AccessToken, - OAuth2Application, Organization, Project, ProjectUpdate, ProjectUpdateEvent, ReceptorAddress, - RefreshToken, Role, Schedule, SystemJob, @@ -1059,9 +1052,6 @@ def get_related(self, obj): 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 @@ -1078,199 +1068,6 @@ class Meta: 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'] @@ -1291,7 +1088,6 @@ def get_related(self, obj): 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}), @@ -6074,8 +5870,6 @@ def _local_summarizable_fk_fields(self, obj): ('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 328daaab7adc..000000000000 --- 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 f613b34a0ba6..000000000000 --- 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 1a5a444bc634..000000000000 --- 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 a75ee9d3ccb7..bbfe98af4aa9 100644 --- a/awx/api/urls/organization.py +++ b/awx/api/urls/organization.py @@ -25,7 +25,7 @@ OrganizationObjectRolesList, OrganizationAccessList, ) -from awx.api.views import OrganizationCredentialList, OrganizationApplicationList +from awx.api.views import OrganizationCredentialList urls = [ @@ -66,7 +66,6 @@ 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 3f257da9560b..909a7f028765 100644 --- a/awx/api/urls/urls.py +++ b/awx/api/urls/urls.py @@ -25,10 +25,6 @@ JobTemplateCredentialsList, SchedulePreview, ScheduleZoneInfo, - OAuth2ApplicationList, - OAuth2TokenList, - ApplicationOAuth2TokenList, - OAuth2ApplicationDetail, HostMetricSummaryMonthlyList, ) @@ -79,8 +75,6 @@ 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 @@ 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 @@ 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 39bc07aec402..ffdb03453a7d 100644 --- a/awx/api/urls/user.py +++ b/awx/api/urls/user.py @@ -14,10 +14,6 @@ UserRolesList, UserActivityStreamList, UserAccessList, - OAuth2ApplicationList, - OAuth2UserTokenList, - UserPersonalTokenList, - UserAuthorizedTokenList, ) urls = [ @@ -31,10 +27,6 @@ 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 bbe79bd2a453..7a11278adaf8 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -50,9 +50,6 @@ # ansi2html from ansi2html import Ansi2HTMLConverter -# Django OAuth Toolkit -from oauth2_provider.models import get_access_token_model - import pytz from wsgiref.util import FileWrapper @@ -1147,121 +1144,6 @@ def get_queryset(self): 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 d0f3a88e4c21..f8dad5b928f6 100644 --- a/awx/api/views/root.py +++ b/awx/api/views/root.py @@ -51,8 +51,6 @@ def get(self, request, format=None): 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 @@ def get(self, request, format=None): 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 @@ def get(self, request, format=None): 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 74c604436807..78a6882e2fb9 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -17,9 +17,6 @@ # 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 @@ -753,82 +750,6 @@ def can_unattach(self, obj, sub_obj, relationship, *args, **kwargs): 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: @@ -2736,8 +2657,6 @@ class ActivityStreamAccess(BaseAccess): 'credential_type', 'team', 'ad_hoc_command', - 'o_auth2_application', - 'o_auth2_access_token', 'notification_template', 'notification', 'label', @@ -2823,14 +2742,6 @@ def filtered_queryset(self): 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 2deefd379018..000000000000 --- 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 7df9b49f9ca1..000000000000 --- 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 248256eb7ed3..e1dbf4243978 100644 --- a/awx/main/management/commands/regenerate_secret_key.py +++ b/awx/main/management/commands/regenerate_secret_key.py @@ -10,7 +10,7 @@ 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 @@ def handle(self, **options): 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 @@ def _unified_jobs(self): 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 1cc128afdfee..000000000000 --- 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 88afcd7dff30..fd93014daaba 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 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 96d906705820..ce14d415c84e 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 @@ 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 ec5867b2f5e2..75164236fdf6 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 @@ 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 7ff23582a7a9..e728eca2e85b 100644 --- a/awx/main/migrations/0041_v330_update_oauth_refreshtoken.py +++ b/awx/main/migrations/0041_v330_update_oauth_refreshtoken.py @@ -9,20 +9,7 @@ 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 ec31b9e0a312..fefb12756503 100644 --- a/awx/main/migrations/0183_pre_django_upgrade.py +++ b/awx/main/migrations/0183_pre_django_upgrade.py @@ -7,19 +7,10 @@ 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/0196_remove_activitystream_o_auth2_access_token_and_more.py b/awx/main/migrations/0196_remove_activitystream_o_auth2_access_token_and_more.py new file mode 100644 index 000000000000..1de270a19b99 --- /dev/null +++ b/awx/main/migrations/0196_remove_activitystream_o_auth2_access_token_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.10 on 2024-08-07 15:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0195_EE_permissions'), + ] + + operations = [ + migrations.RemoveField( + model_name='activitystream', + name='o_auth2_access_token', + ), + migrations.RemoveField( + model_name='activitystream', + name='o_auth2_application', + ), + migrations.AlterField( + model_name='oauth2application', + name='client_id', + field=models.CharField(db_index=True, default=lambda: "", max_length=100, unique=True), + ), + migrations.DeleteModel( + name='OAuth2AccessToken', + ), + ] diff --git a/awx/main/migrations/0197_delete_oauth2application.py b/awx/main/migrations/0197_delete_oauth2application.py new file mode 100644 index 000000000000..027777e972e4 --- /dev/null +++ b/awx/main/migrations/0197_delete_oauth2application.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.10 on 2024-08-07 15:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0196_remove_activitystream_o_auth2_access_token_and_more'), + ] + + operations = [ + migrations.DeleteModel( + name='OAuth2Application', + ), + ] diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index a63cc31bf877..081496605a26 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -94,8 +94,6 @@ 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 +242,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 +273,6 @@ def o_auth2_token_get_absolute_url(self, request=None): 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 +280,3 @@ def o_auth2_token_get_absolute_url(self, request=None): # 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 2dccf3158f44..62b1970b5af0 100644 --- a/awx/main/models/activity_stream.py +++ b/awx/main/models/activity_stream.py @@ -81,8 +81,6 @@ class Meta: 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 adda62d5741e..000000000000 --- 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 e2fb00a9076a..5d096e255a8c 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -39,7 +39,6 @@ Job, JobHostSummary, JobTemplate, - OAuth2AccessToken, Organization, Project, Role, @@ -400,8 +399,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 +440,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 +501,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 +662,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 b4b0b49b6eb9..000000000000 --- 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 5c7a13813717..000000000000 --- 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 69b25fd0a849..000000000000 --- 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 808fefbdc997..4084baea0832 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 @@ def test_survey_spec(self, inventory, project, survey_spec_factory, cls): 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 8a100b86d078..288e5264c86f 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -44,7 +44,6 @@ ) 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 c55943adeb35..000000000000 --- 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 aa6363d47a10..000000000000 --- 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 29c2e3a93d97..90d0b4afd485 100644 --- a/awx/main/tests/unit/api/test_filters.py +++ b/awx/main/tests/unit/api/test_filters.py @@ -19,7 +19,6 @@ WorkflowJobTemplate, WorkflowJobOptions, ) -from awx.main.models.oauth import OAuth2Application from awx.main.models.jobs import JobOptions @@ -28,7 +27,6 @@ [ (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 +38,6 @@ (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 f283b8ec90ac..ec4813d3e249 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 1edcc11edbbb..1bcfdf0826e8 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -343,7 +343,6 @@ # 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 @@ '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 @@ 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/docs/docsite/rst/administration/awx-manage.rst b/docs/docsite/rst/administration/awx-manage.rst index 3de9e6cb819a..5af1ba9a9756 100644 --- a/docs/docsite/rst/administration/awx-manage.rst +++ b/docs/docsite/rst/administration/awx-manage.rst @@ -74,75 +74,12 @@ commands. .. _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`` ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -170,9 +107,6 @@ Use this command to delete all sessions that have expired. Refer to `Django's do -For more information on OAuth2 token management in the AWX user interface, see the :ref:`ug_applications_auth` section of the |atu|. - - Analytics gathering ~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/docsite/rst/administration/configure_awx.rst b/docs/docsite/rst/administration/configure_awx.rst index 3c7c8fecf50e..9aa4c01d79f8 100644 --- a/docs/docsite/rst/administration/configure_awx.rst +++ b/docs/docsite/rst/administration/configure_awx.rst @@ -68,10 +68,6 @@ The System tab allows you to define the base URL for the AWX host, configure ale .. image:: ../common/images/configure-awx-system.png :alt: Miscellaneous System settings window showing all possible configurable options. -.. note:: - - The **Allow External Users to Create Oauth2 Tokens** setting is disabled by default. This ensures external users cannot *create* their own tokens. If you enable then disable it, any tokens created by external users in the meantime will still exist, and are not automatically revoked. - 4. Click **Save** to apply the settings or **Cancel** to abandon the changes. .. _configure_awx_ui: diff --git a/docs/docsite/rst/administration/index.rst b/docs/docsite/rst/administration/index.rst index 7a4ac043da7b..4145e92a4fb3 100644 --- a/docs/docsite/rst/administration/index.rst +++ b/docs/docsite/rst/administration/index.rst @@ -38,7 +38,6 @@ Need help or want to discuss AWX including the documentation? See the :ref:`Comm awx-manage configure_awx isolation_variables - oauth2_token_auth authentication_timeout session_limits custom_rebranding diff --git a/docs/docsite/rst/administration/management_jobs.rst b/docs/docsite/rst/administration/management_jobs.rst index ca243e5f7726..e00c35ee5a01 100644 --- a/docs/docsite/rst/administration/management_jobs.rst +++ b/docs/docsite/rst/administration/management_jobs.rst @@ -18,7 +18,6 @@ Management Jobs Several job types are available for you to schedule and launch: - **Cleanup Activity Stream**: Remove activity stream history older than a specified number of days -- **Cleanup Expired OAuth 2 Tokens**: Remove expired OAuth 2 access tokens and refresh tokens - **Cleanup Expired Sessions**: Remove expired browser sessions from the database - **Cleanup Job Details**: Remove job history older than a specified number of days @@ -90,20 +89,6 @@ An example of a notifications with details specified: .. image:: ../common/images/management-job-add-notification-details.png -Cleanup Expired OAuth2 Tokens -==================================== - -.. index:: - pair: management jobs; cleanup expired OAuth2 tokens - single: expired OAuth2 tokens cleanup management job - -To remove expired OAuth2 tokens, click on the launch (|launch|) button beside **Cleanup Expired OAuth2 Tokens**. - -You can review or set a schedule for cleaning up expired OAuth2 tokens by performing the same procedure described for activity stream management jobs. See :ref:`ag_mgmt_job_schedule` for detail. - -You can also set or review notifications associated with this management job the same way as described in :ref:`ag_mgmt_job_notify` for activity stream management jobs, and refer to :ref:`ug_notifications` for more detail. - - Cleanup Expired Sessions ==================================== diff --git a/docs/docsite/rst/administration/metrics.rst b/docs/docsite/rst/administration/metrics.rst index af508a346435..28b25632d2c2 100644 --- a/docs/docsite/rst/administration/metrics.rst +++ b/docs/docsite/rst/administration/metrics.rst @@ -19,10 +19,6 @@ To set up and use Prometheus, you will need to install Prometheus on a virtual m 1. In the Prometheus config file (typically ``prometheus.yml``), specify a ````, a valid user/password for an AWX user you have created, and a ````. - .. note:: Alternatively, you can provide an OAuth2 token (which can be generated at ``/api/v2/users/N/personal_tokens/``). By default, the config assumes a user with username=admin and password=password. - - Using an OAuth2 Token, created at the ``/api/v2/tokens`` endpoint to authenticate prometheus with AWX, the following example provides a valid scrape config if the URL for your AWX's metrics endpoint was ``https://awx_host:443/metrics``. - :: scrape_configs @@ -51,4 +47,4 @@ To set up and use Prometheus, you will need to install Prometheus on a virtual m .. image:: ../common/images/metrics-prometheus-ui-query-example.png -Refer to the metrics endpoint in AWX API for your instance (``api/v2/metrics``) for more ways to query. \ No newline at end of file +Refer to the metrics endpoint in AWX API for your instance (``api/v2/metrics``) for more ways to query. diff --git a/docs/docsite/rst/administration/oauth2_token_auth.rst b/docs/docsite/rst/administration/oauth2_token_auth.rst deleted file mode 100644 index d99604cf6fe2..000000000000 --- a/docs/docsite/rst/administration/oauth2_token_auth.rst +++ /dev/null @@ -1,460 +0,0 @@ -.. _ag_oauth2_token_auth: - -Token-Based Authentication -================================================== - - -.. index:: - single: token-based authentication - single: authentication - - -OAuth 2 is used for token-based authentication. You can manage OAuth tokens as well as applications, a server-side representation of API clients used to generate tokens. By including an OAuth token as part of the HTTP authentication header, you can authenticate yourself and adjust the degree of restrictive permissions in addition to the base RBAC permissions. Refer to `RFC 6749`_ for more details of OAuth 2 specification. - - .. _`RFC 6749`: https://tools.ietf.org/html/rfc6749 - -For details on using the ``manage`` utility to create tokens, refer to the :ref:`ag_token_utility` section. - - -Managing OAuth 2 Applications and Tokens ------------------------------------------- - -Applications and tokens can be managed as a top-level resource at ``/api//applications`` and ``/api//tokens``. These resources can also be accessed respective to the user at ``/api//users/N/``. Applications can be created by making a **POST** to either ``api//applications`` or ``/api//users/N/applications``. - -Each OAuth 2 application represents a specific API client on the server side. For an API client to use the API via an application token, it must first have an application and issue an access token. Individual applications are accessible via their primary keys: ``/api//applications//``. Here is a typical application: - -:: - - { - "id": 1, - "type": "o_auth2_application", - "url": "/api/v2/applications/2/", - "related": { - "tokens": "/api/v2/applications/2/tokens/" - }, - "summary_fields": { - "organization": { - "id": 1, - "name": "Default", - "description": "" - }, - "user_capabilities": { - "edit": true, - "delete": true - }, - "tokens": { - "count": 0, - "results": [] - } - }, - "created": "2018-07-02T21:16:45.824400Z", - "modified": "2018-07-02T21:16:45.824514Z", - "name": "My Application", - "description": "", - "client_id": "Ecmc6RjjhKUOWJzDYEP8TZ35P3dvsKt0AKdIjgHV", - "client_secret": "7Ft7ym8MpE54yWGUNvxxg6KqGwPFsyhYn9QQfYHlgBxai74Qp1GE4zsvJduOfSFkTfWFnPzYpxqcRsy1KacD0HH0vOAQUDJDCidByMiUIH4YQKtGFM1zE1dACYbpN44E", - "client_type": "confidential", - "redirect_uris": "", - "authorization_grant_type": "password", - "skip_authorization": false, - "organization": 1 - } - - -As shown in the example above, ``name`` is the human-readable identifier of the application. The rest of the fields, like ``client_id`` and ``redirect_uris``, are mainly used for OAuth2 authorization, which is covered later in :ref:`ag_use_oauth_pat`. - -The values for the ``client_id`` and ``client_secret`` fields are generated during creation and are non-editable identifiers of applications, while ``organization`` and ``authorization_grant_type`` are required upon creation and become non-editable. - - -Access Rules for Applications -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Access rules for applications are as follows: - -- System administrators can view and manipulate all applications in the system -- Organization administrators can view and manipulate all applications belonging to Organization members -- Other users can only view, update, and delete their own applications, but cannot create any new applications - -Tokens, on the other hand, are resources used to actually authenticate incoming requests and mask the permissions of the underlying user. There are two ways to create a token: - -- POST to the ``/api/v2/tokens/`` endpoint with ``application`` and ``scope`` fields to point to the related application and specify token scope -- POST to the ``/api/v2/applications//tokens/`` endpoint with the ``scope`` field (the parent application will be automatically linked) - -Individual tokens are accessible via their primary keys: ``/api//tokens//``. Here is an example of a typical token: - - .. code-block:: text - - { - "id": 4, - "type": "o_auth2_access_token", - "url": "/api/v2/tokens/4/", - "related": { - "user": "/api/v2/users/1/", - "application": "/api/v2/applications/1/", - "activity_stream": "/api/v2/tokens/4/activity_stream/" - }, - "summary_fields": { - "application": { - "id": 1, - "name": "Default application for root", - "client_id": "mcU5J5uGQcEQMgAZyr5JUnM3BqBJpgbgL9fLOVch" - }, - "user": { - "id": 1, - "username": "root", - "first_name": "", - "last_name": "" - } - }, - "created": "2018-02-23T14:39:32.618932Z", - "modified": "2018-02-23T14:39:32.643626Z", - "description": "App Token Test", - "user": 1, - "token": "*************", - "refresh_token": "*************", - "application": 1, - "expires": "2018-02-24T00:39:32.618279Z", - "scope": "read" - }, - - -For an OAuth 2 token, the only fully editable fields are ``scope`` and ``description``. The ``application`` field is non-editable on update, and all other fields are entirely non-editable, and are auto-populated during creation, as follows: - -- ``user`` field corresponds to the user the token is created for, and in this case, is also the user creating the token -- ``expires`` is generated according to AWX configuration setting ``OAUTH2_PROVIDER`` -- ``token`` and ``refresh_token`` are auto-generated to be non-clashing random strings - -Both application tokens and personal access tokens are shown at the ``/api/v2/tokens/`` endpoint. The ``application`` field in the personal access tokens is always **null**. This is a good way to differentiate the two types of tokens. - - -Access rules for tokens -^^^^^^^^^^^^^^^^^^^^^^^^^ - -Access rules for tokens are as follows: - -- Users can create a token if they are able to view the related application; and are also able to create a personal token for themselves -- System administrators are able to view and manipulate every token in the system -- Organization administrators are able to view and manipulate all tokens belonging to Organization members -- System Auditors can view all tokens and applications -- Other normal users are only able to view and manipulate their own tokens - -.. note:: - Users can only view the token or refresh the token value at the time of creation only. - -.. _ag_use_oauth_pat: - -Using OAuth 2 Token System for Personal Access Tokens (PAT) ---------------------------------------------------------------- - -The easiest and most common way to obtain an OAuth 2 token is to create a personal access token at the ``/api/v2/users//personal_tokens/`` endpoint, as shown in this example below: - -:: - - curl -H "Content-type: application/json" -d '{"description":"Personal AWX CLI token", "application":null, "scope":"write"}' https://:@/api/v2/users//personal_tokens/ | python -m json.tool - -You could also pipe the JSON output through ``jq``, if installed. - - -Following is an example of using the personal token to access an API endpoint using curl: - -:: - - curl -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{}' https://awx/api/v2/job_templates/5/launch/ - - -In AWX, the OAuth 2 system is built on top of the `Django Oauth Toolkit`_, which provides dedicated endpoints for authorizing, revoking, and refreshing tokens. These endpoints can be found under the ``/api/v2/users//personal_tokens/`` endpoint, which also provides detailed examples on some typical usage of those endpoints. These special OAuth 2 endpoints only support using the ``x-www-form-urlencoded`` **Content-type**, so none of the ``api/o/*`` endpoints accept ``application/json``. - - -.. _`Django Oauth Toolkit`: https://django-oauth-toolkit.readthedocs.io/en/latest/ - -.. note:: - You can also request tokens using the ``/api/o/token`` endpoint by specifying ``null`` for the application type. - - -Alternatively, you can :ref:`add tokens ` for users through the AWX user interface, as well as configure the expiration of an access token and its associated refresh token (if applicable). - -.. image:: ../common/images/configure-awx-system-misc-sys-token-expire.png - - -Token scope mask over RBAC system -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The scope of an OAuth 2 token is a space-separated string composed of valid scope keywords, 'read' and 'write'. These keywords are configurable and used to specify permission level of the authenticated API client. Read and write scopes provide a mask layer over the Role-Based Access Control (RBAC) permission system of AWX. Specifically, a 'write' scope gives the authenticated user the full permissions the RBAC system provides, while a 'read' scope gives the authenticated user only read permissions the RBAC system provides. Note that 'write' implies 'read' as well. - -For example, if you have administrative permissions to a job template, you can view, modify, launch, and delete the job template if authenticated via session or basic authentication. In contrast, if you are authenticated using OAuth 2 token, and the related token scope is 'read', you can only view, but not manipulate or launch the job template, despite being an administrator. If the token scope is 'write' or 'read write', you can take full advantage of the job template as its administrator. - - -To acquire and use a token, first create an application token: - -1. Make an application with ``authorization_grant_type`` set to ``password``. HTTP POST the following to the ``/api/v2/applications/`` endpoint (supplying your own organization ID): - -:: - - - { - "name": "Admin Internal Application", - "description": "For use by secure services & clients. ", - "client_type": "confidential", - "redirect_uris": "", - "authorization_grant_type": "password", - "skip_authorization": false, - "organization": - } - -2. Make a token and POST to the ``/api/v2/tokens/`` endpoint: - -:: - - { - "description": "My Access Token", - "application": , - "scope": "write" - } - -This returns a that you can use to authenticate with for future requests (this will not be shown again). - -3. Use the token to access a resource. The following uses curl as an example: - -:: - - curl -H "Authorization: Bearer " -H "Content-Type: application/json" https:///api/v2/users/ - - -The ``-k`` flag may be needed if you have not set up a CA yet and are using SSL. - - -To revoke a token, you can make a DELETE on the detail page for that token, using that token's ID. For example: - -:: - - curl -u : -X DELETE https:///api/v2/tokens// - - -Similarly, using a token: - -:: - - curl -H "Authorization: Bearer " -X DELETE https:///api/v2/tokens// - - -.. _ag_oauth2_token_auth_grant_types: - -Application Functions ------------------------ - -This page lists OAuth 2 utility endpoints used for authorization, token refresh, and revoke. The ``/api/o/`` endpoints are not meant to be used in browsers and do not support HTTP GET. The endpoints prescribed here strictly follow RFC specifications for OAuth 2, so use that for detailed reference. The following is an example of the typical usage of these endpoints in AWX, in particular, when creating an application using various grant types: - - - Authorization Code - - Password - -.. note:: - - You can perform any of the application functions described here using AWX user interface. Refer to the :ref:`ug_applications_auth` section of the |atu| for more detail. - - - -Application using ``authorization code`` grant type -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The application ``authorization code`` grant type should be used when access tokens need to be issued directly to an external application or service. - -.. note:: - - You can only use the ``authorization code`` type to acquire an access token when using an application. When integrating an external webapp with AWX, that webapp may need to create OAuth2 Tokens on behalf of users in that other webapp. Creating an application in AWX with the ``authorization code`` grant type is the preferred way to do this because: - - - this allows an external application to obtain a token from AWX for a user, using their credentials. - - compartmentalized tokens issued for a particular application allows those tokens to be easily managed (revoke all tokens associated with that application without having to revoke *all* tokens in the system, for example) - - To create an application named *AuthCodeApp* with the ``authorization-code`` grant type, perform a POST to the ``/api/v2/applications/`` endpoint: - -:: - - { - "name": "AuthCodeApp", - "user": 1, - "client_type": "confidential", - "redirect_uris": "http:///api/v2", - "authorization_grant_type": "authorization-code", - "skip_authorization": false - } - - - .. _`Django-oauth-toolkit simple test application`: http://django-oauth-toolkit.herokuapp.com/consumer/ - -The workflow that occurs when you issue a **GET** to the ``authorize`` endpoint from the client application with the ``response_type``, ``client_id``, ``redirect_uris``, and ``scope``: - -1. AWX responds with the authorization code and status to the ``redirect_uri`` specified in the application. -2. The client application then makes a **POST** to the ``api/o/token/`` endpoint on AWX with the ``code``, ``client_id``, ``client_secret``, ``grant_type``, and ``redirect_uri``. -3. AWX responds with the ``access_token``, ``token_type``, ``refresh_token``, and ``expires_in``. - - -Refer to `Django's Test Your Authorization Server`_ toolkit to test this flow. - - .. _`Django's Test Your Authorization Server`: http://django-oauth-toolkit.readthedocs.io/en/latest/tutorial/tutorial_01.html#test-your-authorization-server - -You may specify the number of seconds an authorization code remains valid in the **System settings** screen: - -.. image:: ../common/images/configure-awx-system-misc-sys-authcode-expire.png - - -Requesting an access token after this duration will fail. The duration defaults to 600 seconds (10 minutes), based on the `RFC6749 `_ recommendation. - -The best way to set up app integrations with AWX using the Authorization Code grant type is to whitelist the origins for those cross-site requests. More generally, you need to whitelist the service or application you are integrating with AWX, for which you want to provide access tokens. To do this, have your Administrator add this whitelist to their local AWX settings: - -:: - - CORS_ALLOWED_ORIGIN_REGEXES = [ - r"http://django-oauth-toolkit.herokuapp.com*", - r"http://www.example.com*" - ] - -Where ``http://django-oauth-toolkit.herokuapp.com`` and ``http://www.example.com`` are applications needing tokens with which to access AWX. - -Application using ``password`` grant type -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The ``password`` grant type or ``Resource owner password-based`` grant type is ideal for users who have native access to the web app and should be used when the client is the Resource owner. The following supposes an application, 'Default Application' with grant type ``password``: - -:: - - { - "id": 6, - "type": "application", - ... - "name": "Default Application", - "user": 1, - "client_id": "gwSPoasWSdNkMDtBN3Hu2WYQpPWCO9SwUEsKK22l", - "client_secret": "fI6ZpfocHYBGfm1tP92r0yIgCyfRdDQt0Tos9L8a4fNsJjQQMwp9569eIaUBsaVDgt2eiwOGe0bg5m5vCSstClZmtdy359RVx2rQK5YlIWyPlrolpt2LEpVeKXWaiybo", - "client_type": "confidential", - "redirect_uris": "", - "authorization_grant_type": "password", - "skip_authorization": false - } - - -Logging in is not required for ``password`` grant type, so you can simply use curl to acquire a personal access token through the ``/api/v2/tokens/`` endpoint: - - .. code-block:: text - - curl --user : -H "Content-type: application/json" \ - --data '{ - "description": "Token for Nagios Monitoring app", - "application": 1, - "scope": "write" - }' \ - https:///api/v2/tokens/ - - -.. note:: - - The special OAuth 2 endpoints only support using the ``x-www-form-urlencoded`` **Content-type**, so as a result, none of the ``api/o/*`` endpoints accept ``application/json``. - -Upon success, a response displays in JSON format containing the access token, refresh token and other information: - -:: - - HTTP/1.1 200 OK - Server: nginx/1.12.2 - Date: Tue, 05 Dec 2017 16:48:09 GMT - Content-Type: application/json - Content-Length: 163 - Connection: keep-alive - Content-Language: en - Vary: Accept-Language, Cookie - Pragma: no-cache - Cache-Control: no-store - Strict-Transport-Security: max-age=15768000 - - {"access_token": "9epHOqHhnXUcgYK8QanOmUQPSgX92g", "token_type": "Bearer", "expires_in": 315360000000, "refresh_token": "jMRX6QvzOTf046KHee3TU5mT3nyXsz", "scope": "read"} - - -Application Token Functions ------------------------------- - -This section describes the refresh and revoke functions associated with tokens. Everything that follows (Refreshing and revoking tokens at the ``/api/o/`` endpoints) can currently only be done with application tokens. - - -Refresh an existing access token -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The following example shows an existing access token with a refresh token provided: - -:: - - { - "id": 35, - "type": "access_token", - ... - "user": 1, - "token": "omMFLk7UKpB36WN2Qma9H3gbwEBSOc", - "refresh_token": "AL0NK9TTpv0qp54dGbC4VUZtsZ9r8z", - "application": 6, - "expires": "2017-12-06T03:46:17.087022Z", - "scope": "read write" - } - -The ``/api/o/token/`` endpoint is used for refreshing the access token: - -:: - - curl \ - -d "grant_type=refresh_token&refresh_token=AL0NK9TTpv0qp54dGbC4VUZtsZ9r8z" \ - -u "gwSPoasWSdNkMDtBN3Hu2WYQpPWCO9SwUEsKK22l:fI6ZpfocHYBGfm1tP92r0yIgCyfRdDQt0Tos9L8a4fNsJjQQMwp9569eIaUBsaVDgt2eiwOGe0bg5m5vCSstClZmtdy359RVx2rQK5YlIWyPlrolpt2LEpVeKXWaiybo" \ - http:///api/o/token/ -i - - -In the above POST request, ``refresh_token`` is provided by ``refresh_token`` field of the access token above that. The authentication information is of format ``:``, where ``client_id`` and ``client_secret`` are the corresponding fields of the underlying related application of the access token. - -.. note:: - - The special OAuth 2 endpoints only support using the ``x-www-form-urlencoded`` **Content-type**, so as a result, none of the ``api/o/*`` endpoints accept ``application/json``. - -Upon success, a response displays in JSON format containing the new (refreshed) access token with the same scope information as the previous one: - -:: - - HTTP/1.1 200 OK - Server: nginx/1.12.2 - Date: Tue, 05 Dec 2017 17:54:06 GMT - Content-Type: application/json - Content-Length: 169 - Connection: keep-alive - Content-Language: en - Vary: Accept-Language, Cookie - Pragma: no-cache - Cache-Control: no-store - Strict-Transport-Security: max-age=15768000 - - {"access_token": "NDInWxGJI4iZgqpsreujjbvzCfJqgR", "token_type": "Bearer", "expires_in": 315360000000, "refresh_token": "DqOrmz8bx3srlHkZNKmDpqA86bnQkT", "scope": "read write"} - -Essentially, the refresh operation replaces the existing token by deleting the original and then immediately creating a new token with the same scope and related application as the original one. Verify that new token is present and the old one is deleted in the ``/api/v2/tokens/`` endpoint. - -.. _ag_oauth2_token_revoke: - -Revoke an access token -^^^^^^^^^^^^^^^^^^^^^^^^^ - -Similarly, you can revoke an access token by using the ``/api/o/revoke-token/`` endpoint. - -Revoking an access token by this method is the same as deleting the token resource object, but it allows you to delete a token by providing its token value, and the associated ``client_id`` (and ``client_secret`` if the application is ``confidential``). For example: - -:: - - curl -d "token=rQONsve372fQwuc2pn76k3IHDCYpi7" \ - -u "gwSPoasWSdNkMDtBN3Hu2WYQpPWCO9SwUEsKK22l:fI6ZpfocHYBGfm1tP92r0yIgCyfRdDQt0Tos9L8a4fNsJjQQMwp9569eIaUBsaVDgt2eiwOGe0bg5m5vCSstClZmtdy359RVx2rQK5YlIWyPlrolpt2LEpVeKXWaiybo" \ - http:///api/o/revoke_token/ -i - -.. note:: - - The special OAuth 2 endpoints only support using the ``x-www-form-urlencoded`` **Content-type**, so as a result, none of the ``api/o/*`` endpoints accept ``application/json``. - - -Alternatively, you can use the ``manage`` utility, :ref:`ag_manage_utility_revoke_tokens`, to revoke tokens as described in the :ref:`ag_token_utility` section. - - -This setting can be configured at the system-level in the AWX User Interface: - -.. image:: ../common/images/configure-awx-system-oauth2-tokens-toggle.png - - -Upon success, a response of ``200 OK`` displays. Verify the deletion by checking whether the token is present in the ``/api/v2/tokens/`` endpoint. diff --git a/docs/docsite/rst/administration/performance.rst b/docs/docsite/rst/administration/performance.rst index 05b1c0f62be5..8a8f0b3e56bc 100644 --- a/docs/docsite/rst/administration/performance.rst +++ b/docs/docsite/rst/administration/performance.rst @@ -372,5 +372,3 @@ For workloads with high levels of API interaction, best practices include: - Set max connections per node to 100 - Use dynamic inventory sources instead of individually creating inventory hosts via the API - Use webhook notifications instead of polling for job status - -Since the published blog, additional observations have been made in the field regarding authentication methods. For automation clients that will make many requests in rapid succession, using tokens is a best practice, because depending on the type of user, there may be additional overhead when using basic authentication. Refer to :ref:`ag_oauth2_token_auth` for detail on how to generate and use tokens. diff --git a/docs/docsite/rst/rest_api/authentication.rst b/docs/docsite/rst/rest_api/authentication.rst index 5c636ec7dba5..9f8faab36d28 100644 --- a/docs/docsite/rst/rest_api/authentication.rst +++ b/docs/docsite/rst/rest_api/authentication.rst @@ -5,7 +5,6 @@ Authentication Methods Using the API .. index:: pair: session; authentication pair: basic; authentication - pair: OAuth 2 Token; authentication pair: SSO; authentication This chapter describes different authentication methods, the best use case for each, and examples: @@ -46,7 +45,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. For programmatic integration with AWX, see :ref:`api_oauth2_auth`. +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: @@ -83,7 +82,7 @@ When a user is successfully authenticated with this method, the server will resp 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. :ref:`api_oauth2_auth` is recommended for accessing the API when at all possible. +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: @@ -99,89 +98,3 @@ For more information about the Basic HTTP Authentication scheme, see `RFC 7617 < You can disable the Basic Auth for security purposes from the Miscellaneous Authentication settings of the AWX UI Settings menu: .. image:: ../common/images/configure-awx-auth-basic-off.png - -.. _api_oauth2_auth: - -OAuth 2 Token Authentication ------------------------------ - -OAuth (Open Authorization) is an open standard for token-based authentication and authorization. OAuth 2 authentication is commonly used when interacting with the AWX API programmatically. Like Basic Auth, an OAuth 2 token is supplied with each API request via the Authorization header. Unlike Basic Auth, OAuth 2 tokens have a configurable timeout and are scopable. Tokens have a configurable expiration time and can be easily revoked for one user or for the entire AWX system by an admin if needed. This can be done with the :ref:`ag_manage_utility_revoke_tokens` management command, which is covered in more detail in |ata| or by using the API as explained in :ref:`ag_oauth2_token_revoke`. - -.. note:: - - By default, external users such as those created by SSO are not allowed to generate OAuth tokens for security purposes. This can be changed from the Miscellaneous Authentication settings of the AWX UI Settings menu: - - .. image:: ../common/images/configure-awx-external-tokens-off.png - -The different methods for obtaining OAuth 2 Access Tokens in AWX are: - -- Personal access tokens (PAT) -- Application Token: Password grant type -- Application Token: Implicit grant type -- Application Token: Authorization Code grant type - -For more information on the above methods, see :ref:`ag_oauth2_token_auth` in the |ata|. - - -First, a user needs to create an OAuth 2 Access Token in the API or in their User’s **Tokens** tab in the UI. For further detail on creating them through the UI, see :ref:`ug_users_tokens`. For the purposes of this example, use the PAT method for creating a token in the API. Upon token creation, the user can set the scope. - -.. note:: - - The expiration time of the token can be configured system-wide. See :ref:`ag_use_oauth_pat` for more detail. - -Token authentication is best used for any programmatic use of the AWX API, such as Python scripts or tools like curl, as in the example for creating a PAT (without an associated application) below. - -**Curl Example** - -.. code-block:: text - - curl -u user:password -k -X POST https:///api/v2/tokens/ - - -This call will return JSON data like: - -.. image:: ../common/images/api_oauth2_json_returned_token_value.png - -The value of the ``token`` property is what you can now use to perform a GET request for an AWX resource, e.g., Hosts. - -.. code-block:: text - - curl -k -X POST \ - -H “Content-Type: application/json” - -H “Authorization: Bearer ” \ - https:///api/v2/hosts/ - -Similarly, you can launch a job by making a POST to the job template that you want to launch. - -.. code-block:: text - - curl -k -X POST \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - --data '{"limit" : "ansible"}' \ - https:///api/v2/job_templates/14/launch/ - - -**Python Example** - -`awxkit `_ is an open source tool that makes it easy to use HTTP requests to access the AWX API. -You can have awxkit acquire a PAT on your behalf by using the ``awxkit login`` command. Refer to the :ref:`api_start` for more detail. - -For more information on how to use OAuth 2 in AWX in the context of integrating external applications, see :ref:`ag_oauth2_token_auth` in the |ata|. - -If you need to write custom requests, you can write a Python script using `Python library requests `_, like in this example: - -.. code-block:: text - - import requests - oauth2_token_value = 'y1Q8ye4hPvT61aQq63Da6N1C25jiA' # your token value from AWX - url = 'https:///api/v2/users/' - payload = {} - headers = {'Authorization': 'Bearer ' + oauth2_token_value,} - - # makes request to awx user endpoint - response = requests.request('GET', url, headers=headers, data=payload, - allow_redirects=False, verify=False) - - # prints json returned from awx with formatting - print(json.dumps(response.json(), indent=4, sort_keys=True)) diff --git a/docs/docsite/rst/userguide/applications_auth.rst b/docs/docsite/rst/userguide/applications_auth.rst deleted file mode 100644 index 36c979b42f54..000000000000 --- a/docs/docsite/rst/userguide/applications_auth.rst +++ /dev/null @@ -1,114 +0,0 @@ -.. _ug_applications_auth: - -Applications -===================== - -.. index:: - single: token authentication - single: authentication - pair: applications; authentication - pair: applications; tokens - - -Creating and configuring token-based authentication for external applications makes it easier for external applications such as ServiceNow and Jenkins to integrate with AWX. OAuth 2 allows you to use tokens to share certain data with an application without disclosing login information, and furthermore, these tokens can be scoped as "read-only". You create an application that is representative of the external application you are integrating with, then use it to create tokens for that application to use on behalf of the users of the external application. - -Having these tokens associated to an application resource gives you the ability to manage all tokens issued for a particular application more easily. By separating token issuance under Applications, you can revoke all tokens based on the Application without having to revoke all tokens in the system. - -When integrating an external web app with AWX that web app may need to create OAuth2 Tokens on behalf of users in that other web app. Creating an application with the Authorization Code grant type is the preferred way to do this because: - -- external applications can obtain a token for users, using their credentials -- compartmentalized tokens issued for a particular application, allows those tokens to be easily managed (revoke all tokens associated with that application, for example) - - -Getting Started with Applications ------------------------------------ - -.. index:: - pair: applications; getting started - -Access the Applications page by clicking **Applications** from the left navigation bar. The Applications page displays a search-able list of all available Applications currently managed by AWX and can be sorted by **Name**. - -|Applications - home with example apps| - -.. |Applications - home with example apps| image:: ../common/images/apps-list-view-examples.png - :alt: Applications list view - -If no other applications exist, only a gray box with a message to add applications displays. - -.. image:: ../common/images/apps-list-view-empty.png - :alt: No applications found in the list view - -.. _ug_applications_auth_create: - -Create a new application ------------------------------- - -.. index:: - pair: applications; create - pair: applications; adding new - -Token-based authentication for users can be configured in the Applications window. - -1. In the AWX User Interface, click **Applications** from the left navigation bar. - -The Applications window opens. - -2. Click the **Add** button located in the upper right corner of the Applications window. - -The New Application window opens. - -|Create application| - -.. |Create application| image:: ../common/images/apps-create-new.png - :alt: Create new application dialog - -3. Enter the following details in **Create New Application** window: - -- **Name** (required): provide a name for the application you want to create -- **Description**: optionally provide a short description for your application -- **Organization** (required): provide an organization for which this application is associated -- **Authorization Grant Type** (required): Select from one of the grant types to use in order for the user to acquire tokens for this application. Refer to :ref:`grant types ` in the Applications section of the |ata|. -- **Redirect URIS**: Provide a list of allowed URIs, separated by spaces. This is required if you specified the grant type to be **Authorization code**. -- **Client Type** (required): Select the level of security of the client device - -4. When done, click **Save** or **Cancel** to abandon your changes. Upon saving, the client ID displays in a pop-up window. - -.. image:: ../common/images/apps-client-id-popup.png - :alt: Client ID popup - -Applications - Tokens -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. index:: - pair: applications; tokens - pair: applications; adding tokens - -Selecting the **Tokens** view displays a list of the users that have tokens to access the application. - -|Applications - tokens list| - -.. |Applications - tokens list| image:: ../common/images/apps-tokens-list-view-examples.png - :alt: Application tokens list view - -Tokens can only access resources that its associated user can access, and can be limited further by specifying the scope of the token. - - -.. _ug_tokens_auth_create: - -Add Tokens -^^^^^^^^^^^^^^^^^^^^^^ - -Tokens are added through the Users screen and can be associated with an application at that time. Specifying an application can be performed directly in the User's token settings. You can create a token for *your* user in the Tokens configuration tab, meaning only you can create and see your tokens in your own user screen. To add a token: - -1. Access the Users list view by clicking **Users** from the left navigation bar then click on your user to configure your OAuth 2 tokens. - -.. note:: - - You can only create OAuth 2 Tokens for your user via the API or UI, which means you can only access your own user profile in order to configure or view your tokens. If you are an admin and need to create or remove tokens for other users, see the revoke and create commands in the :ref:`Token and session management ` section of the |ata|. - -.. include:: ../common/add-token.rst - -To verify the application in the example above now shows the user with the appropriate token, go to the **Tokens** tab of the Applications window: - -.. image:: ../common/images/apps-tokens-list-view-example2.png - :alt: Verifying a specific user application token diff --git a/docs/docsite/rst/userguide/credentials.rst b/docs/docsite/rst/userguide/credentials.rst index 55c011303d89..c795bfd19cf3 100644 --- a/docs/docsite/rst/userguide/credentials.rst +++ b/docs/docsite/rst/userguide/credentials.rst @@ -521,7 +521,6 @@ The Red Hat Ansible Automation Platform credentials have the following inputs th - **Red Hat Ansible Automation Platform**: The base URL or IP address of the other instance to connect to. - **Username**: The username to use to connect to it. - **Password**: The password to use to connect to it. -- **Oauth Token**: If username and password is not used, provide an OAuth token to use to authenticate. Red Hat Satellite 6 diff --git a/docs/docsite/rst/userguide/main_menu.rst b/docs/docsite/rst/userguide/main_menu.rst index 73873d1ab9e1..1e0481c3be13 100644 --- a/docs/docsite/rst/userguide/main_menu.rst +++ b/docs/docsite/rst/userguide/main_menu.rst @@ -172,7 +172,6 @@ The **Administration** menu provides access to the various administrative option - :ref:`ag_management_jobs` - :ref:`ug_instance_groups` - :ref:`Instances ` -- :ref:`ug_applications_auth` - :ref:`ug_execution_environments` - :ref:`ag_topology_viewer` diff --git a/docs/docsite/rst/userguide/overview.rst b/docs/docsite/rst/userguide/overview.rst index 318c61349e61..d34e10e14dc3 100644 --- a/docs/docsite/rst/userguide/overview.rst +++ b/docs/docsite/rst/userguide/overview.rst @@ -187,8 +187,6 @@ Authentication Enhancements .. index:: pair: features; authentication - pair: features; OAuth 2 token - Cluster Management ~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/docsite/rst/userguide/users.rst b/docs/docsite/rst/userguide/users.rst index 772e6caec58a..a8d4beb8f106 100644 --- a/docs/docsite/rst/userguide/users.rst +++ b/docs/docsite/rst/userguide/users.rst @@ -64,10 +64,7 @@ The same window opens whether you click on the user's name, or the Edit (|edit-b .. image:: ../common/images/users-last-login-info.png :alt: User details with last login information -When you log in as yourself, and view the details of your own user profile, you can manage tokens from your user profile. See :ref:`ug_users_tokens` for more detail. - -.. image:: ../common/images/user-with-token-button.png - :alt: User details with Tokens tab highlighted +When you log in as yourself, and view the details of your own user profile. .. _ug_users_delete: @@ -170,14 +167,3 @@ To remove Permissions for a particular resource, click the disassociate (x) butt .. note:: You can also add teams, individual, or multiple users and assign them permissions at the object level (templates, credentials, inventories, projects, organizations, or instance groups) as well. This feature reduces the time for an organization to onboard many users at one time. - -.. _ug_users_tokens: - -Users - Tokens -~~~~~~~~~~~~~~~ - -The **Tokens** tab will only be present for your user (yourself). Before you add a token for your user, you may want to :ref:`create an application ` if you want to associate your token to it. You may also create a personal access token (PAT) without associating it with any application. To create a token for your user: - -1. If not already selected, click on your user from the Users list view to configure your OAuth 2 tokens. - -.. include:: ../common/add-token.rst From 8e15ffae40cd9bf72e7390490c80e0f9cbf0f093 Mon Sep 17 00:00:00 2001 From: Mike Graves Date: Fri, 9 Aug 2024 14:32:46 -0400 Subject: [PATCH 2/4] Fix docs build --- docs/docsite/rst/common/add-token.rst | 32 --------------------------- docs/docsite/rst/userguide/index.rst | 1 - 2 files changed, 33 deletions(-) delete mode 100644 docs/docsite/rst/common/add-token.rst diff --git a/docs/docsite/rst/common/add-token.rst b/docs/docsite/rst/common/add-token.rst deleted file mode 100644 index 8c47228dccb7..000000000000 --- a/docs/docsite/rst/common/add-token.rst +++ /dev/null @@ -1,32 +0,0 @@ - - -2. Click the **Tokens** tab from your user's profile. - -When no tokens are present, the Tokens screen prompts you to add them: - -.. image:: ../common/images/users-tokens-empty.png - -3. Click the **Add** button, which opens the Create Token window. - -4. Enter the following details in Create Token window: - - - **Application**: enter the name of the application with which you want to associate your token. Alternatively, you can search for it by clicking the |search| button. This opens a separate window that allows you to choose from the available options. Use the Search bar to filter by name if the list is extensive. Leave this field blank if you want to create a Personal Access Token (PAT) that is not linked to any application. - - - **Description**: optionally provide a short description for your token. - - - **Scope** (required): specify the level of access you want this token to have. - -.. |search| image:: ../common/images/search-button.png - -5. When done, click **Save** or **Cancel** to abandon your changes. - -After the token is saved, the newly created token for the user displays with the token information and when it expires. - -.. image:: ../common/images/users-token-information-example.png - -.. note:: This is the only time the token value and associated refresh token value will ever be shown. - -In the user's profile, the application for which it is assigned to and its expiration displays in the token list view. - -.. image:: ../common/images/users-token-assignment-example.png - diff --git a/docs/docsite/rst/userguide/index.rst b/docs/docsite/rst/userguide/index.rst index e5550d2d28d2..940dcfad7826 100644 --- a/docs/docsite/rst/userguide/index.rst +++ b/docs/docsite/rst/userguide/index.rst @@ -30,7 +30,6 @@ Need help or want to discuss AWX including the documentation? See the :ref:`Comm rbac credentials credential_types - applications_auth execution_environments ee_reference projects From 5e2aa26da85c35d85290d1283f60168fb24f8865 Mon Sep 17 00:00:00 2001 From: Mike Graves Date: Fri, 9 Aug 2024 15:15:53 -0400 Subject: [PATCH 3/4] Fix linters --- awx/api/fields.py | 1 - awx/api/serializers.py | 2 +- awx/api/views/root.py | 2 +- awx/main/migrations/0041_v330_update_oauth_refreshtoken.py | 4 +--- awx/main/migrations/0183_pre_django_upgrade.py | 2 -- awx/main/models/__init__.py | 1 - awx/main/signals.py | 1 - awx/main/tests/unit/api/test_filters.py | 1 - 8 files changed, 3 insertions(+), 11 deletions(-) diff --git a/awx/api/fields.py b/awx/api/fields.py index ecdd65881108..0a85d8f15579 100644 --- a/awx/api/fields.py +++ b/awx/api/fields.py @@ -9,7 +9,6 @@ from rest_framework import serializers # AWX -from awx.conf import fields from awx.main.models import Credential __all__ = ['BooleanNullField', 'CharNullField', 'ChoiceNullField', 'VerbatimField'] diff --git a/awx/api/serializers.py b/awx/api/serializers.py index c720633a7ca8..6e13163a6a9f 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -46,7 +46,7 @@ # 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, diff --git a/awx/api/views/root.py b/awx/api/views/root.py index f8dad5b928f6..af0a9c5b7b13 100644 --- a/awx/api/views/root.py +++ b/awx/api/views/root.py @@ -28,7 +28,7 @@ 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 diff --git a/awx/main/migrations/0041_v330_update_oauth_refreshtoken.py b/awx/main/migrations/0041_v330_update_oauth_refreshtoken.py index e728eca2e85b..0264957c2666 100644 --- a/awx/main/migrations/0041_v330_update_oauth_refreshtoken.py +++ b/awx/main/migrations/0041_v330_update_oauth_refreshtoken.py @@ -2,9 +2,7 @@ # 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): diff --git a/awx/main/migrations/0183_pre_django_upgrade.py b/awx/main/migrations/0183_pre_django_upgrade.py index fefb12756503..e2b844ab992e 100644 --- a/awx/main/migrations/0183_pre_django_upgrade.py +++ b/awx/main/migrations/0183_pre_django_upgrade.py @@ -1,8 +1,6 @@ # 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): diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index 081496605a26..6b58411d7022 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -93,7 +93,6 @@ WorkflowApproval, WorkflowApprovalTemplate, ) -from awx.api.versioning import reverse # Add custom methods to User model for permissions checks. diff --git a/awx/main/signals.py b/awx/main/signals.py index 5d096e255a8c..8d2d77be5e0a 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -53,7 +53,6 @@ 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 diff --git a/awx/main/tests/unit/api/test_filters.py b/awx/main/tests/unit/api/test_filters.py index 90d0b4afd485..78cc7401cf15 100644 --- a/awx/main/tests/unit/api/test_filters.py +++ b/awx/main/tests/unit/api/test_filters.py @@ -9,7 +9,6 @@ from awx.main.models import ( AdHocCommand, - ActivityStream, Job, JobTemplate, SystemJob, From f847bdcf7ebc7bf0faafdd526218a544dd7306c5 Mon Sep 17 00:00:00 2001 From: Mike Graves Date: Thu, 17 Oct 2024 13:43:32 -0400 Subject: [PATCH 4/4] Update migrations after rebase --- ...itystream_o_auth2_access_token_and_more.py | 29 -------------- .../0197_delete_oauth2application.py | 16 -------- ...th2application_unique_together_and_more.py | 39 +++++++++++++++++++ 3 files changed, 39 insertions(+), 45 deletions(-) delete mode 100644 awx/main/migrations/0196_remove_activitystream_o_auth2_access_token_and_more.py delete mode 100644 awx/main/migrations/0197_delete_oauth2application.py create mode 100644 awx/main/migrations/0198_alter_oauth2application_unique_together_and_more.py diff --git a/awx/main/migrations/0196_remove_activitystream_o_auth2_access_token_and_more.py b/awx/main/migrations/0196_remove_activitystream_o_auth2_access_token_and_more.py deleted file mode 100644 index 1de270a19b99..000000000000 --- a/awx/main/migrations/0196_remove_activitystream_o_auth2_access_token_and_more.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 4.2.10 on 2024-08-07 15:38 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0195_EE_permissions'), - ] - - operations = [ - migrations.RemoveField( - model_name='activitystream', - name='o_auth2_access_token', - ), - migrations.RemoveField( - model_name='activitystream', - name='o_auth2_application', - ), - migrations.AlterField( - model_name='oauth2application', - name='client_id', - field=models.CharField(db_index=True, default=lambda: "", max_length=100, unique=True), - ), - migrations.DeleteModel( - name='OAuth2AccessToken', - ), - ] diff --git a/awx/main/migrations/0197_delete_oauth2application.py b/awx/main/migrations/0197_delete_oauth2application.py deleted file mode 100644 index 027777e972e4..000000000000 --- a/awx/main/migrations/0197_delete_oauth2application.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 4.2.10 on 2024-08-07 15:39 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('main', '0196_remove_activitystream_o_auth2_access_token_and_more'), - ] - - operations = [ - migrations.DeleteModel( - name='OAuth2Application', - ), - ] diff --git a/awx/main/migrations/0198_alter_oauth2application_unique_together_and_more.py b/awx/main/migrations/0198_alter_oauth2application_unique_together_and_more.py new file mode 100644 index 000000000000..5b8b622cfa06 --- /dev/null +++ b/awx/main/migrations/0198_alter_oauth2application_unique_together_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.10 on 2024-10-17 17:23 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0197_remove_sso_app_content'), + ] + + 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', + ), + ]