Near Stable Code commit

Large commit with current existing code base.
The application mostly works, but for ease of
review I'm merging a lot of my commits and will
then do more consistent commits on top of this.

This code base is ready for review as a whole
and is unlikely to change drastically anymore.

Change-Id: Iecf75c44da28d3be14964a205cbe44dc3d9d5619
This commit is contained in:
adriant 2015-01-26 13:33:28 +13:00
parent 745637d3da
commit 96cea92af0
36 changed files with 2837 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*.pyc
*.sqlite3

0
api_v1/__init__.py Normal file
View File

20
api_v1/admin.py Normal file
View File

@ -0,0 +1,20 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.contrib import admin
from models import Token, Registration
# Register your models here.
admin.site.register(Token)
admin.site.register(Registration)

View File

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import jsonfield.fields
import api_v1.models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='Notification',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('notes', jsonfield.fields.JSONField(default={})),
('created', models.DateTimeField(default=django.utils.timezone.now)),
('acknowledged', models.BooleanField(default=False)),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Registration',
fields=[
('uuid', models.CharField(default=api_v1.models.hex_uuid, max_length=200, serialize=False, primary_key=True)),
('reg_ip', models.GenericIPAddressField()),
('keystone_user', jsonfield.fields.JSONField(default={})),
('action_notes', jsonfield.fields.JSONField(default={})),
('approved', models.BooleanField(default=False)),
('completed', models.BooleanField(default=False)),
('created', models.DateTimeField(default=django.utils.timezone.now)),
('approved_on', models.DateTimeField(null=True)),
('completed_on', models.DateTimeField(null=True)),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Token',
fields=[
('token', models.CharField(max_length=200, serialize=False, primary_key=True)),
('created', models.DateTimeField(default=django.utils.timezone.now)),
('expires', models.DateTimeField()),
('registration', models.ForeignKey(to='api_v1.Registration')),
],
options={
},
bases=(models.Model,),
),
migrations.AddField(
model_name='notification',
name='registration',
field=models.ForeignKey(to='api_v1.Registration'),
preserve_default=True,
),
]

View File

109
api_v1/models.py Normal file
View File

@ -0,0 +1,109 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.db import models
from uuid import uuid4
from django.utils import timezone
from jsonfield import JSONField
def hex_uuid():
return uuid4().hex
class Registration(models.Model):
"""
Wrapper object for the request and related actions.
Stores the state registration and a log for the
action.
"""
uuid = models.CharField(max_length=200, default=hex_uuid,
primary_key=True)
# who is this:
reg_ip = models.GenericIPAddressField()
keystone_user = JSONField(default={})
# Effectively a log of what the actions are doing.
action_notes = JSONField(default={})
approved = models.BooleanField(default=False)
completed = models.BooleanField(default=False)
created = models.DateTimeField(default=timezone.now)
approved_on = models.DateTimeField(null=True)
completed_on = models.DateTimeField(null=True)
def __init__(self, *args, **kwargs):
super(Registration, self).__init__(*args, **kwargs)
# in memory dict to be used for passing data between actions:
self.cache = {}
@property
def actions(self):
return self.action_set.order_by('order')
def to_dict(self):
actions = []
for action in self.actions:
actions.append({
"action_name": action.action_name,
"data": action.action_data,
"valid": action.valid
})
return {
"ip_address": self.reg_ip, "notes": self.action_notes,
"approved": self.approved, "completed": self.completed,
"actions": actions, "uuid": self.uuid
}
def add_action_note(self, action, note):
if action in self.action_notes:
self.action_notes[action].append(note)
else:
self.action_notes[action] = [note]
self.save()
class Token(models.Model):
""""""
registration = models.ForeignKey(Registration)
token = models.CharField(max_length=200, primary_key=True)
created = models.DateTimeField(default=timezone.now)
expires = models.DateTimeField()
def to_dict(self):
return {
"registration": self.registration.uuid,
"token": self.token, "expires": self.expires
}
class Notification(models.Model):
""""""
notes = JSONField(default={})
registration = models.ForeignKey(Registration)
created = models.DateTimeField(default=timezone.now)
acknowledged = models.BooleanField(default=False)
def to_dict(self):
return {
"notes": self.notes,
"registration": self.registration.uuid,
"acknowledged": self.acknowledged,
"created": self.created
}

649
api_v1/tests.py Normal file
View File

