mirror of
https://github.com/ansible/awx.git
synced 2026-05-02 07:05:28 -02:30
Remove oauth provider (#15666)
* Remove oauth provider This removes the oauth provider functionality from awx. The oauth2_provider app and all references to it have been removed. Migrations to delete the two tables that locally overwrote oauth2_provider tables are included. This change does not include migrations to delete the tables provided by the oauth2_provider app. Also not included here are changes to awxkit, awx_collection or the ui. * Fix linters * Update migrations after rebase * Update collection tests for auth changes The changes in https://github.com/ansible/awx/pull/15554 will cause a few collection tests to fail, depending on what the test configuration is. This changes the tests to look for a specific warning rather than counting the number of warnings emitted. * Update migration * Removed unused oauth_scopes references --------- Co-authored-by: Mike Graves <mgraves@redhat.com> Co-authored-by: Alan Rominger <arominge@redhat.com>
This commit is contained in:
@@ -60,25 +60,6 @@ else:
|
||||
from django.db import connection
|
||||
|
||||
|
||||
def oauth2_getattribute(self, attr):
|
||||
# Custom method to override
|
||||
# oauth2_provider.settings.OAuth2ProviderSettings.__getattribute__
|
||||
from django.conf import settings
|
||||
from oauth2_provider.settings import DEFAULTS
|
||||
|
||||
val = None
|
||||
if (isinstance(attr, str)) and (attr in DEFAULTS) and (not attr.startswith('_')):
|
||||
# certain Django OAuth Toolkit migrations actually reference
|
||||
# setting lookups for references to model classes (e.g.,
|
||||
# oauth2_settings.REFRESH_TOKEN_MODEL)
|
||||
# If we're doing an OAuth2 setting lookup *while running* a migration,
|
||||
# don't do our usual database settings lookup
|
||||
val = settings.OAUTH2_PROVIDER.get(attr)
|
||||
if val is None:
|
||||
val = object.__getattribute__(self, attr)
|
||||
return val
|
||||
|
||||
|
||||
def prepare_env():
|
||||
# Update the default settings environment variable based on current mode.
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'awx.settings.%s' % MODE)
|
||||
@@ -89,12 +70,6 @@ def prepare_env():
|
||||
if not settings.DEBUG: # pragma: no cover
|
||||
warnings.simplefilter('ignore', DeprecationWarning)
|
||||
|
||||
# Monkeypatch Oauth2 toolkit settings class to check for settings
|
||||
# in django.conf settings each time, not just once during import
|
||||
import oauth2_provider.settings
|
||||
|
||||
oauth2_provider.settings.OAuth2ProviderSettings.__getattribute__ = oauth2_getattribute
|
||||
|
||||
|
||||
def manage():
|
||||
# Prepare the AWX environment.
|
||||
|
||||
@@ -11,9 +11,6 @@ from django.utils.encoding import smart_str
|
||||
# Django REST Framework
|
||||
from rest_framework import authentication
|
||||
|
||||
# Django-OAuth-Toolkit
|
||||
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
|
||||
|
||||
logger = logging.getLogger('awx.api.authentication')
|
||||
|
||||
|
||||
@@ -36,16 +33,3 @@ class LoggedBasicAuthentication(authentication.BasicAuthentication):
|
||||
class SessionAuthentication(authentication.SessionAuthentication):
|
||||
def authenticate_header(self, request):
|
||||
return 'Session'
|
||||
|
||||
|
||||
class LoggedOAuth2Authentication(OAuth2Authentication):
|
||||
def authenticate(self, request):
|
||||
ret = super(LoggedOAuth2Authentication, self).authenticate(request)
|
||||
if ret:
|
||||
user, token = ret
|
||||
username = user.username if user else '<none>'
|
||||
logger.info(
|
||||
smart_str(u"User {} performed a {} to {} through the API using OAuth 2 token {}.".format(username, request.method, request.path, token.pk))
|
||||
)
|
||||
setattr(user, 'oauth_scopes', [x for x in token.scope.split() if x])
|
||||
return ret
|
||||
|
||||
@@ -6,8 +6,6 @@ from rest_framework import serializers
|
||||
|
||||
# AWX
|
||||
from awx.conf import fields, register, register_validate
|
||||
from awx.api.fields import OAuth2ProviderField
|
||||
from oauth2_provider.settings import oauth2_settings
|
||||
|
||||
|
||||
register(
|
||||
@@ -46,27 +44,6 @@ register(
|
||||
category=_('Authentication'),
|
||||
category_slug='authentication',
|
||||
)
|
||||
register(
|
||||
'OAUTH2_PROVIDER',
|
||||
field_class=OAuth2ProviderField,
|
||||
default={
|
||||
'ACCESS_TOKEN_EXPIRE_SECONDS': oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS,
|
||||
'AUTHORIZATION_CODE_EXPIRE_SECONDS': oauth2_settings.AUTHORIZATION_CODE_EXPIRE_SECONDS,
|
||||
'REFRESH_TOKEN_EXPIRE_SECONDS': oauth2_settings.REFRESH_TOKEN_EXPIRE_SECONDS,
|
||||
},
|
||||
label=_('OAuth 2 Timeout Settings'),
|
||||
help_text=_(
|
||||
'Dictionary for customizing OAuth 2 timeouts, available items are '
|
||||
'`ACCESS_TOKEN_EXPIRE_SECONDS`, the duration of access tokens in the number '
|
||||
'of seconds, `AUTHORIZATION_CODE_EXPIRE_SECONDS`, the duration of '
|
||||
'authorization codes in the number of seconds, and `REFRESH_TOKEN_EXPIRE_SECONDS`, '
|
||||
'the duration of refresh tokens, after expired access tokens, '
|
||||
'in the number of seconds.'
|
||||
),
|
||||
category=_('Authentication'),
|
||||
category_slug='authentication',
|
||||
unit=_('seconds'),
|
||||
)
|
||||
register(
|
||||
'LOGIN_REDIRECT_OVERRIDE',
|
||||
field_class=fields.CharField,
|
||||
|
||||
@@ -9,7 +9,6 @@ from django.core.exceptions import ObjectDoesNotExist
|
||||
from rest_framework import serializers
|
||||
|
||||
# AWX
|
||||
from awx.conf import fields
|
||||
from awx.main.models import Credential
|
||||
|
||||
__all__ = ['BooleanNullField', 'CharNullField', 'ChoiceNullField', 'VerbatimField']
|
||||
@@ -79,19 +78,6 @@ class VerbatimField(serializers.Field):
|
||||
return value
|
||||
|
||||
|
||||
class OAuth2ProviderField(fields.DictField):
|
||||
default_error_messages = {'invalid_key_names': _('Invalid key names: {invalid_key_names}')}
|
||||
valid_key_names = {'ACCESS_TOKEN_EXPIRE_SECONDS', 'AUTHORIZATION_CODE_EXPIRE_SECONDS', 'REFRESH_TOKEN_EXPIRE_SECONDS'}
|
||||
child = fields.IntegerField(min_value=1)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
data = super(OAuth2ProviderField, self).to_internal_value(data)
|
||||
invalid_flags = set(data.keys()) - self.valid_key_names
|
||||
if invalid_flags:
|
||||
self.fail('invalid_key_names', invalid_key_names=', '.join(list(invalid_flags)))
|
||||
return data
|
||||
|
||||
|
||||
class DeprecatedCredentialField(serializers.IntegerField):
|
||||
def __init__(self, **kwargs):
|
||||
kwargs['allow_null'] = True
|
||||
|
||||
@@ -374,12 +374,6 @@ class APIView(views.APIView):
|
||||
kwargs.pop('version')
|
||||
return super(APIView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def check_permissions(self, request):
|
||||
if request.method not in ('GET', 'OPTIONS', 'HEAD'):
|
||||
if 'write' not in getattr(request.user, 'oauth_scopes', ['write']):
|
||||
raise PermissionDenied()
|
||||
return super(APIView, self).check_permissions(request)
|
||||
|
||||
|
||||
class GenericAPIView(generics.GenericAPIView, APIView):
|
||||
# Base class for all model-based views.
|
||||
|
||||
@@ -10,10 +10,6 @@ from collections import Counter, OrderedDict
|
||||
from datetime import timedelta
|
||||
from uuid import uuid4
|
||||
|
||||
# OAuth2
|
||||
from oauthlib import oauth2
|
||||
from oauthlib.common import generate_token
|
||||
|
||||
# Jinja
|
||||
from jinja2 import sandbox, StrictUndefined
|
||||
from jinja2.exceptions import TemplateSyntaxError, UndefinedError, SecurityError
|
||||
@@ -50,7 +46,7 @@ from ansible_base.rbac import permission_registry
|
||||
|
||||
# AWX
|
||||
from awx.main.access import get_user_capabilities
|
||||
from awx.main.constants import ACTIVE_STATES, CENSOR_VALUE, org_role_to_permission
|
||||
from awx.main.constants import ACTIVE_STATES, org_role_to_permission
|
||||
from awx.main.models import (
|
||||
ActivityStream,
|
||||
AdHocCommand,
|
||||
@@ -79,14 +75,11 @@ from awx.main.models import (
|
||||
Label,
|
||||
Notification,
|
||||
NotificationTemplate,
|
||||
OAuth2AccessToken,
|
||||
OAuth2Application,
|
||||
Organization,
|
||||
Project,
|
||||
ProjectUpdate,
|
||||
ProjectUpdateEvent,
|
||||
ReceptorAddress,
|
||||
RefreshToken,
|
||||
Role,
|
||||
Schedule,
|
||||
SystemJob,
|
||||
@@ -1060,9 +1053,6 @@ class UserSerializer(BaseSerializer):
|
||||
roles=self.reverse('api:user_roles_list', kwargs={'pk': obj.pk}),
|
||||
activity_stream=self.reverse('api:user_activity_stream_list', kwargs={'pk': obj.pk}),
|
||||
access_list=self.reverse('api:user_access_list', kwargs={'pk': obj.pk}),
|
||||
tokens=self.reverse('api:o_auth2_token_list', kwargs={'pk': obj.pk}),
|
||||
authorized_tokens=self.reverse('api:user_authorized_token_list', kwargs={'pk': obj.pk}),
|
||||
personal_tokens=self.reverse('api:user_personal_token_list', kwargs={'pk': obj.pk}),
|
||||
)
|
||||
)
|
||||
return res
|
||||
@@ -1079,199 +1069,6 @@ class UserActivityStreamSerializer(UserSerializer):
|
||||
fields = ('*', '-is_system_auditor')
|
||||
|
||||
|
||||
class BaseOAuth2TokenSerializer(BaseSerializer):
|
||||
refresh_token = serializers.SerializerMethodField()
|
||||
token = serializers.SerializerMethodField()
|
||||
ALLOWED_SCOPES = ['read', 'write']
|
||||
|
||||
class Meta:
|
||||
model = OAuth2AccessToken
|
||||
fields = ('*', '-name', 'description', 'user', 'token', 'refresh_token', 'application', 'expires', 'scope')
|
||||
read_only_fields = ('user', 'token', 'expires', 'refresh_token')
|
||||
extra_kwargs = {'scope': {'allow_null': False, 'required': False}, 'user': {'allow_null': False, 'required': True}}
|
||||
|
||||
def get_token(self, obj):
|
||||
request = self.context.get('request', None)
|
||||
try:
|
||||
if request.method == 'POST':
|
||||
return obj.token
|
||||
else:
|
||||
return CENSOR_VALUE
|
||||
except ObjectDoesNotExist:
|
||||
return ''
|
||||
|
||||
def get_refresh_token(self, obj):
|
||||
request = self.context.get('request', None)
|
||||
try:
|
||||
if not obj.refresh_token:
|
||||
return None
|
||||
elif request.method == 'POST':
|
||||
return getattr(obj.refresh_token, 'token', '')
|
||||
else:
|
||||
return CENSOR_VALUE
|
||||
except ObjectDoesNotExist:
|
||||
return None
|
||||
|
||||
def get_related(self, obj):
|
||||
ret = super(BaseOAuth2TokenSerializer, self).get_related(obj)
|
||||
if obj.user:
|
||||
ret['user'] = self.reverse('api:user_detail', kwargs={'pk': obj.user.pk})
|
||||
if obj.application:
|
||||
ret['application'] = self.reverse('api:o_auth2_application_detail', kwargs={'pk': obj.application.pk})
|
||||
ret['activity_stream'] = self.reverse('api:o_auth2_token_activity_stream_list', kwargs={'pk': obj.pk})
|
||||
return ret
|
||||
|
||||
def _is_valid_scope(self, value):
|
||||
if not value or (not isinstance(value, str)):
|
||||
return False
|
||||
words = value.split()
|
||||
for word in words:
|
||||
if words.count(word) > 1:
|
||||
return False # do not allow duplicates
|
||||
if word not in self.ALLOWED_SCOPES:
|
||||
return False
|
||||
return True
|
||||
|
||||
def validate_scope(self, value):
|
||||
if not self._is_valid_scope(value):
|
||||
raise serializers.ValidationError(_('Must be a simple space-separated string with allowed scopes {}.').format(self.ALLOWED_SCOPES))
|
||||
return value
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['user'] = self.context['request'].user
|
||||
try:
|
||||
return super(BaseOAuth2TokenSerializer, self).create(validated_data)
|
||||
except oauth2.AccessDeniedError as e:
|
||||
raise PermissionDenied(str(e))
|
||||
|
||||
|
||||
class UserAuthorizedTokenSerializer(BaseOAuth2TokenSerializer):
|
||||
class Meta:
|
||||
extra_kwargs = {
|
||||
'scope': {'allow_null': False, 'required': False},
|
||||
'user': {'allow_null': False, 'required': True},
|
||||
'application': {'allow_null': False, 'required': True},
|
||||
}
|
||||
|
||||
def create(self, validated_data):
|
||||
current_user = self.context['request'].user
|
||||
validated_data['token'] = generate_token()
|
||||
validated_data['expires'] = now() + timedelta(seconds=settings.OAUTH2_PROVIDER['ACCESS_TOKEN_EXPIRE_SECONDS'])
|
||||
obj = super(UserAuthorizedTokenSerializer, self).create(validated_data)
|
||||
obj.save()
|
||||
if obj.application:
|
||||
RefreshToken.objects.create(user=current_user, token=generate_token(), application=obj.application, access_token=obj)
|
||||
return obj
|
||||
|
||||
|
||||
class OAuth2TokenSerializer(BaseOAuth2TokenSerializer):
|
||||
def create(self, validated_data):
|
||||
current_user = self.context['request'].user
|
||||
validated_data['token'] = generate_token()
|
||||
validated_data['expires'] = now() + timedelta(seconds=settings.OAUTH2_PROVIDER['ACCESS_TOKEN_EXPIRE_SECONDS'])
|
||||
obj = super(OAuth2TokenSerializer, self).create(validated_data)
|
||||
if obj.application and obj.application.user:
|
||||
obj.user = obj.application.user
|
||||
obj.save()
|
||||
if obj.application:
|
||||
RefreshToken.objects.create(user=current_user, token=generate_token(), application=obj.application, access_token=obj)
|
||||
return obj
|
||||
|
||||
|
||||
class OAuth2TokenDetailSerializer(OAuth2TokenSerializer):
|
||||
class Meta:
|
||||
read_only_fields = ('*', 'user', 'application')
|
||||
|
||||
|
||||
class UserPersonalTokenSerializer(BaseOAuth2TokenSerializer):
|
||||
class Meta:
|
||||
read_only_fields = ('user', 'token', 'expires', 'application')
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['token'] = generate_token()
|
||||
validated_data['expires'] = now() + timedelta(seconds=settings.OAUTH2_PROVIDER['ACCESS_TOKEN_EXPIRE_SECONDS'])
|
||||
validated_data['application'] = None
|
||||
obj = super(UserPersonalTokenSerializer, self).create(validated_data)
|
||||
obj.save()
|
||||
return obj
|
||||
|
||||
|
||||
class OAuth2ApplicationSerializer(BaseSerializer):
|
||||
show_capabilities = ['edit', 'delete']
|
||||
|
||||
class Meta:
|
||||
model = OAuth2Application
|
||||
fields = (
|
||||
'*',
|
||||
'description',
|
||||
'-user',
|
||||
'client_id',
|
||||
'client_secret',
|
||||
'client_type',
|
||||
'redirect_uris',
|
||||
'authorization_grant_type',
|
||||
'skip_authorization',
|
||||
'organization',
|
||||
)
|
||||
read_only_fields = ('client_id', 'client_secret')
|
||||
read_only_on_update_fields = ('user', 'authorization_grant_type')
|
||||
extra_kwargs = {
|
||||
'user': {'allow_null': True, 'required': False},
|
||||
'organization': {'allow_null': False},
|
||||
'authorization_grant_type': {'allow_null': False, 'label': _('Authorization Grant Type')},
|
||||
'client_secret': {'label': _('Client Secret')},
|
||||
'client_type': {'label': _('Client Type')},
|
||||
'redirect_uris': {'label': _('Redirect URIs')},
|
||||
'skip_authorization': {'label': _('Skip Authorization')},
|
||||
}
|
||||
|
||||
def to_representation(self, obj):
|
||||
ret = super(OAuth2ApplicationSerializer, self).to_representation(obj)
|
||||
request = self.context.get('request', None)
|
||||
if request.method != 'POST' and obj.client_type == 'confidential':
|
||||
ret['client_secret'] = CENSOR_VALUE
|
||||
if obj.client_type == 'public':
|
||||
ret.pop('client_secret', None)
|
||||
return ret
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(OAuth2ApplicationSerializer, self).get_related(obj)
|
||||
res.update(
|
||||
dict(
|
||||
tokens=self.reverse('api:o_auth2_application_token_list', kwargs={'pk': obj.pk}),
|
||||
activity_stream=self.reverse('api:o_auth2_application_activity_stream_list', kwargs={'pk': obj.pk}),
|
||||
)
|
||||
)
|
||||
if obj.organization_id:
|
||||
res.update(
|
||||
dict(
|
||||
organization=self.reverse('api:organization_detail', kwargs={'pk': obj.organization_id}),
|
||||
)
|
||||
)
|
||||
return res
|
||||
|
||||
def get_modified(self, obj):
|
||||
if obj is None:
|
||||
return None
|
||||
return obj.updated
|
||||
|
||||
def _summary_field_tokens(self, obj):
|
||||
token_list = [{'id': x.pk, 'token': CENSOR_VALUE, 'scope': x.scope} for x in obj.oauth2accesstoken_set.all()[:10]]
|
||||
if has_model_field_prefetched(obj, 'oauth2accesstoken_set'):
|
||||
token_count = len(obj.oauth2accesstoken_set.all())
|
||||
else:
|
||||
if len(token_list) < 10:
|
||||
token_count = len(token_list)
|
||||
else:
|
||||
token_count = obj.oauth2accesstoken_set.count()
|
||||
return {'count': token_count, 'results': token_list}
|
||||
|
||||
def get_summary_fields(self, obj):
|
||||
ret = super(OAuth2ApplicationSerializer, self).get_summary_fields(obj)
|
||||
ret['tokens'] = self._summary_field_tokens(obj)
|
||||
return ret
|
||||
|
||||
|
||||
class OrganizationSerializer(BaseSerializer):
|
||||
show_capabilities = ['edit', 'delete']
|
||||
|
||||
@@ -1292,7 +1089,6 @@ class OrganizationSerializer(BaseSerializer):
|
||||
admins=self.reverse('api:organization_admins_list', kwargs={'pk': obj.pk}),
|
||||
teams=self.reverse('api:organization_teams_list', kwargs={'pk': obj.pk}),
|
||||
credentials=self.reverse('api:organization_credential_list', kwargs={'pk': obj.pk}),
|
||||
applications=self.reverse('api:organization_applications_list', kwargs={'pk': obj.pk}),
|
||||
activity_stream=self.reverse('api:organization_activity_stream_list', kwargs={'pk': obj.pk}),
|
||||
notification_templates=self.reverse('api:organization_notification_templates_list', kwargs={'pk': obj.pk}),
|
||||
notification_templates_started=self.reverse('api:organization_notification_templates_started_list', kwargs={'pk': obj.pk}),
|
||||
@@ -6084,8 +5880,6 @@ class ActivityStreamSerializer(BaseSerializer):
|
||||
('workflow_job_template_node', ('id', 'unified_job_template_id')),
|
||||
('label', ('id', 'name', 'organization_id')),
|
||||
('notification', ('id', 'status', 'notification_type', 'notification_template_id')),
|
||||
('o_auth2_access_token', ('id', 'user_id', 'description', 'application_id', 'scope')),
|
||||
('o_auth2_application', ('id', 'name', 'description')),
|
||||
('credential_type', ('id', 'name', 'description', 'kind', 'managed')),
|
||||
('ad_hoc_command', ('id', 'name', 'status', 'limit')),
|
||||
('workflow_approval', ('id', 'name', 'unified_job_id')),
|
||||
|
||||
@@ -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=<username>&password=<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
|
||||
`<client_id>:<client_secret>`, 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 `<client_id>:<client_secret>`, 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.
|
||||
|
||||
|
||||
@@ -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<pk>[0-9]+)/$', OAuth2ApplicationDetail.as_view(), name='o_auth2_application_detail'),
|
||||
re_path(r'^applications/(?P<pk>[0-9]+)/tokens/$', ApplicationOAuth2TokenList.as_view(), name='o_auth2_application_token_list'),
|
||||
re_path(r'^applications/(?P<pk>[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<pk>[0-9]+)/$', OAuth2TokenDetail.as_view(), name='o_auth2_token_detail'),
|
||||
re_path(r'^tokens/(?P<pk>[0-9]+)/activity_stream/$', OAuth2TokenActivityStreamList.as_view(), name='o_auth2_token_activity_stream_list'),
|
||||
]
|
||||
|
||||
__all__ = ['urls']
|
||||
@@ -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']
|
||||
@@ -25,7 +25,7 @@ from awx.api.views.organization import (
|
||||
OrganizationObjectRolesList,
|
||||
OrganizationAccessList,
|
||||
)
|
||||
from awx.api.views import OrganizationCredentialList, OrganizationApplicationList
|
||||
from awx.api.views import OrganizationCredentialList
|
||||
|
||||
|
||||
urls = [
|
||||
@@ -66,7 +66,6 @@ urls = [
|
||||
re_path(r'^(?P<pk>[0-9]+)/galaxy_credentials/$', OrganizationGalaxyCredentialsList.as_view(), name='organization_galaxy_credentials_list'),
|
||||
re_path(r'^(?P<pk>[0-9]+)/object_roles/$', OrganizationObjectRolesList.as_view(), name='organization_object_roles_list'),
|
||||
re_path(r'^(?P<pk>[0-9]+)/access_list/$', OrganizationAccessList.as_view(), name='organization_access_list'),
|
||||
re_path(r'^(?P<pk>[0-9]+)/applications/$', OrganizationApplicationList.as_view(), name='organization_applications_list'),
|
||||
]
|
||||
|
||||
__all__ = ['urls']
|
||||
|
||||
@@ -25,10 +25,6 @@ from awx.api.views import (
|
||||
JobTemplateCredentialsList,
|
||||
SchedulePreview,
|
||||
ScheduleZoneInfo,
|
||||
OAuth2ApplicationList,
|
||||
OAuth2TokenList,
|
||||
ApplicationOAuth2TokenList,
|
||||
OAuth2ApplicationDetail,
|
||||
HostMetricSummaryMonthlyList,
|
||||
)
|
||||
|
||||
@@ -79,8 +75,6 @@ from .schedule import urls as schedule_urls
|
||||
from .activity_stream import urls as activity_stream_urls
|
||||
from .instance import urls as instance_urls
|
||||
from .instance_group import urls as instance_group_urls
|
||||
from .oauth2 import urls as oauth2_urls
|
||||
from .oauth2_root import urls as oauth2_root_urls
|
||||
from .workflow_approval_template import urls as workflow_approval_template_urls
|
||||
from .workflow_approval import urls as workflow_approval_urls
|
||||
from .analytics import urls as analytics_urls
|
||||
@@ -95,11 +89,6 @@ v2_urls = [
|
||||
re_path(r'^job_templates/(?P<pk>[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<pk>[0-9]+)/$', OAuth2ApplicationDetail.as_view(), name='o_auth2_application_detail'),
|
||||
re_path(r'^applications/(?P<pk>[0-9]+)/tokens/$', ApplicationOAuth2TokenList.as_view(), name='application_o_auth2_token_list'),
|
||||
re_path(r'^tokens/$', OAuth2TokenList.as_view(), name='o_auth2_token_list'),
|
||||
re_path(r'^', include(oauth2_urls)),
|
||||
re_path(r'^metrics/$', MetricsView.as_view(), name='metrics_view'),
|
||||
re_path(r'^ping/$', ApiV2PingView.as_view(), name='api_v2_ping_view'),
|
||||
re_path(r'^config/$', ApiV2ConfigView.as_view(), name='api_v2_config_view'),
|
||||
@@ -164,7 +153,6 @@ urlpatterns = [
|
||||
re_path(r'^(?P<version>(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
|
||||
|
||||
@@ -14,10 +14,6 @@ from awx.api.views import (
|
||||
UserRolesList,
|
||||
UserActivityStreamList,
|
||||
UserAccessList,
|
||||
OAuth2ApplicationList,
|
||||
OAuth2UserTokenList,
|
||||
UserPersonalTokenList,
|
||||
UserAuthorizedTokenList,
|
||||
)
|
||||
|
||||
urls = [
|
||||
@@ -31,10 +27,6 @@ urls = [
|
||||
re_path(r'^(?P<pk>[0-9]+)/roles/$', UserRolesList.as_view(), name='user_roles_list'),
|
||||
re_path(r'^(?P<pk>[0-9]+)/activity_stream/$', UserActivityStreamList.as_view(), name='user_activity_stream_list'),
|
||||
re_path(r'^(?P<pk>[0-9]+)/access_list/$', UserAccessList.as_view(), name='user_access_list'),
|
||||
re_path(r'^(?P<pk>[0-9]+)/applications/$', OAuth2ApplicationList.as_view(), name='o_auth2_application_list'),
|
||||
re_path(r'^(?P<pk>[0-9]+)/tokens/$', OAuth2UserTokenList.as_view(), name='o_auth2_token_list'),
|
||||
re_path(r'^(?P<pk>[0-9]+)/authorized_tokens/$', UserAuthorizedTokenList.as_view(), name='user_authorized_token_list'),
|
||||
re_path(r'^(?P<pk>[0-9]+)/personal_tokens/$', UserPersonalTokenList.as_view(), name='user_personal_token_list'),
|
||||
]
|
||||
|
||||
__all__ = ['urls']
|
||||
|
||||
@@ -50,9 +50,6 @@ from rest_framework_yaml.renderers import YAMLRenderer
|
||||
# ansi2html
|
||||
from ansi2html import Ansi2HTMLConverter
|
||||
|
||||
# Django OAuth Toolkit
|
||||
from oauth2_provider.models import get_access_token_model
|
||||
|
||||
import pytz
|
||||
from wsgiref.util import FileWrapper
|
||||
|
||||
@@ -1148,121 +1145,6 @@ class UserMeList(ListAPIView):
|
||||
return self.model.objects.filter(pk=self.request.user.pk)
|
||||
|
||||
|
||||
class OAuth2ApplicationList(ListCreateAPIView):
|
||||
name = _("OAuth 2 Applications")
|
||||
|
||||
model = models.OAuth2Application
|
||||
serializer_class = serializers.OAuth2ApplicationSerializer
|
||||
swagger_topic = 'Authentication'
|
||||
|
||||
|
||||
class OAuth2ApplicationDetail(RetrieveUpdateDestroyAPIView):
|
||||
name = _("OAuth 2 Application Detail")
|
||||
|
||||
model = models.OAuth2Application
|
||||
serializer_class = serializers.OAuth2ApplicationSerializer
|
||||
swagger_topic = 'Authentication'
|
||||
|
||||
def update_raw_data(self, data):
|
||||
data.pop('client_secret', None)
|
||||
return super(OAuth2ApplicationDetail, self).update_raw_data(data)
|
||||
|
||||
|
||||
class ApplicationOAuth2TokenList(SubListCreateAPIView):
|
||||
name = _("OAuth 2 Application Tokens")
|
||||
|
||||
model = models.OAuth2AccessToken
|
||||
serializer_class = serializers.OAuth2TokenSerializer
|
||||
parent_model = models.OAuth2Application
|
||||
relationship = 'oauth2accesstoken_set'
|
||||
parent_key = 'application'
|
||||
swagger_topic = 'Authentication'
|
||||
|
||||
|
||||
class OAuth2ApplicationActivityStreamList(SubListAPIView):
|
||||
model = models.ActivityStream
|
||||
serializer_class = serializers.ActivityStreamSerializer
|
||||
parent_model = models.OAuth2Application
|
||||
relationship = 'activitystream_set'
|
||||
swagger_topic = 'Authentication'
|
||||
search_fields = ('changes',)
|
||||
|
||||
|
||||
class OAuth2TokenList(ListCreateAPIView):
|
||||
name = _("OAuth2 Tokens")
|
||||
|
||||
model = models.OAuth2AccessToken
|
||||
serializer_class = serializers.OAuth2TokenSerializer
|
||||
swagger_topic = 'Authentication'
|
||||
|
||||
|
||||
class OAuth2UserTokenList(SubListCreateAPIView):
|
||||
name = _("OAuth2 User Tokens")
|
||||
|
||||
model = models.OAuth2AccessToken
|
||||
serializer_class = serializers.OAuth2TokenSerializer
|
||||
parent_model = models.User
|
||||
relationship = 'main_oauth2accesstoken'
|
||||
parent_key = 'user'
|
||||
swagger_topic = 'Authentication'
|
||||
|
||||
|
||||
class UserAuthorizedTokenList(SubListCreateAPIView):
|
||||
name = _("OAuth2 User Authorized Access Tokens")
|
||||
|
||||
model = models.OAuth2AccessToken
|
||||
serializer_class = serializers.UserAuthorizedTokenSerializer
|
||||
parent_model = models.User
|
||||
relationship = 'oauth2accesstoken_set'
|
||||
parent_key = 'user'
|
||||
swagger_topic = 'Authentication'
|
||||
|
||||
def get_queryset(self):
|
||||
return get_access_token_model().objects.filter(application__isnull=False, user=self.request.user)
|
||||
|
||||
|
||||
class OrganizationApplicationList(SubListCreateAPIView):
|
||||
name = _("Organization OAuth2 Applications")
|
||||
|
||||
model = models.OAuth2Application
|
||||
serializer_class = serializers.OAuth2ApplicationSerializer
|
||||
parent_model = models.Organization
|
||||
relationship = 'applications'
|
||||
parent_key = 'organization'
|
||||
swagger_topic = 'Authentication'
|
||||
|
||||
|
||||
class UserPersonalTokenList(SubListCreateAPIView):
|
||||
name = _("OAuth2 Personal Access Tokens")
|
||||
|
||||
model = models.OAuth2AccessToken
|
||||
serializer_class = serializers.UserPersonalTokenSerializer
|
||||
parent_model = models.User
|
||||
relationship = 'main_oauth2accesstoken'
|
||||
parent_key = 'user'
|
||||
swagger_topic = 'Authentication'
|
||||
|
||||
def get_queryset(self):
|
||||
return get_access_token_model().objects.filter(application__isnull=True, user=self.request.user)
|
||||
|
||||
|
||||
class OAuth2TokenDetail(RetrieveUpdateDestroyAPIView):
|
||||
name = _("OAuth Token Detail")
|
||||
|
||||
model = models.OAuth2AccessToken
|
||||
serializer_class = serializers.OAuth2TokenDetailSerializer
|
||||
swagger_topic = 'Authentication'
|
||||
|
||||
|
||||
class OAuth2TokenActivityStreamList(SubListAPIView):
|
||||
model = models.ActivityStream
|
||||
serializer_class = serializers.ActivityStreamSerializer
|
||||
parent_model = models.OAuth2AccessToken
|
||||
relationship = 'activitystream_set'
|
||||
swagger_topic = 'Authentication'
|
||||
search_fields = ('changes',)
|
||||
|
||||
|
||||
class UserTeamsList(SubListAPIView):
|
||||
model = models.Team
|
||||
serializer_class = serializers.TeamSerializer
|
||||
|
||||
@@ -28,7 +28,7 @@ from awx.main.analytics import all_collectors
|
||||
from awx.main.ha import is_ha_environment
|
||||
from awx.main.utils import get_awx_version, get_custom_venv_choices
|
||||
from awx.main.utils.licensing import validate_entitlement_manifest
|
||||
from awx.api.versioning import URLPathVersioning, is_optional_api_urlpattern_prefix_request, reverse, drf_reverse
|
||||
from awx.api.versioning import URLPathVersioning, reverse, drf_reverse
|
||||
from awx.main.constants import PRIVILEGE_ESCALATION_METHODS
|
||||
from awx.main.models import Project, Organization, Instance, InstanceGroup, JobTemplate
|
||||
from awx.main.utils import set_environ
|
||||
@@ -51,8 +51,6 @@ class ApiRootView(APIView):
|
||||
data['description'] = _('AWX REST API')
|
||||
data['current_version'] = v2
|
||||
data['available_versions'] = dict(v2=v2)
|
||||
if not is_optional_api_urlpattern_prefix_request(request):
|
||||
data['oauth2'] = drf_reverse('api:oauth_authorization_root_view')
|
||||
data['custom_logo'] = settings.CUSTOM_LOGO
|
||||
data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO
|
||||
data['login_redirect_override'] = settings.LOGIN_REDIRECT_OVERRIDE
|
||||
@@ -61,20 +59,6 @@ class ApiRootView(APIView):
|
||||
return Response(data)
|
||||
|
||||
|
||||
class ApiOAuthAuthorizationRootView(APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
name = _("API OAuth 2 Authorization Root")
|
||||
versioning_class = None
|
||||
swagger_topic = 'Authentication'
|
||||
|
||||
def get(self, request, format=None):
|
||||
data = OrderedDict()
|
||||
data['authorize'] = drf_reverse('api:authorize')
|
||||
data['token'] = drf_reverse('api:token')
|
||||
data['revoke_token'] = drf_reverse('api:revoke-token')
|
||||
return Response(data)
|
||||
|
||||
|
||||
class ApiVersionRootView(APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
swagger_topic = 'Versioning'
|
||||
@@ -99,8 +83,6 @@ class ApiVersionRootView(APIView):
|
||||
data['credentials'] = reverse('api:credential_list', request=request)
|
||||
data['credential_types'] = reverse('api:credential_type_list', request=request)
|
||||
data['credential_input_sources'] = reverse('api:credential_input_source_list', request=request)
|
||||
data['applications'] = reverse('api:o_auth2_application_list', request=request)
|
||||
data['tokens'] = reverse('api:o_auth2_token_list', request=request)
|
||||
data['metrics'] = reverse('api:metrics_view', request=request)
|
||||
data['inventory'] = reverse('api:inventory_list', request=request)
|
||||
data['constructed_inventory'] = reverse('api:constructed_inventory_list', request=request)
|
||||
|
||||
@@ -17,9 +17,6 @@ from django.core.exceptions import ObjectDoesNotExist, FieldDoesNotExist
|
||||
# Django REST Framework
|
||||
from rest_framework.exceptions import ParseError, PermissionDenied
|
||||
|
||||
# Django OAuth Toolkit
|
||||
from awx.main.models.oauth import OAuth2Application, OAuth2AccessToken
|
||||
|
||||
# django-ansible-base
|
||||
from ansible_base.lib.utils.validation import to_python_boolean
|
||||
from ansible_base.rbac.models import RoleEvaluation
|
||||
@@ -441,10 +438,7 @@ class BaseAccess(object):
|
||||
|
||||
# Actions not possible for reason unrelated to RBAC
|
||||
# Cannot copy with validation errors, or update a manual group/project
|
||||
if 'write' not in getattr(self.user, 'oauth_scopes', ['write']):
|
||||
user_capabilities[display_method] = False # Read tokens cannot take any actions
|
||||
continue
|
||||
elif display_method in ['copy', 'start', 'schedule'] and isinstance(obj, JobTemplate):
|
||||
if display_method in ['copy', 'start', 'schedule'] and isinstance(obj, JobTemplate):
|
||||
if obj.validation_errors:
|
||||
user_capabilities[display_method] = False
|
||||
continue
|
||||
@@ -753,82 +747,6 @@ class UserAccess(BaseAccess):
|
||||
return False
|
||||
|
||||
|
||||
class OAuth2ApplicationAccess(BaseAccess):
|
||||
"""
|
||||
I can read, change or delete OAuth 2 applications when:
|
||||
- I am a superuser.
|
||||
- I am the admin of the organization of the user of the application.
|
||||
- I am a user in the organization of the application.
|
||||
I can create OAuth 2 applications when:
|
||||
- I am a superuser.
|
||||
- I am the admin of the organization of the application.
|
||||
"""
|
||||
|
||||
model = OAuth2Application
|
||||
select_related = ('user',)
|
||||
prefetch_related = ('organization', 'oauth2accesstoken_set')
|
||||
|
||||
def filtered_queryset(self):
|
||||
org_access_qs = Organization.access_qs(self.user, 'member')
|
||||
return self.model.objects.filter(organization__in=org_access_qs)
|
||||
|
||||
def can_change(self, obj, data):
|
||||
return self.user.is_superuser or self.check_related('organization', Organization, data, obj=obj, role_field='admin_role', mandatory=True)
|
||||
|
||||
def can_delete(self, obj):
|
||||
return self.user.is_superuser or obj.organization in self.user.admin_of_organizations
|
||||
|
||||
def can_add(self, data):
|
||||
if self.user.is_superuser:
|
||||
return True
|
||||
if not data:
|
||||
return Organization.access_qs(self.user, 'change').exists()
|
||||
return self.check_related('organization', Organization, data, role_field='admin_role', mandatory=True)
|
||||
|
||||
|
||||
class OAuth2TokenAccess(BaseAccess):
|
||||
"""
|
||||
I can read, change or delete an app token when:
|
||||
- I am a superuser.
|
||||
- I am the admin of the organization of the application of the token.
|
||||
- I am the user of the token.
|
||||
I can create an OAuth2 app token when:
|
||||
- I have the read permission of the related application.
|
||||
I can read, change or delete a personal token when:
|
||||
- I am the user of the token
|
||||
- I am the superuser
|
||||
I can create an OAuth2 Personal Access Token when:
|
||||
- I am a user. But I can only create a PAT for myself.
|
||||
"""
|
||||
|
||||
model = OAuth2AccessToken
|
||||
|
||||
select_related = ('user', 'application')
|
||||
prefetch_related = ('refresh_token',)
|
||||
|
||||
def filtered_queryset(self):
|
||||
org_access_qs = Organization.objects.filter(Q(admin_role__members=self.user) | Q(auditor_role__members=self.user))
|
||||
return self.model.objects.filter(application__organization__in=org_access_qs) | self.model.objects.filter(user__id=self.user.pk)
|
||||
|
||||
def can_delete(self, obj):
|
||||
if (self.user.is_superuser) | (obj.user == self.user):
|
||||
return True
|
||||
elif not obj.application:
|
||||
return False
|
||||
return self.user in obj.application.organization.admin_role
|
||||
|
||||
def can_change(self, obj, data):
|
||||
return self.can_delete(obj)
|
||||
|
||||
def can_add(self, data):
|
||||
if 'application' in data:
|
||||
app = get_object_from_data('application', OAuth2Application, data)
|
||||
if app is None:
|
||||
return True
|
||||
return OAuth2ApplicationAccess(self.user).can_read(app)
|
||||
return True
|
||||
|
||||
|
||||
class OrganizationAccess(NotificationAttachMixin, BaseAccess):
|
||||
"""
|
||||
I can see organizations when:
|
||||
@@ -2741,8 +2659,6 @@ class ActivityStreamAccess(BaseAccess):
|
||||
'credential_type',
|
||||
'team',
|
||||
'ad_hoc_command',
|
||||
'o_auth2_application',
|
||||
'o_auth2_access_token',
|
||||
'notification_template',
|
||||
'notification',
|
||||
'label',
|
||||
@@ -2828,14 +2744,6 @@ class ActivityStreamAccess(BaseAccess):
|
||||
if team_set:
|
||||
q |= Q(team__in=team_set)
|
||||
|
||||
app_set = OAuth2ApplicationAccess(self.user).filtered_queryset()
|
||||
if app_set:
|
||||
q |= Q(o_auth2_application__in=app_set)
|
||||
|
||||
token_set = OAuth2TokenAccess(self.user).filtered_queryset()
|
||||
if token_set:
|
||||
q |= Q(o_auth2_access_token__in=token_set)
|
||||
|
||||
return qs.filter(q).distinct()
|
||||
|
||||
def can_add(self, data):
|
||||
|
||||
@@ -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()))
|
||||
@@ -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)
|
||||
@@ -10,7 +10,7 @@ from django.db.models.signals import post_save
|
||||
from awx.conf import settings_registry
|
||||
from awx.conf.models import Setting
|
||||
from awx.conf.signals import on_post_save_setting
|
||||
from awx.main.models import UnifiedJob, Credential, NotificationTemplate, Job, JobTemplate, WorkflowJob, WorkflowJobTemplate, OAuth2Application
|
||||
from awx.main.models import UnifiedJob, Credential, NotificationTemplate, Job, JobTemplate, WorkflowJob, WorkflowJobTemplate
|
||||
from awx.main.utils.encryption import encrypt_field, decrypt_field, encrypt_value, decrypt_value, get_encryption_key
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ class Command(BaseCommand):
|
||||
self._notification_templates()
|
||||
self._credentials()
|
||||
self._unified_jobs()
|
||||
self._oauth2_app_secrets()
|
||||
self._settings()
|
||||
self._survey_passwords()
|
||||
return self.new_key
|
||||
@@ -74,13 +73,6 @@ class Command(BaseCommand):
|
||||
uj.start_args = encrypt_field(uj, 'start_args', secret_key=self.new_key)
|
||||
uj.save()
|
||||
|
||||
def _oauth2_app_secrets(self):
|
||||
for app in OAuth2Application.objects.iterator():
|
||||
raw = app.client_secret
|
||||
app.client_secret = raw
|
||||
encrypted = encrypt_value(raw, secret_key=self.new_key)
|
||||
OAuth2Application.objects.filter(pk=app.pk).update(client_secret=encrypted)
|
||||
|
||||
def _settings(self):
|
||||
# don't update the cache, the *actual* value isn't changing
|
||||
post_save.disconnect(on_post_save_setting, sender=Setting)
|
||||
|
||||
@@ -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))
|
||||
@@ -5,7 +5,6 @@ from __future__ import unicode_literals
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import oauth2_provider
|
||||
import re
|
||||
|
||||
|
||||
@@ -13,19 +12,13 @@ class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('main', '0024_v330_create_user_session_membership'),
|
||||
]
|
||||
run_before = [
|
||||
# As of this migration, OAuth2Application and OAuth2AccessToken are models in main app
|
||||
# Grant and RefreshToken models are still in the oauth2_provider app and reference
|
||||
# the app and token models, so these must be created before the oauth2_provider models
|
||||
('oauth2_provider', '0001_initial')
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='OAuth2Application',
|
||||
fields=[
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('client_id', models.CharField(db_index=True, default=oauth2_provider.generators.generate_client_id, max_length=100, unique=True)),
|
||||
('client_id', models.CharField(db_index=True, default=lambda: "", max_length=100, unique=True)),
|
||||
(
|
||||
'redirect_uris',
|
||||
models.TextField(blank=True, help_text='Allowed URIs list, space separated'),
|
||||
@@ -43,7 +36,7 @@ class Migration(migrations.Migration):
|
||||
max_length=32,
|
||||
),
|
||||
),
|
||||
('client_secret', models.CharField(blank=True, db_index=True, default=oauth2_provider.generators.generate_client_secret, max_length=255)),
|
||||
('client_secret', models.CharField(blank=True, db_index=True, default=lambda: "", max_length=255)),
|
||||
('name', models.CharField(blank=True, max_length=255)),
|
||||
('skip_authorization', models.BooleanField(default=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
@@ -72,10 +65,6 @@ class Migration(migrations.Migration):
|
||||
('updated', models.DateTimeField(auto_now=True)),
|
||||
('description', models.CharField(blank=True, default='', max_length=200)),
|
||||
('last_used', models.DateTimeField(default=None, editable=False, null=True)),
|
||||
(
|
||||
'application',
|
||||
models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
|
||||
),
|
||||
(
|
||||
'user',
|
||||
models.ForeignKey(
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import unicode_literals
|
||||
|
||||
import awx.main.fields
|
||||
from django.db import migrations
|
||||
import oauth2_provider.generators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -16,8 +15,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='oauth2application',
|
||||
name='client_secret',
|
||||
field=awx.main.fields.OAuth2ClientSecretField(
|
||||
blank=True, db_index=True, default=oauth2_provider.generators.generate_client_secret, max_length=1024
|
||||
),
|
||||
field=awx.main.fields.OAuth2ClientSecretField(blank=True, db_index=True, default=lambda: "", max_length=1024),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,7 +6,6 @@ import awx.main.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import oauth2_provider.generators
|
||||
|
||||
# TODO: Squash all of these migrations with '0024_v330_add_oauth_activity_stream_registrar'
|
||||
|
||||
@@ -54,7 +53,7 @@ class Migration(migrations.Migration):
|
||||
field=awx.main.fields.OAuth2ClientSecretField(
|
||||
blank=True,
|
||||
db_index=True,
|
||||
default=oauth2_provider.generators.generate_client_secret,
|
||||
default=lambda: "",
|
||||
help_text='Used for more stringent verification of access to an application when creating a token.',
|
||||
max_length=1024,
|
||||
),
|
||||
|
||||
@@ -2,27 +2,12 @@
|
||||
# Generated by Django 1.11.11 on 2018-06-14 21:03
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL),
|
||||
('main', '0040_v330_unifiedjob_controller_node'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='oauth2accesstoken',
|
||||
name='source_refresh_token',
|
||||
field=models.OneToOneField(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='refreshed_access_token',
|
||||
to=settings.OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
||||
operations = []
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
# Generated by Django 3.2.16 on 2023-04-21 14:15
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_ID_TOKEN_MODEL),
|
||||
('main', '0182_constructed_inventory'),
|
||||
('oauth2_provider', '0005_auto_20211222_2352'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='oauth2accesstoken',
|
||||
name='id_token',
|
||||
field=models.OneToOneField(
|
||||
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='access_token', to=settings.OAUTH2_PROVIDER_ID_TOKEN_MODEL
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='oauth2application',
|
||||
name='algorithm',
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Generated by Django 4.2.10 on 2024-10-24 14:06
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0198_alter_inventorysource_source_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='oauth2application',
|
||||
unique_together=None,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='oauth2application',
|
||||
name='organization',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='oauth2application',
|
||||
name='user',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='activitystream',
|
||||
name='o_auth2_access_token',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='activitystream',
|
||||
name='o_auth2_application',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='OAuth2AccessToken',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='OAuth2Application',
|
||||
),
|
||||
]
|
||||
@@ -93,9 +93,6 @@ from awx.main.models.workflow import ( # noqa
|
||||
WorkflowApproval,
|
||||
WorkflowApprovalTemplate,
|
||||
)
|
||||
from awx.api.versioning import reverse
|
||||
from awx.main.models.oauth import OAuth2AccessToken, OAuth2Application # noqa
|
||||
from oauth2_provider.models import Grant, RefreshToken # noqa -- needed django-oauth-toolkit model migrations
|
||||
|
||||
|
||||
# Add custom methods to User model for permissions checks.
|
||||
@@ -244,19 +241,6 @@ def user_is_system_auditor(user, tf):
|
||||
User.add_to_class('is_system_auditor', user_is_system_auditor)
|
||||
|
||||
|
||||
def o_auth2_application_get_absolute_url(self, request=None):
|
||||
return reverse('api:o_auth2_application_detail', kwargs={'pk': self.pk}, request=request)
|
||||
|
||||
|
||||
OAuth2Application.add_to_class('get_absolute_url', o_auth2_application_get_absolute_url)
|
||||
|
||||
|
||||
def o_auth2_token_get_absolute_url(self, request=None):
|
||||
return reverse('api:o_auth2_token_detail', kwargs={'pk': self.pk}, request=request)
|
||||
|
||||
|
||||
OAuth2AccessToken.add_to_class('get_absolute_url', o_auth2_token_get_absolute_url)
|
||||
|
||||
from awx.main.registrar import activity_stream_registrar # noqa
|
||||
|
||||
activity_stream_registrar.connect(Organization)
|
||||
@@ -288,8 +272,6 @@ activity_stream_registrar.connect(WorkflowJobTemplateNode)
|
||||
activity_stream_registrar.connect(WorkflowJob)
|
||||
activity_stream_registrar.connect(WorkflowApproval)
|
||||
activity_stream_registrar.connect(WorkflowApprovalTemplate)
|
||||
activity_stream_registrar.connect(OAuth2Application)
|
||||
activity_stream_registrar.connect(OAuth2AccessToken)
|
||||
|
||||
# Register models
|
||||
permission_registry.register(Project, Team, WorkflowJobTemplate, JobTemplate, Inventory, Organization, Credential, NotificationTemplate, ExecutionEnvironment)
|
||||
@@ -297,8 +279,3 @@ permission_registry.register(InstanceGroup, parent_field_name=None) # Not part
|
||||
|
||||
# prevent API filtering on certain Django-supplied sensitive fields
|
||||
prevent_search(User._meta.get_field('password'))
|
||||
prevent_search(OAuth2AccessToken._meta.get_field('token'))
|
||||
prevent_search(RefreshToken._meta.get_field('token'))
|
||||
prevent_search(OAuth2Application._meta.get_field('client_secret'))
|
||||
prevent_search(OAuth2Application._meta.get_field('client_id'))
|
||||
prevent_search(Grant._meta.get_field('code'))
|
||||
|
||||
@@ -81,8 +81,6 @@ class ActivityStream(models.Model):
|
||||
role = models.ManyToManyField("Role", blank=True)
|
||||
instance = models.ManyToManyField("Instance", blank=True)
|
||||
instance_group = models.ManyToManyField("InstanceGroup", blank=True)
|
||||
o_auth2_application = models.ManyToManyField("OAuth2Application", blank=True)
|
||||
o_auth2_access_token = models.ManyToManyField("OAuth2AccessToken", blank=True)
|
||||
|
||||
setting = models.JSONField(default=dict, blank=True)
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -39,7 +39,6 @@ from awx.main.models import (
|
||||
Job,
|
||||
JobHostSummary,
|
||||
JobTemplate,
|
||||
OAuth2AccessToken,
|
||||
Organization,
|
||||
Project,
|
||||
Role,
|
||||
@@ -54,7 +53,6 @@ from awx.main.models import (
|
||||
WorkflowApprovalTemplate,
|
||||
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
|
||||
)
|
||||
from awx.main.constants import CENSOR_VALUE
|
||||
from awx.main.utils import model_instance_diff, model_to_dict, camelcase_to_underscore, get_current_apps
|
||||
from awx.main.utils import ignore_inventory_computed_fields, ignore_inventory_group_removal, _inventory_updates
|
||||
from awx.main.tasks.system import update_inventory_computed_fields, handle_removed_image
|
||||
@@ -400,8 +398,6 @@ def model_serializer_mapping():
|
||||
models.WorkflowApproval: serializers.WorkflowApprovalActivityStreamSerializer,
|
||||
models.WorkflowApprovalTemplate: serializers.WorkflowApprovalTemplateSerializer,
|
||||
models.WorkflowJob: serializers.WorkflowJobSerializer,
|
||||
models.OAuth2AccessToken: serializers.OAuth2TokenSerializer,
|
||||
models.OAuth2Application: serializers.OAuth2ApplicationSerializer,
|
||||
}
|
||||
|
||||
|
||||
@@ -443,8 +439,6 @@ def activity_stream_create(sender, instance, created, **kwargs):
|
||||
changes['labels'] = [label.name for label in instance.labels.iterator()]
|
||||
if 'extra_vars' in changes:
|
||||
changes['extra_vars'] = instance.display_extra_vars()
|
||||
if type(instance) == OAuth2AccessToken:
|
||||
changes['token'] = CENSOR_VALUE
|
||||
activity_entry = get_activity_stream_class()(operation='create', object1=object1, changes=json.dumps(changes), actor=get_current_user_or_none())
|
||||
# TODO: Weird situation where cascade SETNULL doesn't work
|
||||
# it might actually be a good idea to remove all of these FK references since
|
||||
@@ -506,8 +500,6 @@ def activity_stream_delete(sender, instance, **kwargs):
|
||||
return
|
||||
changes.update(model_to_dict(instance, model_serializer_mapping()))
|
||||
object1 = camelcase_to_underscore(instance.__class__.__name__)
|
||||
if type(instance) == OAuth2AccessToken:
|
||||
changes['token'] = CENSOR_VALUE
|
||||
activity_entry = get_activity_stream_class()(operation='delete', changes=json.dumps(changes), object1=object1, actor=get_current_user_or_none())
|
||||
activity_entry.save()
|
||||
connection.on_commit(lambda: emit_activity_stream_change(activity_entry))
|
||||
@@ -669,13 +661,3 @@ def save_user_session_membership(sender, **kwargs):
|
||||
membership.delete()
|
||||
if len(expired):
|
||||
consumers.emit_channel_notification('control-limit_reached_{}'.format(user_id), dict(group_name='control', reason='limit_reached'))
|
||||
|
||||
|
||||
@receiver(post_save, sender=OAuth2AccessToken)
|
||||
def create_access_token_user_if_missing(sender, **kwargs):
|
||||
obj = kwargs['instance']
|
||||
if obj.application and obj.application.user:
|
||||
obj.user = obj.application.user
|
||||
post_save.disconnect(create_access_token_user_if_missing, sender=OAuth2AccessToken)
|
||||
obj.save()
|
||||
post_save.connect(create_access_token_user_if_missing, sender=OAuth2AccessToken)
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -147,22 +147,6 @@ class TestKeyRegeneration:
|
||||
with override_settings(SECRET_KEY=new_key):
|
||||
assert json.loads(new_job.decrypted_extra_vars())['secret_key'] == 'donttell'
|
||||
|
||||
def test_oauth2_application_client_secret(self, oauth_application):
|
||||
# test basic decryption
|
||||
secret = oauth_application.client_secret
|
||||
assert len(secret) == 128
|
||||
|
||||
# re-key the client_secret
|
||||
new_key = regenerate_secret_key.Command().handle()
|
||||
|
||||
# verify that the old SECRET_KEY doesn't work
|
||||
with pytest.raises(InvalidToken):
|
||||
models.OAuth2Application.objects.get(pk=oauth_application.pk).client_secret
|
||||
|
||||
# verify that the new SECRET_KEY *does* work
|
||||
with override_settings(SECRET_KEY=new_key):
|
||||
assert models.OAuth2Application.objects.get(pk=oauth_application.pk).client_secret == secret
|
||||
|
||||
def test_use_custom_key_with_tower_secret_key_env_var(self):
|
||||
custom_key = 'MXSq9uqcwezBOChl/UfmbW1k4op+bC+FQtwPqgJ1u9XV'
|
||||
os.environ['TOWER_SECRET_KEY'] = custom_key
|
||||
|
||||
@@ -44,7 +44,6 @@ from awx.main.models.events import (
|
||||
)
|
||||
from awx.main.models.workflow import WorkflowJobTemplate
|
||||
from awx.main.models.ad_hoc_commands import AdHocCommand
|
||||
from awx.main.models.oauth import OAuth2Application as Application
|
||||
from awx.main.models.execution_environments import ExecutionEnvironment
|
||||
from awx.main.utils import is_testing
|
||||
|
||||
@@ -812,11 +811,6 @@ def get_db_prep_save(self, value, connection, **kwargs):
|
||||
return value
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def oauth_application(admin):
|
||||
return Application.objects.create(name='test app', user=admin, client_type='confidential', authorization_grant_type='password')
|
||||
|
||||
|
||||
class MockCopy:
|
||||
events = []
|
||||
index = -1
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
@@ -9,7 +9,6 @@ from ansible_base.rest_filters.rest_framework.field_lookup_backend import FieldL
|
||||
|
||||
from awx.main.models import (
|
||||
AdHocCommand,
|
||||
ActivityStream,
|
||||
Job,
|
||||
JobTemplate,
|
||||
SystemJob,
|
||||
@@ -19,7 +18,6 @@ from awx.main.models import (
|
||||
WorkflowJobTemplate,
|
||||
WorkflowJobOptions,
|
||||
)
|
||||
from awx.main.models.oauth import OAuth2Application
|
||||
from awx.main.models.jobs import JobOptions
|
||||
|
||||
|
||||
@@ -28,7 +26,6 @@ from awx.main.models.jobs import JobOptions
|
||||
[
|
||||
(User, 'password__icontains'),
|
||||
(User, 'settings__value__icontains'),
|
||||
(User, 'main_oauth2accesstoken__token__gt'),
|
||||
(UnifiedJob, 'job_args__icontains'),
|
||||
(UnifiedJob, 'job_env__icontains'),
|
||||
(UnifiedJob, 'start_args__icontains'),
|
||||
@@ -40,8 +37,6 @@ from awx.main.models.jobs import JobOptions
|
||||
(WorkflowJob, 'survey_passwords__icontains'),
|
||||
(JobTemplate, 'survey_spec__icontains'),
|
||||
(WorkflowJobTemplate, 'survey_spec__icontains'),
|
||||
(ActivityStream, 'o_auth2_application__client_secret__gt'),
|
||||
(OAuth2Application, 'grant__code__gt'),
|
||||
],
|
||||
)
|
||||
def test_filter_sensitive_fields_and_relations(model, query):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -343,7 +343,6 @@ INSTALLED_APPS = [
|
||||
# According to channels 4.0 docs you install daphne instead of channels now
|
||||
'daphne',
|
||||
'django.contrib.staticfiles',
|
||||
'oauth2_provider',
|
||||
'rest_framework',
|
||||
'django_extensions',
|
||||
'polymorphic',
|
||||
@@ -369,7 +368,6 @@ REST_FRAMEWORK = {
|
||||
'PAGE_SIZE': 25,
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'ansible_base.jwt_consumer.awx.auth.AwxJWTAuthentication',
|
||||
'awx.api.authentication.LoggedOAuth2Authentication',
|
||||
'awx.api.authentication.SessionAuthentication',
|
||||
'awx.api.authentication.LoggedBasicAuthentication',
|
||||
),
|
||||
@@ -389,16 +387,6 @@ REST_FRAMEWORK = {
|
||||
|
||||
AUTHENTICATION_BACKENDS = ('awx.main.backends.AWXModelBackend',)
|
||||
|
||||
|
||||
# Django OAuth Toolkit settings
|
||||
OAUTH2_PROVIDER_APPLICATION_MODEL = 'main.OAuth2Application'
|
||||
OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = 'main.OAuth2AccessToken'
|
||||
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = 'oauth2_provider.RefreshToken'
|
||||
OAUTH2_PROVIDER_ID_TOKEN_MODEL = "oauth2_provider.IDToken"
|
||||
|
||||
OAUTH2_PROVIDER = {'ACCESS_TOKEN_EXPIRE_SECONDS': 31536000000, 'AUTHORIZATION_CODE_EXPIRE_SECONDS': 600, 'REFRESH_TOKEN_EXPIRE_SECONDS': 2628000}
|
||||
|
||||
|
||||
# Enable / Disable HTTP Basic Authentication used in the API browser
|
||||
# Note: Session limits are not enforced when using HTTP Basic Authentication.
|
||||
# Note: This setting may be overridden by database settings.
|
||||
|
||||
Reference in New Issue
Block a user