Skip to content

Commit

Permalink
Merge pull request #101 from #64
Browse files Browse the repository at this point in the history
Cueobserve #64 Add Authentication Layer
  • Loading branch information
vincue authored Aug 16, 2021
2 parents ef44f8f + be07f7f commit 4e4b988
Show file tree
Hide file tree
Showing 34 changed files with 762 additions and 25 deletions.
8 changes: 8 additions & 0 deletions api/.env.dev
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,11 @@ export POSTGRES_DB_USERNAME="postgres"
export POSTGRES_DB_PASSWORD="postgres"
export POSTGRES_DB_SCHEMA="cue_observe"
export POSTGRES_DB_PORT=5432

## SUPERUSER'S VARIABLE
export DJANGO_SUPERUSER_USERNAME="User"
export DJANGO_SUPERUSER_PASSWORD="admin"
export DJANGO_SUPERUSER_EMAIL="admin@domain.com"

## AUTHENTICATION
export IS_AUTHENTICATION_REQUIRED=False ### Write either True or False not like (true or TRUE) and (false or FALSE)
2 changes: 2 additions & 0 deletions api/anomaly/tests/test_anomalyDefinition_views.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import pytest
import unittest
from unittest import mock
import json
from django.test import TestCase
from django.urls import reverse
from mixer.backend.django import mixer
from anomaly.models import AnomalyDefinition
from anomaly.services.anomalyDefinitions import AnomalyDefinitions