@ -0,0 +1,649 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from rest_framework import status
from rest_framework.test import APITestCase
from api_v1.models import Registration, Token
import mock
from django.utils import timezone
from datetime import timedelta
temp_cache = {}
class FakeManager(object):
def find_user(self, name):
global temp_cache
return temp_cache['users'].get(name, None)
def get_user(self, user_id):
pass
def create_user(self, name, password, email, project_id):
global temp_cache
user = mock.Mock()
temp_cache['i'] += 1
user.id = temp_cache['i']
user.name = name
user.password = password
user.email = email
user.default_project = project_id
temp_cache['users'][name] = user
return user
def update_user_password(self, user, password):
global temp_cache
user = temp_cache['users'][user.name]
user.password = password
def find_role(self, name):
global temp_cache
temp_cache['roles'].get(name, None)
def add_user_role(self, user, role, project_id):
project = self.get_project(project_id)
project.roles.append((user, role))
def find_project(self, project_name):
global temp_cache
return temp_cache['projects'].get(project_name, None)
def get_project(self, project_id):
global temp_cache
for project in temp_cache['projects'].values():
if project.id == project_id:
return project
def create_project(self, project_name, p_id=None):
global temp_cache
project = mock.Mock()
if p_id:
project.id = p_id
else:
temp_cache['i'] += 1
project.id = temp_cache['i']
project.name = project_name
project.roles = []
temp_cache['projects'][project_name] = project
return project
class APITests(APITestCase):
"""Tests to ensure the approval/token workflow does
what is expected. These test don't check final
results for actions, simply that the registrations,
action, and tokens are created/updated.
These tests also focus on authentication status
and role prermissions."""
@mock.patch('base.models.IdentityManager', FakeManager)
def test_new_user(self):
"""
Ensure the new user workflow goes as expected.
Create registration, create token, submit token.
"""
global temp_cache
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.roles = []
temp_cache = {
'i': 0,
'users': {},
'projects': {'test_project': project},
'roles': {
'Member': 'Member', 'admin': 'admin',
'project_owner': 'project_owner'
}
}
url = "/api_v1/user"
headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "project_owner,Member",
'username': "test@example.com",
'user_id': "test_user_id",
'authenticated': True
}
data = {'email': "test@example.com", 'role': "Member",
'project_id': 'test_project_id'}
response = self.client.post(url, data, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'notes': ['created token']})
new_token = Token.objects.all()[0]
url = "/api_v1/token/" + new_token.token
data = {'password': 'testpassword'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
@mock.patch('base.models.IdentityManager', FakeManager)
def test_new_user_no_project(self):
"""
Can't create a user for a non-existent project.
"""
global temp_cache
temp_cache = {
'i': 0,
'users': {},
'projects': {},
'roles': {
'Member': 'Member', 'admin': 'admin',
'project_owner': 'project_owner'
}
}
url = "/api_v1/user"
headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "project_owner,Member",
'username': "test@example.com",
'user_id': "test_user_id",
'authenticated': True
}
data = {'email': "test@example.com", 'role': "Member",
'project_id': 'test_project_id'}
response = self.client.post(url, data, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'notes': ['actions invalid']})
@mock.patch('base.models.IdentityManager', FakeManager)
def test_new_user_not_my_project(self):
"""
Can't create a user for project that isn't mine.
"""
global temp_cache
temp_cache = {
'i': 0,
'users': {},
'projects': {},
'roles': {
'Member': 'Member', 'admin': 'admin',
'project_owner': 'project_owner'
}
}
url = "/api_v1/user"
headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "Member",
'username': "test@example.com",
'user_id': "test_user_id",
'authenticated': True
}
data = {'email': "test@example.com", 'role': "Member",
'project_id': 'test_project_id'}
response = self.client.post(url, data, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(
response.data,
{'notes':
[("Must have one of the following roles: " +
"['admin', 'project_owner']")]}
)
@mock.patch('base.models.IdentityManager', FakeManager)
def test_new_user_not_my_project_admin(self):
"""
Can create a user for project that isn't mine if admin.
"""
global temp_cache
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.roles = []
temp_cache = {
'i': 0,
'users': {},
'projects': {'test_project': project},
'roles': {
'Member': 'Member', 'admin': 'admin',
'project_owner': 'project_owner'
}
}
url = "/api_v1/user"
headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "admin",
'username': "test@example.com",
'user_id': "test_user_id",
'authenticated': True
}
data = {'email': "test@example.com", 'role': "Member",
'project_id': 'test_project_id'}
response = self.client.post(url, data, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'notes': ['created token']})
new_token = Token.objects.all()[0]
url = "/api_v1/token/" + new_token.token
data = {'password': 'testpassword'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
@mock.patch('base.models.IdentityManager', FakeManager)
def test_new_user_not_authenticated(self):
"""
Can't create a user if unauthenticated.
"""
global temp_cache
temp_cache = {
'i': 0,
'users': {},
'projects': {},
'roles': {
'Member': 'Member', 'admin': 'admin',
'project_owner': 'project_owner'
}
}
url = "/api_v1/user"
headers = {}
data = {'email': "test@example.com", 'role': "Member",
'project_id': 'test_project_id'}
response = self.client.post(url, data, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(
response.data,
{'notes':
[("Must have one of the following roles: " +
"['admin', 'project_owner']")]}
)
@mock.patch('base.models.IdentityManager', FakeManager)
@mock.patch('tenant_setup.models.IdentityManager', FakeManager)
def test_new_project(self):
"""
Ensure the new project workflow goes as expected.
"""
global temp_cache
temp_cache = {
'i': 0,
'users': {},
'projects': {},
'roles': {
'Member': 'Member', 'admin': 'admin',
'project_owner': 'project_owner'
}
}
url = "/api_v1/project"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "admin,Member",
'username': "test@example.com",
'user_id': "test_user_id",
'authenticated': True
}
new_registration = Registration.objects.all()[0]
url = "/api_v1/registration/" + new_registration.uuid
response = self.client.post(url, {'approved': True}, format='json',
headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
new_token = Token.objects.all()[0]
url = "/api_v1/token/" + new_token.token
data = {'password': 'testpassword'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
@mock.patch('base.models.IdentityManager', FakeManager)
@mock.patch('tenant_setup.models.IdentityManager', FakeManager)
def test_new_project_existing(self):
"""
Test to ensure validation marks actions as invalid
if project is already present.
"""
global temp_cache
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.roles = []
temp_cache = {
'i': 0,
'users': {},
'projects': {'test_project': project},
'roles': {
'Member': 'Member', 'admin': 'admin',
'project_owner': 'project_owner'
}
}
url = "/api_v1/user"
headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "project_owner,Member",
'username': "test@example.com",
'user_id': "test_user_id",
'authenticated': True
}
url = "/api_v1/project"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "admin,Member",
'username': "test@example.com",
'user_id': "test_user_id",
'authenticated': True
}
new_registration = Registration.objects.all()[0]
url = "/api_v1/registration/" + new_registration.uuid
response = self.client.post(url, {'approved': True}, format='json',
headers=headers)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'notes': ['actions invalid']})
@mock.patch('base.models.IdentityManager', FakeManager)
@mock.patch('tenant_setup.models.IdentityManager', FakeManager)
def test_new_project_existing_user(self):
"""
Project created if not present, existing user attached.
No token should be needed.
"""
global temp_cache
user = mock.Mock()
user.id = 'user_id'
user.name = "test@example.com"
user.email = "test@example.com"
temp_cache = {
'i': 0,
'users': {user.name: user},
'projects': {},
'roles': {
'Member': 'Member', 'admin': 'admin',
'project_owner': 'project_owner'
}
}
url = "/api_v1/user"
headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "project_owner,Member",
'username': "test@example.com",
'user_id': "test_user_id",
'authenticated': True
}
url = "/api_v1/project"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "admin,Member",
'username': "test@example.com",
'user_id': "test_user_id",
'authenticated': True
}
new_registration = Registration.objects.all()[0]
url = "/api_v1/registration/" + new_registration.uuid
response = self.client.post(url, {'approved': True}, format='json',
headers=headers)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data,
{'notes': 'Registration completed successfully.'}
)
@mock.patch('base.models.IdentityManager', FakeManager)
def test_reset_user(self):
"""
Ensure the reset user workflow goes as expected.
Create registration + create token, submit token.
"""
global temp_cache
user = mock.Mock()
user.id = 'user_id'
user.name = "test@example.com"
user.email = "test@example.com"
user.password = "test_password"
temp_cache = {
'i': 0,
'users': {user.name: user},
'projects': {},
'roles': {
'Member': 'Member', 'admin': 'admin',
'project_owner': 'project_owner'
}
}
url = "/api_v1/reset"
data = {'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'notes': ['created token']})
new_token = Token.objects.all()[0]
url = "/api_v1/token/" + new_token.token
data = {'password': 'new_test_password'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(user.password, 'new_test_password')
@mock.patch('base.models.IdentityManager', FakeManager)
def test_reset_user_no_existing(self):
"""
Actions should be invalid.
"""
global temp_cache
temp_cache = {
'i': 0,
'users': {},
'projects': {},
'roles': {
'Member': 'Member', 'admin': 'admin',
'project_owner': 'project_owner'
}
}
url = "/api_v1/reset"
data = {'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'notes': ['actions invalid']})
def test_no_token_get(self):
"""
Should be a 404.
"""
url = "/api_v1/token/e8b3f57f5da64bf3a6bf4f9bbd3a40b5"
response = self.client.get(url, format='json')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(
response.data, {'notes': ['This token does not exist.']})
def test_no_token_post(self):
"""
Should be a 404.
"""
url = "/api_v1/token/e8b3f57f5da64bf3a6bf4f9bbd3a40b5"
response = self.client.post(url, format='json')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(
response.data, {'notes': ['This token does not exist.']})
def test_no_registration_get(self):
"""
Should be a 404.
"""
headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "admin",
'username': "test@example.com",
'user_id': "test_user_id",
'authenticated': True
}
url = "/api_v1/registration/e8b3f57f5da64bf3a6bf4f9bbd3a40b5"
response = self.client.get(url, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(
response.data, {'notes': ['No registration with this id.']})
def test_no_registration_post(self):
"""
Should be a 404.
"""
headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "admin",
'username': "test@example.com",
'user_id': "test_user_id",
'authenticated': True
}
url = "/api_v1/registration/e8b3f57f5da64bf3a6bf4f9bbd3a40b5"
response = self.client.post(url, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(
response.data, {'notes': ['No registration with this id.']})
@mock.patch('base.models.IdentityManager', FakeManager)
def test_token_expired_post(self):
"""
Expired token should do nothing, then delete itself.
"""
global temp_cache
user = mock.Mock()
user.id = 'user_id'
user.name = "test@example.com"
user.email = "test@example.com"
user.password = "test_password"
temp_cache = {
'i': 0,
'users': {user.name: user},
'projects': {},
'roles': {
'Member': 'Member', 'admin': 'admin',
'project_owner': 'project_owner'
}
}
url = "/api_v1/reset"
data = {'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'notes': ['created token']})
new_token = Token.objects.all()[0]
new_token.expires = timezone.now() - timedelta(hours=24)
new_token.save()
url = "/api_v1/token/" + new_token.token
data = {'password': 'new_test_password'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'notes': ['This token has expired.']})
self.assertEqual(0, Token.objects.count())
@mock.patch('base.models.IdentityManager', FakeManager)
def test_token_expired_get(self):
"""
Expired token should do nothing, then delete itself.
"""
global temp_cache
user = mock.Mock()
user.id = 'user_id'
user.name = "test@example.com"
user.email = "test@example.com"
user.password = "test_password"
temp_cache = {
'i': 0,
'users': {user.name: user},
'projects': {},
'roles': {
'Member': 'Member', 'admin': 'admin',
'project_owner': 'project_owner'
}
}
url = "/api_v1/reset"
data = {'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'notes': ['created token']})
new_token = Token.objects.all()[0]
new_token.expires = timezone.now() - timedelta(hours=24)
new_token.save()
url = "/api_v1/token/" + new_token.token
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'notes': ['This token has expired.']})
self.assertEqual(0, Token.objects.count())
@mock.patch('base.models.IdentityManager', FakeManager)
@mock.patch('tenant_setup.models.IdentityManager', FakeManager)
def test_registration_complete(self):
"""
Can't approve a completed registration.
"""
global temp_cache
temp_cache = {
'i': 0,
'users': {},
'projects': {},
'roles': {
'Member': 'Member', 'admin': 'admin',
'project_owner': 'project_owner'
}
}
url = "/api_v1/project"
data = {'project_name': "test_project", 'email': "test@example.com"}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
headers = {
'project_name': "test_project",
'project_id': "test_project_id",
'roles': "admin,Member",
'username': "test@example.com",
'user_id': "test_user_id",
'authenticated': True
}
new_registration = Registration.objects.all()[0]
new_registration.completed = True
new_registration.save()
url = "/api_v1/registration/" + new_registration.uuid
response = self.client.post(url, {'approved': True}, format='json',
headers=headers)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.data,
{'notes': ['This registration has already been completed.']})

31
api_v1/urls.py Normal file
View File

@ -0,0 +1,31 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls import patterns, url
from api_v1 import views
urlpatterns = patterns(
'',
# Examples:
# url(r'^$', 'user_reg.views.home', name='home'),
# url(r'^blog/', include('blog.urls')),
url(r'^registration/(?P<uuid>\w+)', views.RegistrationDetail.as_view()),
url(r'^registration', views.RegistrationList.as_view()),
url(r'^project', views.CreateProject.as_view()),
url(r'^user', views.AttachUser.as_view()),
url(r'^reset', views.ResetPassword.as_view()),
url(r'^token/(?P<id>\w+)', views.TokenDetail.as_view()),
url(r'^token', views.TokenList.as_view()),
)

602
api_v1/views.py Normal file
View File

@ -0,0 +1,602 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from decorator import decorator
from rest_framework.views import APIView
from rest_framework.response import Response
from models import Registration, Token, Notification
from django.utils import timezone
from datetime import timedelta
from uuid import uuid4
from logging import getLogger
from django.conf import settings
@decorator
def admin_or_owner(func, *args, **kwargs):
req_roles = {'admin', 'project_owner'}
request = args[1]
roles = set(request.keystone_user.get('roles', []))
if roles & req_roles:
return func(*args, **kwargs)
return Response({'notes': ["Must have one of the following roles: %s" %
list(req_roles)]},
403)
@decorator
def admin(func, *args, **kwargs):
request = args[1]
roles = request.keystone_user.get('roles', [])
if "admin" in roles:
return func(*args, **kwargs)
return Response({'notes': ["Must be admin."]},
403)
def create_token(registration):
# expire needs to be made configurable.
expire = timezone.now() + timedelta(hours=24)
# is this a good way to create tokens?
uuid = uuid4().hex
token = Token.objects.create(
registration=registration,
token=uuid,
expires=expire
)
token.save()
def create_notification(registration, notes):
notification = Notification.objects.create(
registration=registration,
notes=notes
)
notification.save()
class APIViewWithLogger(APIView):
"""
APIView with a logger.
"""
def __init__(self, *args, **kwargs):
super(APIViewWithLogger, self).__init__(*args, **kwargs)
self.logger = getLogger('django.request')
class NotificationList(APIViewWithLogger):
@admin
def get(self, request, format=None):
"""A list of dict representations of Notification objects."""
notifications = Notification.objects.all()
note_list = []
for notification in notifications:
note_list.append(notification.to_dict())
return Response(note_list)
class RegistrationList(APIViewWithLogger):
@admin
def get(self, request, format=None):
"""A list of dict representations of Registration objects
and their related actions."""
registrations = Registration.objects.all()
reg_list = []
for registration in registrations:
reg_list.append(registration.to_dict())
return Response(reg_list)
class RegistrationDetail(APIViewWithLogger):
@admin
def get(self, request, uuid, format=None):
"""Dict representation of a Registration object
and its related actions."""
try:
registration = Registration.objects.get(uuid=uuid)
except Registration.DoesNotExist:
return Response(
{'notes': ['No registration with this id.']},
status=404)
return Response(registration.to_dict())
@admin
def post(self, request, uuid, format=None):
"""Will approve the Registration specified,
followed by running the post_approve actions
and if valid will setup and create a related token. """
try:
registration = Registration.objects.get(uuid=uuid)
except Registration.DoesNotExist:
return Response(
{'notes': ['No registration with this id.']},
status=404)
if request.data.get('approved', False) is True:
if registration.completed:
return Response(
{'notes':
['This registration has already been completed.']},
status=400)
registration.approved = True
registration.approved_on = timezone.now()
registration.save()
need_token = False
valid = True
actions = []
for action in registration.actions:
act_model = action.get_action()
actions.append(act_model)
try:
act_model.post_approve()
except Exception as e:
notes = {
'errors':
[("Error: '%s' while approving registration. " +
"See registration itself for details.") % e],
'registration': registration.uuid
}
create_notification(registration, notes)
import traceback
trace = traceback.format_exc()
self.logger.critical(("(%s) - Exception escaped! %s\n" +
"Trace: \n%s") %
(timezone.now(), e, trace))
return Response(notes, status=500)
if not action.valid:
valid = False
if action.need_token:
need_token = True
if valid:
if need_token:
create_token(registration)
return Response({'notes': ['created token']}, status=200)
else:
for action in actions:
try:
action.submit({})
except Exception as e:
notes = {
'errors':
[("Error: '%s' while submitting " +
"registration. See registration " +
"itself for details.") % e],
'registration': registration.uuid
}
create_notification(registration, notes)
import traceback
trace = traceback.format_exc()
self.logger.critical(("(%s) - Exception escaped!" +
" %s\n Trace: \n%s") %
(timezone.now(), e, trace))
return Response(notes, status=500)
registration.completed = True
registration.completed_on = timezone.now()
registration.save()
return Response(
{'notes': "Registration completed successfully."},
status=200)
return Response({'notes': ['actions invalid']}, status=400)
else:
return Response({'approved': ["this field is required."]},
status=400)
class TokenList(APIViewWithLogger):
"""Admin functionality for managing/monitoring tokens."""
@admin
def get(self, request, format=None):
"""A list of dict representations of Token objects."""
tokens = Token.objects.all()
token_list = []
for token in tokens:
token_list.append(token.to_dict())
return Response(token_list)
class TokenDetail(APIViewWithLogger):
def get(self, request, id, format=None):
"""Returns a response with the list of required fields
and what actions those go towards."""
try:
token = Token.objects.get(token=id)
except Token.DoesNotExist:
return Response(
{'notes': ['This token does not exist.']}, status=404)
if token.expires < timezone.now():
token.delete()
return Response({'notes': ['This token has expired.']}, status=400)
required_fields = []
actions = []
for action in token.registration.actions:
action = action.get_action()
actions.append(action)
for field in action.token_fields:
if field not in required_fields:
required_fields.append(field)
return Response({'actions': [unicode(act) for act in actions],
'required_fields': required_fields})
def post(self, request, id, format=None):
"""Ensures the required fields are present,
will then pass those to the actions via the submit
function."""
try:
token = Token.objects.get(token=id)
except Token.DoesNotExist:
return Response(
{'notes': ['This token does not exist.']}, status=404)
if token.expires < timezone.now():
token.delete()
return Response({'notes': ['This token has expired.']}, status=400)
required_fields = set()
actions = []
for action in token.registration.actions:
action = action.get_action()
actions.append(action)
for field in action.token_fields:
required_fields.add(field)
errors = {}
data = {}
for field in required_fields:
try:
data[field] = request.data[field]
except KeyError:
errors[field] = ["This field is required.", ]
if errors:
return Response(errors, status=400)
for action in actions:
try:
action.submit(data)
except Exception as e:
notes = {
'errors':
[("Error: '%s' while submitting registration. " +
"See registration itself for details.") % e],
'registration': token.registration.uuid
}
create_notification(token.registration, notes)
import traceback
trace = traceback.format_exc()
self.logger.critical(("(%s) - Exception escaped! %s\n" +
"Trace: \n%s") %
(timezone.now(), e, trace))
response_dict = {
'errors':
["Error: Something went wrong on the server. " +
"It will be looked into shortly."]
}
return Response(response_dict, status=500)
token.registration.completed = True
token.registration.completed_on = timezone.now()
token.registration.save()
token.delete()
return Response(
{'notes': "Token submitted successfully."},
status=200)
class ActionView(APIViewWithLogger):
"""Base class for api calls that start a Registration.
Until it is moved to settings, 'default_action' is a
required hardcoded field."""
def get(self, request):
actions = [self.default_action, ]
actions += settings.API_ACTIONS.get(self.__class__.__name__, [])
required_fields = []
for action in actions:
action_class, action_serializer = settings.ACTION_CLASSES[action]
for field in action_class.required:
if field not in required_fields:
required_fields.append(field)
return Response({'actions': actions,
'required_fields': required_fields})
def process_actions(self, request):
"""
Will ensure the request data contains the required data
based on the action serializer, and if present will create
a Registration and the linked actions, attaching notes
based on running of the the pre_approve validation
function on all the actions.
"""
actions = [self.default_action, ]
actions += settings.API_ACTIONS.get(self.__class__.__name__, [])
act_list = []
valid = True
for action in actions:
action_class, action_serializer = settings.ACTION_CLASSES[action]
if action_serializer is not None:
serializer = action_serializer(data=request.data)
else:
serializer = None
act_list.append({
'name': action,
'action': action_class,
'serializer': serializer})
if serializer is not None and not serializer.is_valid():
valid = False
if valid:
ip_addr = request.META['REMOTE_ADDR']
keystone_user = request.keystone_user
registration = Registration.objects.create(
reg_ip=ip_addr, keystone_user=keystone_user)
registration.save()
for i, act in enumerate(act_list):
if act['serializer'] is not None:
data = act['serializer'].validated_data
else:
data = {}
action = act['action'](
data=data, registration=registration,
order=i
)
try:
action.pre_approve()
except Exception as e:
notes = {
'errors':
[("Error: '%s' while setting up registration. " +
"See registration itself for details.") % e],
'registration': registration.uuid
}
create_notification(registration, notes)
import traceback
trace = traceback.format_exc()
self.logger.critical(("(%s) - Exception escaped! %s\n" +
"Trace: \n%s") %
(timezone.now(), e, trace))
response_dict = {
'errors':
["Error: Something went wrong on the server. " +
"It will be looked into shortly."]
}
return response_dict
return {'registration': registration}
else:
errors = {}
for act in act_list:
if act['serializer'] is not None:
errors.update(act['serializer'].errors)
return {'errors': errors}
def approve(self, registration):
"""
Approves the registration and runs the post_approve steps.
Will create a token if required, otherwise will run the
submit steps.
"""
registration.approved = True
registration.approved_on = timezone.now()
registration.save()
action_models = registration.actions
actions = []
valid = True
need_token = False
for action in action_models:
act = action.get_action()
actions.append(act)
if not act.valid:
valid = False
if valid:
for action in actions:
try:
action.post_approve()
except Exception as e:
notes = {
'errors':
[("Error: '%s' while approving registration. " +
"See registration itself for details.") % e],
'registration': registration.uuid
}
create_notification(registration, notes)
import traceback
trace = traceback.format_exc()
self.logger.critical(("(%s) - Exception escaped! %s\n" +
"Trace: \n%s") %
(timezone.now(), e, trace))
response_dict = {
'errors':
["Error: Something went wrong on the server. " +
"It will be looked into shortly."]
}
return Response(response_dict, status=500)
if not action.valid:
valid = False
if action.need_token:
need_token = True
if valid:
if need_token:
create_token(registration)
return Response({'notes': ['created token']}, status=200)
else:
for action in actions:
try:
action.submit({})
except Exception as e:
notes = {
'errors':
[("Error: '%s' while submitting " +
"registration. See registration " +
"itself for details.") % e],
'registration': registration.uuid
}
create_notification(registration, notes)
import traceback
trace = traceback.format_exc()
self.logger.critical(("(%s) - Exception escaped!" +
" %s\n Trace: \n%s") %
(timezone.now(), e, trace))
response_dict = {
'errors':
["Error: Something went wrong on the " +
"server. It will be looked into shortly."]
}
return Response(response_dict, status=500)
registration.completed = True
registration.completed_on = timezone.now()
registration.save()
return Response(
{'notes': "Registration completed successfully."},
status=200)
return Response({'notes': ['actions invalid']}, status=400)
return Response({'notes': ['actions invalid']}, status=400)
class CreateProject(ActionView):
default_action = "NewProject"
def post(self, request, format=None):
"""
Runs internal process_actions and sends back notes or errors.
"""
self.logger.info("(%s) - Starting new project registration." %
timezone.now())
processed = self.process_actions(request)
errors = processed.get('errors', None)
if errors:
self.logger.info("(%s) - Validation errors with registration." %
timezone.now())
return Response(errors, status=400)
notes = {
'notes':
['New registration for CreateProject.']
}
create_notification(processed['registration'], notes)
self.logger.info("(%s) - Registration created." % timezone.now())
return Response({'notes': ['registration created']}, status=200)
class AttachUser(ActionView):
default_action = 'NewUser'
@admin_or_owner
def get(self, request):
return super(AttachUser, self).get(request)
@admin_or_owner
def post(self, request, format=None):
"""
This endpoint requires either Admin access or the
request to come from a project_owner.
As such this Registration is considered pre-approved.
Runs process_actions, then does the approve and
post_approve validation, and creates a Token if valid.
"""
self.logger.info("(%s) - New AttachUser request." % timezone.now())
processed = self.process_actions(request)
errors = processed.get('errors', None)
if errors:
self.logger.info("(%s) - Validation errors with registration." %
timezone.now())
return Response(errors, status=400)
registration = processed['registration']
self.logger.info("(%s) - AutoApproving AttachUser request."
% timezone.now())
return self.approve(registration)
class ResetPassword(ActionView):
default_action = 'ResetUser'
def post(self, request, format=None):
"""
Unauthenticated endpoint bound to the password reset action.
"""
self.logger.info("(%s) - New ResetUser request." % timezone.now())
processed = self.process_actions(request)
errors = processed.get('errors', None)
if errors:
self.logger.info("(%s) - Validation errors with registration." %
timezone.now())
return Response(errors, status=400)
registration = processed['registration']
self.logger.info("(%s) - AutoApproving Resetuser request."
% timezone.now())
return self.approve(registration)

0
base/__init__.py Normal file
View File

19
base/admin.py Normal file
View File

@ -0,0 +1,19 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.contrib import admin
from models import Action
# Register your models here.
admin.site.register(Action)

View File

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.utils.timezone
import jsonfield.fields
class Migration(migrations.Migration):
dependencies = [
('api_v1', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Action',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('action_name', models.CharField(max_length=200)),
('action_data', jsonfield.fields.JSONField(default={})),
('cache', jsonfield.fields.JSONField(default={})),
('state', models.CharField(default=b'default', max_length=200)),
('valid', models.BooleanField(default=False)),
('need_token', models.BooleanField()),
('order', models.IntegerField()),
('created', models.DateTimeField(default=django.utils.timezone.now)),
('registration', models.ForeignKey(to='api_v1.Registration')),
],
options={
},
bases=(models.Model,),
),
]

View File

467
base/models.py Normal file
View File

@ -0,0 +1,467 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.db import models
from django.utils import timezone
from user_store import IdentityManager
from serializers import (NewUserSerializer, NewProjectSerializer,
ResetUserSerializer)
from django.conf import settings
from jsonfield import JSONField
from logging import getLogger
class Action(models.Model):
"""
Database model representation of an action.
"""
action_name = models.CharField(max_length=200)
action_data = JSONField(default={})
cache = JSONField(default={})
state = models.CharField(max_length=200, default="default")
valid = models.BooleanField(default=False)
need_token = models.BooleanField()
registration = models.ForeignKey('api_v1.Registration')
order = models.IntegerField()
created = models.DateTimeField(default=timezone.now)
def get_action(self):
"""Returns self as the appropriate action wrapper type."""
data = self.action_data
return settings.ACTION_CLASSES[self.action_name][0](
data=data, action_model=self)
class BaseAction(object):
"""
Base class for the object wrapping around the database model.
Setup to allow multiple action types and different internal logic
per type but built from a single database type.
- 'required' defines what fields to setup from the data.
- 'token_fields' defined which fields are needed by this action
at the token stage.
The Action can do anything it needs at one of the three functions
called by the views:
- 'pre_approve'
- 'post_approve'
- 'submit'
By using 'get_cache' and 'set_cache' they can pass data along which
may be needed by the action later. This cache is backed to the database.
Passing data along to other actions is done via the registration and
it's cache, but this is in memory only, so it is only useful during the
same action stage ('post_approve', etc.).
"""
required = []
token_fields = []
def __init__(self, data, action_model=None, registration=None,
order=None):
"""
Build itself around an existing database model,
or build itself and creates a new database model.
Sets up required data as fields.
"""
self.logger = getLogger('django.request')
for field in self.required:
field_data = data[field]
setattr(self, field, field_data)
if action_model:
self.action = action_model
else:
if self.token_fields:
need_token = True
else:
need_token = False
# make new model and save in db
action = Action.objects.create(
action_name=self.__class__.__name__,
action_data=data,
need_token=need_token,
registration=registration,
order=order
)
action.save()
self.action = action
@property
def valid(self):
return self.action.valid
@property
def need_token(self):
return self.action.need_token
def get_cache(self, key):
return self.action.cache.get(key, None)
def set_cache(self, key, value):
self.action.cache[key] = value
self.action.save()
def add_note(self, note):
self.logger.info("(%s) - %s" % (timezone.now(), note))
note = "%s - (%s)" % (note, timezone.now())
self.action.registration.add_action_note(
unicode(self), note)
def pre_approve(self):
return self._pre_approve()
def post_approve(self):
return self._post_approve()
def submit(self, token_data):
return self._submit(token_data)
def _pre_approve(self):
raise NotImplementedError
def _post_approve(self):
raise NotImplementedError
def _submit(self, token_data):
raise NotImplementedError
def __unicode__(self):
return self.__class__.__name__
class UserAction(BaseAction):
"""
Base action for dealing with users. Removes username if
USERNAME_IS_EMAIL and sets email to be username.
"""
def __init__(self, *args, **kwargs):
if settings.USERNAME_IS_EMAIL:
try:
self.required.remove('username')
except ValueError:
pass
# nothing to remove
super(UserAction, self).__init__(*args, **kwargs)
self.username = self.email
else:
super(UserAction, self).__init__(*args, **kwargs)
class NewUser(UserAction):
"""
Setup a new user with a role on the given project.
Creates the user if they don't exist, otherwise
if the username and email for the request match the
existing one, will simply add the project role.
"""
required = [
'username',
'email',
'project_id',
'role'
]
token_fields = ['password']
def _validate(self):
id_manager = IdentityManager()
user = id_manager.find_user(self.username)
keystone_user = self.action.registration.keystone_user
if not ("admin" in keystone_user['roles'] or
keystone_user['project_id'] == self.project_id):
self.add_note('Project id does not match keystone user project.')
return False
project = id_manager.get_project(self.project_id)
if not project:
self.add_note('Project does not exist.')
valid = False
else:
if user:
if user.email == self.email:
valid = True
self.action.need_token = False
self.action.state = "existing"
self.add_note('Existing user with matching email.')
else:
valid = False
self.add_note('Existing user with non-matching email.')
else:
valid = True
self.add_note('No user present with username')
return valid
def _pre_approve(self):
self.action.valid = self._validate()
self.action.save()
def _post_approve(self):
self.action.valid = self._validate()
self.action.save()
def _submit(self, token_data):
self.action.valid = self._validate()
self.action.save()
if self.valid:
id_manager = IdentityManager()
if self.action.state == "default":
try:
user = id_manager.create_user(
name=self.username, password=token_data['password'],
email=self.email, project_id=self.project_id)
role = id_manager.find_role(self.role)
id_manager.add_user_role(user, role, self.project_id)
except Exception as e:
self.add_note(
"Error: '%s' while creating user: %s with role: " %
(e, self.username, self.role))
raise
self.add_note(
'User %s has been created, with role %s in project %s.'
% (self.username, self.role, self.project_id))
elif self.action.state == "existing":
try:
user = id_manager.find_user(self.username)
role = id_manager.find_role(self.role)
id_manager.add_user_role(user, role, self.project_id)
except Exception as e:
self.add_note(
"Error: '%s' while attaching user: %s with role: " %
(e, self.username, self.role))
raise
self.add_note(
'Existing user %s has been given role %s in project %s.'
% (self.username, self.role, self.project_id))
class NewProject(UserAction):
"""
Similar functionality as the NewUser action,
but will create the project if valid. Will setup
the user (existing or new) with the 'default_role'.
"""
required = [
'project_name',
'username',
'email'
]
default_roles = ["Member", "project_owner"]
token_fields = ['password']
def _validate_project(self):
id_manager = IdentityManager()
project = id_manager.find_project(self.project_name)
valid = self._validate_user(id_manager)
if project:
valid = False
self.add_note("Existing project with name '%s'." %
self.project_name)
else:
self.add_note("No existing project with name '%s'." %
self.project_name)
return valid
def _validate_user(self, id_manager):
user = id_manager.find_user(self.username)
if user:
if user.email == self.email:
valid = True
self.action.state = "existing"
self.action.need_token = False
self.add_note("Existing user '%s' with matching email." %
self.email)
else:
valid = False
self.add_note("Existing user '%s' with non-matching email." %
self.username)
else:
valid = True
self.add_note("No user present with username '%s'." %
self.username)
return valid
def _pre_approve(self):
self.action.valid = self._validate_project()
self.action.save()
def _post_approve(self):
project_id = self.get_cache('project_id')
if project_id:
self.action.registration.cache['project_id'] = project_id
self.add_note("Project already created.")
return
self.action.valid = self._validate_project()
self.action.save()
if self.valid:
id_manager = IdentityManager()
try:
project = id_manager.create_project(self.project_name)
except Exception as e:
self.add_note(
"Error: '%s' while creating project: %s" %
(e, self.project_name))
raise
# put project_id into action cache:
self.action.registration.cache['project_id'] = project.id
self.set_cache('project_id', project.id)
self.add_note("New project '%s' created." % self.project_name)
def _submit(self, token_data):
id_manager = IdentityManager()
self.action.valid = self._validate_user(id_manager)
self.action.save()
if self.valid:
project_id = self.get_cache('project_id')
self.action.registration.cache['project_id'] = project_id
project = id_manager.get_project(project_id)
if self.action.state == "default":
try:
user = id_manager.create_user(
name=self.username, password=token_data['password'],
email=self.email, project_id=project.id)
for role in self.default_roles:
ks_role = id_manager.find_role(role)
id_manager.add_user_role(user, ks_role, project.id)
except Exception as e:
self.add_note(
"Error: '%s' while creating user: %s with roles: %s" %
(e, self.username, self.default_roles))
raise
self.add_note(
"New user '%s' created for project %s with roles: %s" %
(self.username, self.project_name, self.default_roles))
elif self.action.state == "existing":
user = id_manager.find_user(self.username)
try:
for role in self.default_roles:
ks_role = id_manager.find_role(role)
id_manager.add_user_role(user, ks_role, project.id)
except Exception as e:
self.add_note(
"Error: '%s' while attaching user: %s with roles: %s" %
(e, self.username, self.default_roles))
raise
self.add_note(("Existing user '%s' attached to project %s" +
" with roles: %s")
% (self.username, self.project_name,
self.default_roles))
class ResetUser(UserAction):
"""
Simple action to reset a password for a given user.
"""
username = models.CharField(max_length=200)
email = models.EmailField()
required = [
'username',
'email'
]
token_fields = ['password']
def _validate(self):
id_manager = IdentityManager()
user = id_manager.find_user(self.username)
if user:
if user.email == self.email:
valid = True
self.add_note('Existing user with matching email.')
else:
valid = False
self.add_note('Existing user with non-matching email.')
else:
valid = False
self.add_note('No user present with username')
return valid
def _pre_approve(self):
self.action.valid = self._validate()
self.action.save()
def _post_approve(self):
self.action.valid = self._validate()
self.action.save()
def _submit(self, token_data):
self.action.valid = self._validate()
self.action.save()
if self.valid:
id_manager = IdentityManager()
user = id_manager.find_user(self.username)
try:
id_manager.update_user_password(user, token_data['password'])
except Exception as e:
self.add_note(
"Error: '%s' while changing password for user: %s" %
(e, self.username))
raise
self.add_note('User %s password has been changed.' % self.username)
# A dict of tuples in the format: (<ActionClass>, <ActionSerializer>)
action_classes = {
'NewUser': (NewUser, NewUserSerializer),
'NewProject': (NewProject, NewProjectSerializer),
'ResetUser': (ResetUser, ResetUserSerializer)
}
# setup action classes and serializers for global access
settings.ACTION_CLASSES.update(action_classes)

