Commit 7ab6aaaa by Peter Fogg

Merge pull request #11959 from edx/peter-fogg/request-api-access

Admin access for API requests.
parents 9fe06134 374e97c1
......@@ -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"
......
"""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',)
# -*- 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',
},
),
]
# -*- 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)
]
"""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)
"""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)
# 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)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment