Skip to content
This repository was archived by the owner on Nov 17, 2020. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4bcfe2f
Add MinLengthValidator to required at least one hostname
menecio Jul 2, 2017
427fbbe
Add basic test cases for Api objects
menecio Jul 2, 2017
c8a45d9
Use UUID.int to format key
menecio Jul 2, 2017
488c410
Remove all commented lines from settings
menecio Jul 2, 2017
3609e43
Add test cases for Api details view
menecio Jul 2, 2017
993838c
Add test cases for Consumer object
menecio Jul 2, 2017
14a4b14
Add test cases for ConsumerKey object
menecio Jul 2, 2017
9288c1b
Set 8 chars as minimum length for ConsumerKey.key value
menecio Jul 2, 2017
f13bc6a
Add test cases for key generation and length validation
menecio Jul 2, 2017
d4b5ace
Change allowed methods for add_plugin
menecio Jul 2, 2017
cca73f9
Add plugins test cases
menecio Jul 2, 2017
9cd906e
Remove unnecesary method
menecio Jul 2, 2017
49eea65
Provide a default content-type and get only non-empty headers
menecio Jul 2, 2017
6ced43d
Change validation, use specific field validation
menecio Jul 2, 2017
9f02c92
Fix class naming
menecio Jul 2, 2017
cd7b163
Add request bouncer test cases
menecio Jul 2, 2017
8eb41ef
Send headers on prepared request
menecio Jul 2, 2017
72308ca
Fix setting special headers before making the request. Taking the
menecio Jul 2, 2017
aaeb16d
Add test cases for KeyAuthMiddleware, Test apikey authentication
menecio Jul 2, 2017
df4fdbe
Fix lint tests
menecio Jul 2, 2017
309610c
Add flake8 test to matrix
menecio Jul 2, 2017
10e3d38
Fix key validation if key_in_body is set
menecio Jul 2, 2017
d03fd4a
Add test case for key_in_body configuration
menecio Jul 2, 2017
c0d91ce
Fix consumer_key verification, improve code.
menecio Jul 2, 2017
10d293c
Fix settings and add test case when plugin is set to key_in_body
menecio Jul 2, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ language: python

services:
- postgresql

addons:
postgresql: "9.4"

Expand All @@ -24,6 +24,8 @@ matrix:
env: TOXENV=py36-django111
- python: "3.6"
env: TOXENV=py36-djangomaster
- python: "3.5"
env: TOXENV="flake8"
exclude:
- python: "3.5"
env: TOXENV=py36-django111
Expand All @@ -41,13 +43,13 @@ cache:

install:
- pip install coveralls tox

before_script:
- psql -c 'create database travis_ci_test;' -U postgres

script:
- tox

after_script:
- coveralls

Expand Down
45 changes: 30 additions & 15 deletions api_bouncer/middlewares/key_auth.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import json

from django.http import JsonResponse

from ..models import (
Expand All @@ -19,37 +21,50 @@ def __call__(self, request):

if plugin_conf:
config = plugin_conf.config
apikey = self.get_key(request, config['key_names'])

if not self.verify_key(request, config, apikey):
apikey = self.get_key(
request,
config['key_names'],
key_in_body=config['key_in_body']
)
consumer_key = self.verify_key(request, config, apikey)
if not consumer_key:
return JsonResponse(
data={'error': 'Unauthorized'},
status=403
)
if not config['hide_credentials']:
request.META.update({
'HTTP_X_CONSUMER_USERNAME': consumer_key.consumer.username,
'HTTP_X_CONSUMER_ID': str(consumer_key.consumer.id),
})
else:
# Remove apikey from headers
for k in config['key_names']:
request.META.pop(k, None)

response = self.get_response(request)

return response

def verify_key(self, request, config, key):
if not key and config.get('key_in_body'):
key = request.body.get('key')

c_key = (
apikey = (
ConsumerKey.objects
.select_related('consumer')
.filter(key=key).first()
)
return apikey

if c_key:
request.META['X-Consumer-Username'] = c_key.consumer.username
request.META['X-Consumer-Id'] = c_key.consumer.id

return True

return False
def get_key(self, request, key_names, key_in_body=False):
if key_in_body:
try:
body = json.loads(request.body.decode('utf-8'))
for k in key_names:
if k in body:
return body[k]
return None
except json.JSONDecodeError:
return None

def get_key(self, request, key_names):
for n in key_names:
name = n.upper().replace('-', '_')
key_name = 'HTTP_{0}'.format(name)
Expand Down
15 changes: 12 additions & 3 deletions api_bouncer/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
ArrayField,
JSONField,
)
from django.core.validators import RegexValidator
from django.core.validators import (
MinLengthValidator,
RegexValidator,
)
from django.db import models


Expand All @@ -30,7 +33,10 @@ class Api(models.Model):
validators=[
RegexValidator(regex=FQDN_REGEX),
]
)
),
validators=[
MinLengthValidator(1, message='At least one is required'),
]
)
upstream_url = models.URLField(null=False)

