Skip to content

Commit

Permalink
Merge branch 'master' into master-github
Browse files Browse the repository at this point in the history
  • Loading branch information
dabeeeenster committed Aug 13, 2020
2 parents ae57682 + d8f73a2 commit bf2373b
Show file tree
Hide file tree
Showing 62 changed files with 1,756 additions and 679 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ checkstyle.txt
.env
.direnv
.envrc
.elasticbeanstalk/
4 changes: 2 additions & 2 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ deploydevelop:
- develop

deployawsstaging:
image: twstuart/elasticbeanstalk-pipenv
image: bullettrain/elasticbeanstalk-pipenv
stage: deploy-aws
script:
- export AWS_ACCESS_KEY_ID=$AWS_STAGING_ACCESS_KEY_ID
Expand All @@ -45,7 +45,7 @@ deployawsstaging:
- staging

deployawsmaster:
image: twstuart/elasticbeanstalk-pipenv
image: bullettrain/elasticbeanstalk-pipenv
stage: deploy-aws
script:
- export DATABASE_URL=$DATABASE_URL_PRODUCTION
Expand Down
5 changes: 2 additions & 3 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pylint = "*"
"autopep8" = "*"
pytest = "*"
pytest-django = "*"
django-test-migrations = "*"

[packages]
appdirs = "*"
Expand All @@ -33,9 +34,7 @@ sendgrid-django = "*"
psycopg2-binary = "*"
coreapi = "*"
Django = "<3.0"
numpy = "*"
django-simple-history = "*"
twisted = {version = "*",extras = ["tls"]}
django-debug-toolbar = "*"
google-api-python-client = "*"
"oauth2client" = "*"
Expand All @@ -46,8 +45,8 @@ chargebee = "*"
python-http-client = "<3.2.0" # 3.2.0 is the latest but throws an error on installation saying that it's not found
django-health-check = "*"
django-storages = "*"
boto3 = "*"
django-environ = "*"
django-trench = "*"
djoser = "*"
influxdb-client = "*"
django-ordered-model = "*"
573 changes: 191 additions & 382 deletions Pipfile.lock

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ The application relies on the following environment variables to run:
* `INFLUXDB_URL`: The URL for your InfluxDB database
* `INFLUXDB_ORG`: The organisation string for your InfluxDB API call.
* `GA_TABLE_ID`: GA table ID (view) to query when looking for organisation usage
* `USE_S3_STORAGE`: 'True' to store static files in s3
* `AWS_STORAGE_BUCKET_NAME`: bucket name to store static files. Required if `USE_S3_STORAGE' is true.
* `AWS_S3_REGION_NAME`: region name of the static files bucket. Defaults to eu-west-2.
* `ALLOWED_ADMIN_IP_ADDRESSES`: restrict access to the django admin console to a comma separated list of IP addresses (e.g. `127.0.0.1,127.0.0.2`)
Expand Down
5 changes: 5 additions & 0 deletions src/api/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from rest_framework import serializers


class ErrorSerializer(serializers.Serializer):
message = serializers.CharField()
5 changes: 5 additions & 0 deletions src/app/middleware.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from django.conf import settings
from django.core.exceptions import PermissionDenied

from util.logging import get_logger

logger = get_logger(__name__)


class AdminWhitelistMiddleware:
def __init__(self, get_response):
Expand All @@ -12,6 +16,7 @@ def __call__(self, request):
ip = x_forwarded_for.split(',')[0] if x_forwarded_for else request.META.get('REMOTE_ADDR')
if settings.ALLOWED_ADMIN_IP_ADDRESSES and ip not in settings.ALLOWED_ADMIN_IP_ADDRESSES:
# IP address not allowed!
logger.info('Denying access to admin for ip address %s' % ip)
raise PermissionDenied()

return self.get_response(request)
30 changes: 17 additions & 13 deletions src/app/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.9/ref/settings/
"""
import logging
import os
import warnings
from importlib import reload
Expand All @@ -29,6 +28,8 @@
PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))

ENV = env('ENVIRONMENT', default='local')
if ENV not in ('local', 'dev', 'staging', 'production'):
warnings.warn('ENVIRONMENT env variable must be one of local, dev, staging or production')

if 'DJANGO_SECRET_KEY' not in os.environ:
secret_key_gen()
Expand Down Expand Up @@ -106,6 +107,9 @@
# health check plugins
'health_check',
'health_check.db',

# Used for ordering models (e.g. FeatureSegment)
'ordered_model',
]

if GOOGLE_ANALYTICS_KEY or INFLUXDB_TOKEN:
Expand All @@ -125,7 +129,10 @@
),
'PAGE_SIZE': 10,
'UNICODE_JSON': False,
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination'
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'DEFAULT_THROTTLE_RATES': {
'login': '1/s'
}
}

MIDDLEWARE = [
Expand All @@ -147,7 +154,9 @@
if INFLUXDB_TOKEN:
MIDDLEWARE.append('analytics.middleware.InfluxDBMiddleware')

if ENV != 'local':
ALLOWED_ADMIN_IP_ADDRESSES = env.list('ALLOWED_ADMIN_IP_ADDRESSES', default=list())
if len(ALLOWED_ADMIN_IP_ADDRESSES) > 0:
warnings.warn('Restricting access to the admin site for ip addresses %s' % ', '.join(ALLOWED_ADMIN_IP_ADDRESSES))
MIDDLEWARE.append('app.middleware.AdminWhitelistMiddleware')

ROOT_URLCONF = 'app.urls'
Expand Down Expand Up @@ -320,16 +329,6 @@
}
}

if env.bool('USE_S3_STORAGE', default=False):
STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
AWS_STORAGE_BUCKET_NAME = os.environ['AWS_STORAGE_BUCKET_NAME']
AWS_S3_REGION_NAME = os.environ.get('AWS_S3_REGION_NAME', 'eu-west-2')
AWS_LOCATION = 'static'
AWS_DEFAULT_ACL = 'public-read'
AWS_S3_ADDRESSING_STYLE = 'virtual'

ALLOWED_ADMIN_IP_ADDRESSES = env.list('ALLOWED_ADMIN_IP_ADDRESSES', default=list())

LOG_LEVEL = env.str('LOG_LEVEL', 'WARNING')

TRENCH_AUTH = {
Expand Down Expand Up @@ -368,3 +367,8 @@
'user_list': ['custom_auth.permissions.CurrentUser'],
}
}


# Github OAuth credentials
GITHUB_CLIENT_ID = env.str('GITHUB_CLIENT_ID', '')
GITHUB_CLIENT_SECRET = env.str('GITHUB_CLIENT_SECRET', '')
2 changes: 1 addition & 1 deletion src/app/settings/master.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@
REST_FRAMEWORK['PAGE_SIZE'] = 999

SECURE_SSL_REDIRECT = True
SECURE_REDIRECT_EXEMPT = [r'^/$', r'^$'] # root is exempt as it's used for EB health checks
SECURE_REDIRECT_EXEMPT = [r'^health$'] # /health is exempt as it's used for EB health checks
2 changes: 1 addition & 1 deletion src/app/settings/staging.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@
REST_FRAMEWORK['PAGE_SIZE'] = 999

SECURE_SSL_REDIRECT = True
SECURE_REDIRECT_EXEMPT = [r'^/$', r'^$'] # root is exempt as it's used for EB health checks
SECURE_REDIRECT_EXEMPT = [r'^health$'] # /health is exempt as it's used for EB health checks
5 changes: 0 additions & 5 deletions src/app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,5 @@
if settings.DEBUG:
import debug_toolbar
urlpatterns = [
# Django 2
# path('__debug__/', include(debug_toolbar.urls)),

# For django versions before 2.0:
url(r'^__debug__/', include(debug_toolbar.urls)),

] + urlpatterns
14 changes: 13 additions & 1 deletion src/audit/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
FEATURE_UPDATED_MESSAGE = "Flag / Remote Config updated: %s"
SEGMENT_CREATED_MESSAGE = "New Segment created: %s"
SEGMENT_UPDATED_MESSAGE = "Segment updated: %s"
FEATURE_SEGMENT_UPDATED_MESSAGE = "Segment rules updated for flag: %s"
FEATURE_SEGMENT_UPDATED_MESSAGE = "Segment rules updated for flag: %s in environment: %s"
ENVIRONMENT_CREATED_MESSAGE = "New Environment created: %s"
ENVIRONMENT_UPDATED_MESSAGE = "Environment updated: %s"
FEATURE_STATE_UPDATED_MESSAGE = "Flag state / Remote Config value updated for feature: %s"
Expand Down Expand Up @@ -45,3 +45,15 @@ class Meta:

def __str__(self):
return "Audit Log %s" % self.id

@classmethod
def create_record(cls, obj, obj_type, log_message, author, project=None, environment=None):
cls.objects.create(
related_object_id=obj.id,
related_object_type=obj_type.name,
log=log_message,
author=author,
project=project,
environment=environment
)

4 changes: 2 additions & 2 deletions src/audit/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@

from audit.models import AuditLog
from audit.serializers import AuditLogSerializer
from util.logging import get_logger
from webhooks.webhooks import call_organisation_webhooks, WebhookEventType

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
logger = get_logger(__name__)


@receiver(post_save, sender=AuditLog)
Expand Down
10 changes: 10 additions & 0 deletions src/custom_auth/oauth/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class GithubError(Exception):
pass


class GoogleError(Exception):
pass


class OAuthError(Exception):
pass
78 changes: 78 additions & 0 deletions src/custom_auth/oauth/github.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import requests
from django.conf import settings
from requests import RequestException

from custom_auth.oauth.exceptions import GithubError
from custom_auth.oauth.helpers.github_helpers import convert_response_data_to_dictionary, get_first_and_last_name
from util.logging import get_logger

GITHUB_API_URL = "https://api.github.com"
GITHUB_OAUTH_URL = "https://github.com/login/oauth"

NON_200_ERROR_MESSAGE = "Github returned {} status code when getting an access token."

logger = get_logger(__name__)


class GithubUser:
def __init__(self, code: str, client_id: str = None, client_secret: str = None):
self.client_id = client_id or settings.GITHUB_CLIENT_ID
self.client_secret = client_secret or settings.GITHUB_CLIENT_SECRET

self.access_token = self._get_access_token(code)
self.headers = {
"Authorization": f"token {self.access_token}"
}

def _get_access_token(self, code) -> str:
data = {
"code": code,
"client_id": self.client_id,
"client_secret": self.client_secret
}
response = requests.post(f"{GITHUB_OAUTH_URL}/access_token", data=data)

if response.status_code != 200:
raise GithubError(NON_200_ERROR_MESSAGE.format(response.status_code))

response_json = convert_response_data_to_dictionary(response.text)
if "error" in response_json:
error_message = response_json["error_description"].replace("+", " ")
raise GithubError(error_message)

return response_json["access_token"]

def get_user_info(self) -> dict:
# TODO: use threads?
try:
return {
**self._get_user_name_and_id(),
"email": self._get_primary_email()
}
except RequestException:
raise GithubError("Failed to communicate with the Github API.")

def _get_user_name_and_id(self):
user_response = requests.get(f"{GITHUB_API_URL}/user", headers=self.headers)
user_response_json = user_response.json()
full_name = user_response_json.get("name")
first_name, last_name = get_first_and_last_name(full_name) if full_name else ["", ""]
return {
"first_name": first_name,
"last_name": last_name,
"github_user_id": user_response_json.get("id")
}

def _get_primary_email(self):
emails_response = requests.get(f"{GITHUB_API_URL}/user/emails", headers=self.headers)

# response from github should be a list of dictionaries, this will find the first entry that is both verified
# and marked as primary (there should only be one).
primary_email_data = next(
filter(lambda email_data: email_data["primary"] and email_data["verified"], emails_response.json()), None
)

if not primary_email_data:
raise GithubError("User does not have a verified email address with Github.")

return primary_email_data["email"]
30 changes: 21 additions & 9 deletions src/custom_auth/oauth/google.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import requests
from requests import RequestException
from rest_framework import status

from custom_auth.oauth.exceptions import GoogleError

USER_INFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo?alt=json&"
NON_200_ERROR_MESSAGE = "Google returned {} status code when getting an access token."


def get_user_info(access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = requests.get(USER_INFO_URL, headers=headers)
response_json = response.json()
return {
"email": response_json["email"],
"first_name": response_json.get("given_name", ""),
"last_name": response_json.get("family_name", ""),
"google_user_id": response_json["id"]
}
try:
headers = {"Authorization": f"Bearer {access_token}"}
response = requests.get(USER_INFO_URL, headers=headers)

if response.status_code != status.HTTP_200_OK:
raise GoogleError(NON_200_ERROR_MESSAGE.format(response.status_code))

response_json = response.json()
return {
"email": response_json["email"],
"first_name": response_json.get("given_name", ""),
"last_name": response_json.get("family_name", ""),
"google_user_id": response_json["id"]
}
except RequestException:
raise GoogleError("Failed to communicate with the Google API.")
Empty file.
23 changes: 23 additions & 0 deletions src/custom_auth/oauth/helpers/github_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from custom_auth.oauth.exceptions import GithubError
from util.logging import get_logger

logger = get_logger(__name__)


def convert_response_data_to_dictionary(text: str) -> dict:
try:
response_data = {}
for key, value in [param.split("=") for param in text.split("&")]:
response_data[key] = value
return response_data
except ValueError:
logger.warning("Malformed data received from Github (%s)" % text)
raise GithubError("Malformed data received from Github")


def get_first_and_last_name(full_name: str) -> list:
if not full_name:
return ["", ""]

names = full_name.strip().split(" ")
return names if len(names) == 2 else [full_name, ""]
Loading

0 comments on commit bf2373b

Please sign in to comment.