49
base/openstack_clients.py Normal file
View File

@ -0,0 +1,49 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from keystoneclient.v2_0 import client
from neutronclient.v2_0 import client as neutron_client
from django.conf import settings
def get_keystoneclient():
# try:
# return clients['keystone']
# except KeyError:
# auth = v3.Password(
# auth_url='http://10.0.2.15:5000/v3',
# user_id='admin',
# password='openstack',
# project_name='admin'
# )
# sess = session.Session(auth=auth)
# keystone = KeystoneClient.Client(session=sess)
# clients['keystone'] = keystone
auth = client.Client(
username=settings.KEYSTONE['username'],
password=settings.KEYSTONE['password'],
tenant_name=settings.KEYSTONE['project_name'],
auth_url=settings.KEYSTONE['auth_url']
)
return auth
def get_neutronclient():
neutron = neutron_client.Client(
username=settings.KEYSTONE['username'],
password=settings.KEYSTONE['password'],
tenant_name=settings.KEYSTONE['project_name'],
auth_url=settings.KEYSTONE['auth_url'])
return neutron

46
base/serializers.py Normal file
View File

@ -0,0 +1,46 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from rest_framework import serializers
from django.conf import settings
class BaseUserSerializer(serializers.Serializer):
def __init__(self, *args, **kwargs):
super(BaseUserSerializer, self).__init__(*args, **kwargs)
if settings.USERNAME_IS_EMAIL:
self.fields.pop('username')
class NewUserSerializer(BaseUserSerializer):
username = serializers.CharField(max_length=200)
email = serializers.EmailField()
project_id = serializers.CharField(max_length=200)
role_options = (('project_mod', 'Project Owner (can add new users)'),
('Member', "Project Member (can't add new users)"))
role = serializers.ChoiceField(choices=role_options)
class NewProjectSerializer(BaseUserSerializer):
project_name = serializers.CharField(max_length=200)
username = serializers.CharField(max_length=200)
email = serializers.EmailField()
class ResetUserSerializer(BaseUserSerializer):
username = serializers.CharField(max_length=200)
email = serializers.EmailField()