@pytest.mark.django_db(transaction=True)
def test_anomalyDefinition(client, mocker):
"""
Expand Down
1 change: 1 addition & 0 deletions api/anomaly/tests/test_anomaly_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from anomaly.services.anomalys import Anomalys
from anomaly.models import AnomalyDefinition, Anomaly


@pytest.mark.django_db(transaction=True)
def test_anomalys(client, mocker):
schedule = mixer.blend("anomaly.customSchedule", name="Test Schedule")
Expand Down
1 change: 1 addition & 0 deletions api/anomaly/tests/test_connection_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ def test_connections(client, mocker):
"""
Test case for connections
"""

# Add Connection
connectionType = mixer.blend("anomaly.connectionType")
ConnectionParam = mixer.blend("anomaly.ConnectionParam", connectionType = connectionType, name="file")
Expand Down
2 changes: 1 addition & 1 deletion api/anomaly/tests/test_dataset_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
from django.urls import reverse
from mixer.backend.django import mixer


@pytest.mark.django_db(transaction=True)
def test_datasets(client, mocker):

# Get dataset when no entry
path = reverse('datasets')
response = client.get(path)
Expand Down
1 change: 1 addition & 0 deletions api/anomaly/tests/test_runstatus_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def test_runstatus(client, mocker):
"""
Test cases for anomalyDefinition
"""

anomalyDef = mixer.blend("anomaly.AnomalyDefinition", periodicTask=None)
runStatus = mixer.blend("anomaly.RunStatus", anomalyDefinition=anomalyDef, status="RUNNING")

Expand Down
1 change: 0 additions & 1 deletion api/anomaly/tests/test_schedule_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

@pytest.mark.django_db(transaction=True)
def test_schedules(client, populate_seed_data, mocker):

# Create schedule test
path = reverse('scheduleView')
data = {'name': 'Schedule at 3 AM ',
Expand Down
3 changes: 0 additions & 3 deletions api/anomaly/tests/test_setting_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@
from utils.apiResponse import ApiResponse


from anomaly.models import Setting

@pytest.mark.django_db(transaction=True)
def test_setting(client, mocker):

# Create setting test
path = reverse('settings')
res = ApiResponse("Successfully tested setting")
Expand Down
67 changes: 67 additions & 0 deletions api/app/middlewares.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from django.conf import settings
from django.shortcuts import redirect
from django.http import HttpResponseForbidden
from django.contrib.auth import logout
from django.utils.deprecation import MiddlewareMixin
from django.contrib.auth.decorators import login_required

class DisableCsrfCheck(MiddlewareMixin):
"""
Middleware class to disable CSRF check for views
"""

def process_request(self, req):
"""
Process request method to add attr _dont_enforce_csrf_checks
"""
attr = "_dont_enforce_csrf_checks"
if not getattr(req, attr, False):
setattr(req, attr, True)


class LoginRequiredMiddleware:
"""
Middleware class to enforce login for every view
"""

def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
return self.get_response(request)

def process_view(self, request, view_func, view_args, view_kwargs): # pylint: disable=C0103
"""
Process view method to check login_exempt decorator and enforce authentication on all views
"""
try:
appName = request.path.split("/")[1]
if appName in ["admin", "accounts"]:
return
except:
pass
authenticationRequired= True if settings.AUTHENTICATION_REQUIRED == "True" else False
if not authenticationRequired:
return
if getattr(view_func, "login_exempt", False):
return

if not request.user.is_authenticated and request.path.split("/")[2] == "datadownload":
return redirect(settings.LOGIN_REDIRECT_URL)

if request.user.is_authenticated:
if request.user.status != "Active":
logout(request)
return

return login_required(view_func)(request, *view_args, **view_kwargs)




def login_exempt(view): # pylint: disable=C0103
"""
Decorator for views which needs to be exempted from login
"""
view.login_exempt = True
return view
29 changes: 25 additions & 4 deletions api/app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get("DEBUG", True)

SITE_ID = 1

CSRF_TRUSTED_ORIGINS = ["localhost"]
CORS_ORIGIN_WHITELIST = ["http://localhost:3000"]
CORS_ALLOW_CREDENTIALS = True
ALLOWED_HOSTS = ["*", "localhost"]
CORS_ORIGIN_ALLOW_ALL = True
HTTP_HTTPS = "http://"
ROOT_URLCONF = "app.urls"

# Application definition
INSTALLED_APPS = [
Expand All @@ -43,10 +44,17 @@
"django.contrib.messages",
"django.contrib.staticfiles",
"corsheaders",
"allauth",
'django.contrib.sites',
"allauth.account",
"allauth.socialaccount",
"allauth.socialaccount.providers.google",
"django_celery_beat",
"anomaly",
"users",
"rest_framework",
]
AUTH_USER_MODEL = "users.CustomUser"

MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
Expand All @@ -57,10 +65,23 @@
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"app.middlewares.DisableCsrfCheck",
"app.middlewares.LoginRequiredMiddleware",
# "app.middlewares.RestrictApiMiddleware"
]

ROOT_URLCONF = "app.urls"

AUTHENTICATION_BACKENDS = (
# Needed to login by username in Django admin, regardless of `allauth`
"django.contrib.auth.backends.ModelBackend",
# `allauth` specific authentication methods, such as login by e-mail
"allauth.account.auth_backends.AuthenticationBackend",
)

AUTHENTICATION_REQUIRED=os.environ.get("IS_AUTHENTICATION_REQUIRED", False)
ACCOUNT_AUTHENTICATION_METHOD = "email"
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
Expand Down
2 changes: 2 additions & 0 deletions api/app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@

urlpatterns = [
path('admin/', admin.site.urls),
path("accounts/", include("allauth.urls")),
path("api/anomaly/", include("anomaly.urls")),
path("api/", include("users.urls")),
]
1 change: 1 addition & 0 deletions api/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
django-allauth==0.41.0
amqp==5.0.6
asgiref==3.4.0
astroid==2.5.6
Expand Down
4 changes: 2 additions & 2 deletions api/start_server.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# start-server.sh
if [ -n "$DJANGO_SUPERUSER_USERNAME" ] && [ -n "$DJANGO_SUPERUSER_PASSWORD" ] ; then
(python manage.py createsuperuser --no-input)
if [ -n "$DJANGO_SUPERUSER_USERNAME" ] && [ -n "$DJANGO_SUPERUSER_EMAIL" ] && [ -n "$DJANGO_SUPERUSER_PASSWORD" ] ; then
(python manage.py createsuperuser --noinput)
fi
redis-server &
export DEBUG=false
Expand Down
Empty file added api/users/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions api/users/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.contrib import admin

# Register your models here.
6 changes: 6 additions & 0 deletions api/users/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class UsersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'users'
45 changes: 45 additions & 0 deletions api/users/managers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from django.contrib.auth.base_user import BaseUserManager
from django.utils.translation import ugettext_lazy as _
from django.db.models import Q


class CustomUserManager(BaseUserManager):
"""
Custom user model manager where email is the unique identifiers
for authentication instead of usernames.
"""

def create_user(self, email, password, **extra_fields):
"""
Create and save a User with the given email and password.
"""
if not email:
raise ValueError(_("The Email must be set"))
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.status = "Active"
user.save()
return user

def create_superuser(self, email, password, **extra_fields):
"""
Create and save a SuperUser with the given email and password.
"""
# extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
extra_fields.setdefault("is_active", True)

# if extra_fields.get("is_staff") is not True:
# raise ValueError(_("Superuser must have is_staff=True."))
if extra_fields.get("is_superuser") is not True:
raise ValueError(_("Superuser must have is_superuser=True."))
return self.create_user(email, password, **extra_fields)

def get_queryset(self):
"""returns object.all() without Bot in it"""
return super().get_queryset().filter(~Q(email="Bot@cuebook.ai"))

def get_bot(self):
"""returns bot which cannot be accessd through querySet"""
return super().get_queryset().get(email="Bot@cuebook.ai")
34 changes: 34 additions & 0 deletions api/users/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 3.2.5 on 2021-08-09 06:54

from django.db import migrations, models
import users.models


class Migration(migrations.Migration):

initial = True

dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]

operations = [
migrations.CreateModel(
name='CustomUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('email', users.models.LowercaseEmailField(max_length=254, unique=True, verbose_name='email address')),
('name', models.CharField(default='User', max_length=200)),
('is_active', models.BooleanField(default=False)),
('status', models.CharField(default='Inactive', max_length=20)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
],
options={
'abstract': False,
},
),
]
Empty file.
36 changes: 36 additions & 0 deletions api/users/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, Group
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from datetime import datetime
from .managers import CustomUserManager
from django.contrib.postgres.fields import ArrayField

# Create your models here.

class LowercaseEmailField(models.EmailField):
"""
Override EmailField to convert emails to lowercase before saving.
"""

def convertToLowerCase(self, value):
"""
Convert email to lowercase.
"""
value = super(LowercaseEmailField, self).convertToLowerCase(value)
# Value can be None so check that it's a string before lowercasing.
if isinstance(value, str):
return value.lower()
return value

class CustomUser(AbstractBaseUser, PermissionsMixin):
email = LowercaseEmailField(_("email address"), unique=True)
name = models.CharField(max_length=200, null=False, default="User")
is_active = models.BooleanField(default=False)
status = models.CharField(max_length=20, default="Inactive")

USERNAME_FIELD = "email"
REQUIRED_FIELDS = []
objects = CustomUserManager()
def __str__(self):
return self.email
3 changes: 3 additions & 0 deletions api/users/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.test import TestCase

# Create your tests here.
Loading

0 comments on commit 4e4b988

Please sign in to comment.