Admin access for API requests.
ECOM-3943
This commit is contained in:
@@ -320,3 +320,7 @@ SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
|
||||
######### custom courses #########
|
||||
INSTALLED_APPS += ('openedx.core.djangoapps.ccxcon',)
|
||||
FEATURES['CUSTOM_COURSES_EDX'] = True
|
||||
|
||||
# API access management. Necessary so that django-simple-history
|
||||
# doesn't break when running pre-test migrations.
|
||||
INSTALLED_APPS += ('openedx.core.djangoapps.api_admin',)
|
||||
|
||||
@@ -1992,6 +1992,9 @@ INSTALLED_APPS = (
|
||||
|
||||
# Review widgets
|
||||
'openedx.core.djangoapps.coursetalk',
|
||||
|
||||
# API access administration
|
||||
'openedx.core.djangoapps.api_admin',
|
||||
)
|
||||
|
||||
# Migrations which are not in the standard module "migrations"
|
||||
|
||||
0
openedx/core/djangoapps/api_admin/__init__.py
Normal file
0
openedx/core/djangoapps/api_admin/__init__.py
Normal file
12
openedx/core/djangoapps/api_admin/admin.py
Normal file
12
openedx/core/djangoapps/api_admin/admin.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""Admin views for API managment."""
|
||||
from django.contrib import admin
|
||||
|
||||
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest
|
||||
|
||||
|
||||
@admin.register(ApiAccessRequest)
|
||||
class ApiAccessRequestAdmin(admin.ModelAdmin):
|
||||
"""Admin for API access requests."""
|
||||
list_display = ('user', 'status', 'website')
|
||||
list_filter = ('status',)
|
||||
search_fields = ('user__email',)
|
||||
55
openedx/core/djangoapps/api_admin/migrations/0001_initial.py
Normal file
55
openedx/core/djangoapps/api_admin/migrations/0001_initial.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
import django_extensions.db.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ApiAccessRequest',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
|
||||
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
|
||||
('status', models.CharField(default=b'pending', help_text='Status of this API access request', max_length=255, db_index=True, choices=[(b'pending', 'Pending'), (b'denied', 'Denied'), (b'approved', 'Approved')])),
|
||||
('website', models.URLField(help_text='The URL of the website associated with this API user.')),
|
||||
('reason', models.TextField(help_text='The reason this user wants to access the API.')),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-modified', '-created'),
|
||||
'abstract': False,
|
||||
'get_latest_by': 'modified',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='HistoricalApiAccessRequest',
|
||||
fields=[
|
||||
('id', models.IntegerField(verbose_name='ID', db_index=True, auto_created=True, blank=True)),
|
||||
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
|
||||
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
|
||||
('status', models.CharField(default=b'pending', help_text='Status of this API access request', max_length=255, db_index=True, choices=[(b'pending', 'Pending'), (b'denied', 'Denied'), (b'approved', 'Approved')])),
|
||||
('website', models.URLField(help_text='The URL of the website associated with this API user.')),
|
||||
('reason', models.TextField(help_text='The reason this user wants to access the API.')),
|
||||
('history_id', models.AutoField(serialize=False, primary_key=True)),
|
||||
('history_date', models.DateTimeField()),
|
||||
('history_type', models.CharField(max_length=1, choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')])),
|
||||
('history_user', models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('user', models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.DO_NOTHING, db_constraint=False, blank=True, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-history_date', '-history_id'),
|
||||
'get_latest_by': 'history_date',
|
||||
'verbose_name': 'historical api access request',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
API_GROUP_NAME = 'API Access Request Approvers'
|
||||
|
||||
|
||||
def add_api_access_group(apps, schema_editor):
|
||||
Group = apps.get_model('auth', 'Group')
|
||||
Permission = apps.get_model('auth', 'Permission')
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
ApiAccessRequest = apps.get_model('api_admin', 'ApiAccessRequest')
|
||||
|
||||
group, __ = Group.objects.get_or_create(name=API_GROUP_NAME)
|
||||
api_content_type = ContentType.objects.get_for_model(ApiAccessRequest)
|
||||
group.permissions = Permission.objects.filter(content_type=api_content_type)
|
||||
group.save()
|
||||
|
||||
|
||||
def delete_api_access_group(apps, schema_editor):
|
||||
Group = apps.get_model('auth', 'Group')
|
||||
Group.objects.filter(name=API_GROUP_NAME).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api_admin', '0001_initial'),
|
||||
('contenttypes', '0002_remove_content_type_name')
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(add_api_access_group, delete_api_access_group)
|
||||
]
|
||||
63
openedx/core/djangoapps/api_admin/models.py
Normal file
63
openedx/core/djangoapps/api_admin/models.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""Models for API management."""
|
||||
import logging
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext as _
|
||||
from django_extensions.db.models import TimeStampedModel
|
||||
from simple_history.models import HistoricalRecords
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApiAccessRequest(TimeStampedModel):
|
||||
"""Model to track API access for a user."""
|
||||
|
||||
PENDING = 'pending'
|
||||
DENIED = 'denied'
|
||||
APPROVED = 'approved'
|
||||
STATUS_CHOICES = (
|
||||
(PENDING, _('Pending')),
|
||||
(DENIED, _('Denied')),
|
||||
(APPROVED, _('Approved')),
|
||||
)
|
||||
user = models.ForeignKey(User)
|
||||
status = models.CharField(
|
||||
max_length=255,
|
||||
choices=STATUS_CHOICES,
|
||||
default=PENDING,
|
||||
db_index=True,
|
||||
help_text=_('Status of this API access request'),
|
||||
)
|
||||
website = models.URLField(help_text=_('The URL of the website associated with this API user.'))
|
||||
reason = models.TextField(help_text=_('The reason this user wants to access the API.'))
|
||||
|
||||
history = HistoricalRecords()
|
||||
|
||||
@classmethod
|
||||
def has_api_access(cls, user):
|
||||
"""Returns whether or not this user has been granted API access.
|
||||
|
||||
Arguments:
|
||||
user (User): The user to check access for.
|
||||
|
||||
Returns:
|
||||
bool
|
||||
"""
|
||||
return cls.objects.filter(user=user, status=cls.APPROVED).exists()
|
||||
|
||||
def approve(self):
|
||||
"""Approve this request."""
|
||||
log.info('Approving API request from user [%s].', self.user.id)
|
||||
self.status = self.APPROVED
|
||||
self.save()
|
||||
|
||||
def deny(self):
|
||||
"""Deny this request."""
|
||||
log.info('Denying API request from user [%s].', self.user.id)
|
||||
self.status = self.DENIED
|
||||
self.save()
|
||||
|
||||
def __unicode__(self):
|
||||
return u'ApiAccessRequest {website} [{status}]'.format(website=self.website, status=self.status)
|
||||
0
openedx/core/djangoapps/api_admin/tests/__init__.py
Normal file
0
openedx/core/djangoapps/api_admin/tests/__init__.py
Normal file
14
openedx/core/djangoapps/api_admin/tests/factories.py
Normal file
14
openedx/core/djangoapps/api_admin/tests/factories.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""Factories for API management."""
|
||||
import factory
|
||||
from factory.django import DjangoModelFactory
|
||||
|
||||
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
|
||||
class ApiAccessRequestFactory(DjangoModelFactory):
|
||||
"""Factory for ApiAccessRequest objects."""
|
||||
class Meta(object):
|
||||
model = ApiAccessRequest
|
||||
|
||||
user = factory.SubFactory(UserFactory)
|
||||
44
openedx/core/djangoapps/api_admin/tests/test_models.py
Normal file
44
openedx/core/djangoapps/api_admin/tests/test_models.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# pylint: disable=missing-docstring
|
||||
import ddt
|
||||
from django.test import TestCase
|
||||
|
||||
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest
|
||||
from openedx.core.djangoapps.api_admin.tests.factories import ApiAccessRequestFactory
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class ApiAccessRequestTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ApiAccessRequestTests, self).setUp()
|
||||
self.user = UserFactory()
|
||||
self.request = ApiAccessRequestFactory(user=self.user)
|
||||
|
||||
def test_default_status(self):
|
||||
self.assertEqual(self.request.status, ApiAccessRequest.PENDING)
|
||||
self.assertFalse(ApiAccessRequest.has_api_access(self.user))
|
||||
|
||||
def test_approve(self):
|
||||
self.request.approve() # pylint: disable=no-member
|
||||
self.assertEqual(self.request.status, ApiAccessRequest.APPROVED)
|
||||
|
||||
def test_deny(self):
|
||||
self.request.deny() # pylint: disable=no-member
|
||||
self.assertEqual(self.request.status, ApiAccessRequest.DENIED)
|
||||
|
||||
def test_nonexistent_request(self):
|
||||
"""Test that users who have not requested API access do not get it."""
|
||||
other_user = UserFactory()
|
||||
self.assertFalse(ApiAccessRequest.has_api_access(other_user))
|
||||
|
||||
@ddt.data(
|
||||
(ApiAccessRequest.PENDING, False),
|
||||
(ApiAccessRequest.DENIED, False),
|
||||
(ApiAccessRequest.APPROVED, True),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_has_access(self, status, should_have_access):
|
||||
self.request.status = status
|
||||
self.request.save() # pylint: disable=no-member
|
||||
self.assertEqual(ApiAccessRequest.has_api_access(self.user), should_have_access)
|
||||
Reference in New Issue
Block a user