17
base/tests.py Normal file
View File

@ -0,0 +1,17 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.test import TestCase
# Create your tests here.

80
base/user_store.py Normal file
View File

@ -0,0 +1,80 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from openstack_clients import get_keystoneclient
from keystoneclient.openstack.common.apiclient import (
exceptions as ks_exceptions
)
class IdentityManager(object):
"""
A wrapper object for the Keystone Client. Mainly setup as
such for easier testing, but also so it can be replaced
later with an LDAP + Keystone Client variant.
"""
def __init__(self):
self.ks_client = get_keystoneclient()
def find_user(self, name):
try:
user = self.ks_client.users.find(name=name)
except ks_exceptions.NotFound:
user = None
return user
def get_user(self, user_id):
try:
user = self.ks_client.users.get(user_id)
except ks_exceptions.NotFound:
user = None
return user
def create_user(self, name, password, email, project_id):
user = self.ks_client.users.create(
name=name, password=password,
email=email, tenant_id=project_id)
return user
def update_user_password(self, user, password):
self.ks_client.users.update_password(user, password)
def find_role(self, name):
try:
role = self.ks_client.roles.find(name=name)
except ks_exceptions.NotFound:
role = None
return role
def add_user_role(self, user, role, project_id):
self.ks_client.roles.add_user_role(user, role, project_id)
def find_project(self, project_name):
try:
project = self.ks_client.tenants.find(name=project_name)
except ks_exceptions.NotFound:
project = None
return project
def get_project(self, project_id):
try:
project = self.ks_client.tenants.get(project_id)
except ks_exceptions.NotFound:
project = None
return project
def create_project(self, project_name):
project = self.ks_client.tenants.create(project_name)
return project