Expand Down Expand Up @@ -59,7 +65,10 @@ class ConsumerKey(models.Model):
key = models.CharField(
max_length=200,
blank=False,
null=False
null=False,
validators=[
MinLengthValidator(8),
]
)

class Meta:
Expand Down
17 changes: 6 additions & 11 deletions api_bouncer/serializers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import uuid

import jsonschema

from rest_framework import serializers
Expand Down Expand Up @@ -40,7 +41,7 @@ class Meta:
def validate_key(self, value):
"""Verify if no key is given and generate one"""
if not value:
value = str(uuid.uuid4()).replace('-', '')
value = str(uuid.uuid4().int)
return value


Expand Down Expand Up @@ -74,9 +75,6 @@ def validate(self, data):

return data

def process_headers(self, headers={}):
return headers


class ApiSerializer(serializers.ModelSerializer):
plugins = PluginSerializer(
Expand All @@ -94,11 +92,8 @@ class BouncerSerializer(serializers.Serializer):
api = serializers.CharField(allow_blank=False, allow_null=False)
headers = serializers.DictField(child=serializers.CharField())

def validate(self, data):
api = Api.objects.get(name=data['api'])
def validate_api(self, value):
api = Api.objects.get(name=value)
if not api:
raise serializers.ValidationError({
'api': 'Unknown API',
})

return data
raise serializers.ValidationError('Unknown API')
return value
28 changes: 15 additions & 13 deletions api_bouncer/views.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import re

from requests import Request, Session
from django.http import HttpResponse, JsonResponse
from requests import Request, Session
from rest_framework import (
mixins,
permissions,
response,
viewsets,
status,
viewsets,
)
from rest_framework.decorators import detail_route

Expand All @@ -24,19 +24,20 @@
from .serializers import (
ApiSerializer,
BouncerSerializer,
ConsumerSerializer,
ConsumerKeySerializer,
ConsumerSerializer,
PluginSerializer,
)


def api_bouncer(request):
def get_headers(meta):
"""Get all headers beginning with HTTP_ and that have a value"""
regex = re.compile(r'^HTTP_')
return {
(regex.sub('', k)).replace('_', '-'): v
for k, v in meta.items()
if k.startswith('HTTP_')
if k.startswith('HTTP_') and v
}

dest_host = request.META.get('HTTP_HOST')
Expand All @@ -57,11 +58,12 @@ def get_headers(meta):
request.method,
url,
params=request.GET,
data=request.POST
data=request.POST,
headers=serializer.data['headers']
)
prepped = session.prepare_request(req)
resp = session.send(prepped)
content_type = resp.headers['content-type']
content_type = resp.headers.get('content-type', 'text/html')

return HttpResponse(
content=resp.content,
Expand All @@ -85,12 +87,13 @@ class ApiViewSet(viewsets.ModelViewSet):
lookup_field = 'name'

@detail_route(
methods=['patch', 'put'],
methods=['post'],
permission_classes=[permissions.IsAdminUser],
url_path='plugins'
)
def add_plugin(self, request, name=None):
api = self.get_object()

plugin_name = request.data.get('name')
plugin_conf = request.data.get('config')

Expand All @@ -108,21 +111,20 @@ def add_plugin(self, request, name=None):
api_plugin_conf.update(plugin_conf)

data = {
'api': api.id,
'api': api,
'name': plugin_name,
'config': api_plugin_conf,
}

if api_plugin:
serializer = PluginSerializer(api_plugin, data=data)
else:
serializer = PluginSerializer(data=data)
if not api_plugin:
api_plugin = Plugin(**data)

serializer = PluginSerializer(api_plugin, data=data)
if serializer.is_valid():
serializer.save()
return response.Response(
serializer.data,
status=status.HTTP_201_CREATED
status=status.HTTP_200_OK
)

return response.Response(
Expand Down
2 changes: 1 addition & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@

# noqa
54 changes: 1 addition & 53 deletions tests/settings.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,13 @@
"""
Django settings for bouncer project.

Generated by 'django-admin startproject' using Django 1.11.2.

For more information on this file, see
https://docs.djangoproject.com/en/1.11/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/
"""

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'supersecret12345menecio67890'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['*']


# Application definition

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
Expand All @@ -45,7 +23,6 @@
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
Expand All @@ -72,10 +49,6 @@

WSGI_APPLICATION = 'bouncer.wsgi.application'


# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
Expand All @@ -86,28 +59,7 @@
}
}


# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]


# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/
AUTH_PASSWORD_VALIDATORS = []

LANGUAGE_CODE = 'en-us'

Expand All @@ -119,8 +71,4 @@

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/

STATIC_URL = '/static/'
Loading