17
base/views.py Normal file
View File

@ -0,0 +1,17 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.shortcuts import render
# Create your views here.

10
manage.py Executable file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "user_reg.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)

0
openerp/__init__.py Normal file
View File

3
openerp/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

36
openerp/models.py Normal file
View File

@ -0,0 +1,36 @@
from base.models import BaseAction
from serializers import NewClientSerializer
from django.conf import settings
class NewClient(BaseAction):
""""""
required = [
'business_name',
'billing_address',
'billing_phone',
'billing_email'
]
def _pre_approve(self):
self.action.valid = True
self.action.need_token = False
self.action.save()
return []
def _post_approve(self):
self.action.valid = True
self.action.need_token = False
self.action.save()
return []
def _submit(self, token_data):
pass
action_classes = {
'NewClient': (NewClient, NewClientSerializer)
}
settings.ACTION_CLASSES.update(action_classes)

3
openerp/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

6
requirements.txt Normal file
View File

@ -0,0 +1,6 @@
Django==1.7.3
decorator==3.4.0
djangorestframework==3.0.3
keystonemiddleware==1.3.1
python-keystoneclient==1.0.0
jsonfield==1.0.2

0
tenant_setup/__init__.py Normal file
View File

View File

210
tenant_setup/models.py Normal file
View File

@ -0,0 +1,210 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from base.models import BaseAction
from serializers import DefaultProjectResourcesSerializer
from django.conf import settings
from base.user_store import IdentityManager
from base import openstack_clients
class DefaultProjectResources(BaseAction):
"""
This action will setup all required basic networking
resources so that a new user can launch instances
right away.
"""
required = [
'setup_resources'
]
defaults = settings.NETWORK_DEFAULTS[settings.DEFAULT_REGION]
def _validate(self):
project_id = self.action.registration.cache.get('project_id', None)
valid = False
if project_id:
valid = True
self.add_note('project_id given: %s' % project_id)
else:
self.add_note('No project_id given.')
return valid
def _setup_resources(self):
neutron = openstack_clients.get_neutronclient()
project_id = self.action.registration.cache['project_id']
if not self.get_cache('network_id'):
try:
network_body = {
"network": {
"name": self.defaults['network_name'],
'tenant_id': project_id,
"admin_state_up": True
}
}
network = neutron.create_network(body=network_body)
except Exception as e:
self.add_note(
"Error: '%s' while creating network: %s" %
(e, self.defaults['network_name']))
raise
self.set_cache('network_id', network['network']['id'])
self.add_note("Network %s created for project %s" %
(self.defaults['network_name'],
self.action.registration.cache['project_id']))
else:
self.add_note("Network %s already created for project %s" %
(self.defaults['network_name'],
self.action.registration.cache['project_id']))
if not self.get_cache('subnet_id'):
try:
subnet_body = {
"subnet": {
"network_id": self.get_cache('network_id'),
"ip_version": 4,
'tenant_id': project_id,
'dns_nameservers': self.defaults['DNS_NAMESERVERS'],
"cidr": self.defaults['SUBNET_CIDR']
}
}
subnet = neutron.create_subnet(body=subnet_body)
except Exception as e:
self.add_note(
"Error: '%s' while creating subnet" % e)
raise
self.set_cache('subnet_id', subnet['subnet']['id'])
self.add_note("Subnet created for network %s" %
self.defaults['network_name'])
else:
self.add_note("Subnet already created for network %s" %
self.defaults['network_name'])
if not self.get_cache('router_id'):
try:
router_body = {
"router": {
"name": self.defaults['router_name'],
"external_gateway_info": {
"network_id": self.defaults['public_network']
},
'tenant_id': project_id,
"admin_state_up": True
}
}
router = neutron.create_router(body=router_body)
except Exception as e:
self.add_note(
"Error: '%s' while creating router: %s" %
(e, self.defaults['router_name']))
raise
self.set_cache('router_id', router['router']['id'])
self.add_note("Router created for project %s" %
self.action.registration.cache['project_id'])
else:
self.add_note("Router already created for project %s" %
self.action.registration.cache['project_id'])
try:
interface_body = {
"subnet_id": self.get_cache('subnet_id')
}
neutron.add_interface_router(self.get_cache('router_id'),
body=interface_body)
except Exception as e:
self.add_note(
"Error: '%s' while attaching interface" % e)
raise
self.add_note("Interface added to router for subnet")
def _pre_approve(self):
# Not exactly valid, but not exactly invalid.
self.action.valid = True
self.action.save()
def _post_approve(self):
self.action.valid = self._validate()
self.action.save()
if self.setup_resources and self.valid:
self._setup_resources()
def _submit(self, token_data):
pass
class AddAdminToProject(BaseAction):
"""
Action to add 'admin' user to project for
monitoring purposes.
"""
def _validate(self):
project_id = self.action.registration.cache.get('project_id', None)
valid = False
if project_id:
valid = True
self.add_note('project_id given: %s' % project_id)
else:
self.add_note('No project_id given.')
return valid
def _pre_approve(self):
# Not yet exactly valid, but not exactly invalid.
self.action.valid = True
self.action.save()
def _post_approve(self):
self.action.valid = self._validate()
self.action.save()
if self.valid and not self.action.state == "completed":
id_manager = IdentityManager()
project = id_manager.get_project(
self.action.registration.cache['project_id'])
try:
user = id_manager.find_user(name="admin")
role = id_manager.find_role(name="admin")
id_manager.add_user_role(user, role, project.id)
except Exception as e:
self.add_note(
"Error: '%s' while adding admin to project: %s" %
(e, project.id))
raise
self.action.state = "completed"
self.action.save()
self.add_note(
'Admin has been added to %s.' %
self.action.registration.cache['project_id'])
def _submit(self, token_data):
pass
action_classes = {
'DefaultProjectResources': (DefaultProjectResources,
DefaultProjectResourcesSerializer),
'AddAdminToProject': (AddAdminToProject, None)
}
settings.ACTION_CLASSES.update(action_classes)

View File

@ -0,0 +1,19 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from rest_framework import serializers
class DefaultProjectResourcesSerializer(serializers.Serializer):
setup_resources = serializers.BooleanField(default=False)

17
tenant_setup/tests.py Normal file
View File

@ -0,0 +1,17 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.test import TestCase
# Create your tests here.

0
user_reg/__init__.py Normal file
View File

84
user_reg/middleware.py Normal file
View File

@ -0,0 +1,84 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from time import time
from logging import getLogger
from django.utils import timezone
class KeystoneHeaderUnwrapper(object):
"""
Middleware to build an easy to use dict of important data from
what the keystone wsgi middleware gives us.
"""
def process_request(self, request):
try:
token_data = {
'project_name': request.META['HTTP_X_PROJECT_NAME'],
'project_id': request.META['HTTP_X_PROJECT_ID'],
'roles': request.META['HTTP_X_ROLES'].split(','),
'username': request.META['HTTP_X_USER_NAME'],
'user_id': request.META['HTTP_X_USER_ID'],
'authenticated': request.META['HTTP_X_IDENTITY_STATUS']
}
except KeyError:
token_data = {}
request.keystone_user = token_data
class TestingHeaderUnwrapper(object):
"""
Replacement for the KeystoneHeaderUnwrapper for testing purposes.
"""
def process_request(self, request):
try:
token_data = {
'project_name': request.META['headers']['project_name'],
'project_id': request.META['headers']['project_id'],
'roles': request.META['headers']['roles'].split(','),
'username': request.META['headers']['username'],
'user_id': request.META['headers']['user_id'],
'authenticated': request.META['headers']['authenticated']
}
except KeyError:
token_data = {}
request.keystone_user = token_data
class RequestLoggingMiddleware(object):
"""
Middleware to log the requests and responses.
"""
def __init__(self):
self.logger = getLogger('django.request')
def process_request(self, request):
self.logger.info(
'(%s) - <%s> %s [%s]',
timezone.now(),
request.method,
request.META['REMOTE_ADDR'],
request.get_full_path()
)
request.timer = time()
def process_response(self, request, response):
self.logger.info(
'(%s) - <%s> [%s] - (%.1fs)',
timezone.now(),
response.status_code,
request.get_full_path(),
time() - request.timer
)
return response

170
user_reg/settings.py Normal file
View File

@ -0,0 +1,170 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Django settings for user_reg project.
For more information on this file, see
https://docs.djangoproject.com/en/1.7/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.7/ref/settings/
"""
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
import sys
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '+er!!4olta#17a=n%uotcazg2ncpl==yjog%1*o-(cr%zys-)!'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
TEMPLATE_DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'base',
'api_v1',
'tenant_setup',
)
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'user_reg.middleware.KeystoneHeaderUnwrapper',
'user_reg.middleware.RequestLoggingMiddleware'
)
if 'test' in sys.argv:
# modify MIDDLEWARE_CLASSES
MIDDLEWARE_CLASSES = list(MIDDLEWARE_CLASSES)
MIDDLEWARE_CLASSES.remove('user_reg.middleware.KeystoneHeaderUnwrapper')
MIDDLEWARE_CLASSES.append('user_reg.middleware.TestingHeaderUnwrapper')
ROOT_URLCONF = 'user_reg.urls'
WSGI_APPLICATION = 'user_reg.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Internationalization
# https://docs.djangoproject.com/en/1.7/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.7/howto/static-files/
STATIC_URL = '/static/'
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'INFO',
'class': 'logging.FileHandler',
'filename': 'reg_log.log',
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'INFO',
'propagate': True,
},
'keystonemiddleware': {
'handlers': ['file'],
'level': 'INFO',
'propagate': True,
},
},
}
# App apecific settings:
USERNAME_IS_EMAIL = True
# Keystone admin credentials:
KEYSTONE = {
'username': 'admin',
'password': 'openstack',
'project_name': 'admin',
'auth_url': "http://localhost:5000/v2.0"
}
DEFAULT_REGION = "REGION_ONE"
NETWORK_DEFAULTS = {
"REGION_ONE": {
'network_name': 'somenetwork',
'subnet_name': 'somesubnet',
'router_name': 'somerouter',
# this depends on region and needs to be pulled from somewhere:
'public_network': '1eb739bb-607d-4a34-a590-9c15d03ccbe7',
'DNS_NAMESERVERS': ['193.168.1.2',
'193.168.1.3'],
'SUBNET_CIDR': '192.168.1.0/24'
}
}
# the order of the actions matters. These will run after the default action,
# in the given order.
API_ACTIONS = {'CreateProject': ['AddAdminToProject',
'DefaultProjectResources']}
# This is populated from the various model modules at startup:
ACTION_CLASSES = {}

26
user_reg/urls.py Normal file
View File

@ -0,0 +1,26 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls import patterns, include, url
from django.contrib import admin
urlpatterns = patterns(
'',
# Examples:
# url(r'^$', 'user_reg.views.home', name='home'),
# url(r'^blog/', include('blog.urls')),
url(r'^admin/', include(admin.site.urls)),
url(r'^api_v1/', include('api_v1.urls')),
)

47
user_reg/wsgi.py Normal file
View File

@ -0,0 +1,47 @@
# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
WSGI config for user_reg project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
"""
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "user_reg.settings")
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
from django.conf import settings
from urlparse import urlparse
from keystonemiddleware.auth_token import AuthProtocol
identity_url = urlparse(settings.KEYSTONE['auth_url'])
conf = {
'admin_user': settings.KEYSTONE['username'],
'admin_password': settings.KEYSTONE['password'],
'admin_tenant_name': settings.KEYSTONE['project_name'],
'auth_host': identity_url.hostname,
'auth_port': identity_url.port,
'auth_protocol': identity_url.scheme,
'delay_auth_decision': True,
'include_service_catalog': False
}
application = AuthProtocol(application, conf)