Compare commits

...

11 Commits

Author SHA1 Message Date
Harikrishna Jiju bffba7ec30 feat: login and tests
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-08-23 16:13:18 +05:30
Alan Alexander Thomas 77b01cec1c Merge pull request 'chore: set app img to restart always and downgrade docker-compose version' (#4) from realaravinth/mystiq-sso:chore-docker-restart-compose-version into master
ci/woodpecker/push/woodpecker Pipeline was successful Details
Reviewed-on: mystiq/sso#4
2022-08-22 19:47:52 +05:30
Alan Alexander Thomas 8f62c31d49 Merge pull request 'local_settings.py: clean way to store deployment secrets' (#3) from realaravinth/mystiq-sso:wip-local-settings into master
ci/woodpecker/push/woodpecker Pipeline was successful Details
Reviewed-on: mystiq/sso#3
2022-08-22 19:46:44 +05:30
Aravinth Manivannan 729447e9d6 Merge pull request 'feat: registration' (#5) from wip-registration into master
ci/woodpecker/push/woodpecker Pipeline was successful Details
Reviewed-on: mystiq/sso#5
2022-08-22 19:26:43 +05:30
Aravinth Manivannan b7009ebf22
fix: requriements.txt
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline failed Details
2022-08-22 19:25:29 +05:30
Alan Alexander Thomas 6cd459fd5c feat: registration
ci/woodpecker/push/woodpecker Pipeline failed Details
ci/woodpecker/pr/woodpecker Pipeline failed Details
2022-08-22 19:12:29 +05:30
Aravinth Manivannan b5e1bc9d5e
chore: set app img to restart always and downgrade docker-compose version
ci/woodpecker/pr/woodpecker Pipeline failed Details
2022-08-22 17:53:13 +05:30
Aravinth Manivannan 2e9cc6aa7f
feat: use local_settings in docker-compose
ci/woodpecker/pr/woodpecker Pipeline failed Details
2022-08-22 17:32:31 +05:30
Aravinth Manivannan d1cb6b1b95
feat: local_settings to store secrets 2022-08-22 17:23:40 +05:30
Alan Alexander Thomas 33e9d830f8 Merge pull request 'feat: pkg and upload docker img' (#2) from realaravinth/mystiq-sso:wip-docker into master
ci/woodpecker/push/woodpecker Pipeline was successful Details
Reviewed-on: mystiq/sso#2
2022-08-22 15:13:20 +05:30
Aravinth Manivannan 5acf8683a9
feat: pkg and upload docker img
ci/woodpecker/pr/woodpecker Pipeline failed Details
2022-08-22 15:07:46 +05:30
18 changed files with 678 additions and 12 deletions

163
.dockerignore Normal file
View File

@ -0,0 +1,163 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
sso/local_settings.py
sso/local_settings/local_settings.py

2
.gitignore vendored
View File

@ -158,3 +158,5 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
sso/local_settings.py
sso/local_settings/local_settings.py

View File

@ -6,3 +6,12 @@ pipeline:
- make env
- make lint
- make coverage
publish:
image: plugins/docker
settings:
username: realaravinth
password:
from_secret: DOCKER_TOKEN
repo: realaravinth/mystiq-sso
tags: latest

10
Dockerfile Normal file
View File

@ -0,0 +1,10 @@
FROM python:3
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
#RUN useradd -ms /bin/bash -u 1001 mystiq-sso
#USER mystiq-sso
WORKDIR /code
LABEL org.opencontainers.image.source https://git.batsense.net/mystiq/sso
COPY requirements.txt /code/
RUN pip install -r requirements.txt
COPY . /code/

View File

@ -7,15 +7,23 @@ default: ## Run app
$(call run_migrations)
. ./venv/bin/activate && python manage.py runserver
env: ## setup environment
@-virtualenv venv
. ./venv/bin/activate && pip install -r requirements.txt
coverage: ## Generate test coverage report
. ./venv/bin/activate && coverage run manage.py test
. ./venv/bin/activate && coverage report -m
. ./venv/bin/activate && coverage html
docker: ## Build Docker image
docker build -t realaravinth/mystiq-sso:master -t realaravinth/mystiq-sso:latest .
docker-publish: docker ## Build and publish Docker image
docker push realaravinth/mystiq-sso:master
docker push realaravinth/mystiq-sso:latest
env: ## setup environment
@-virtualenv venv
. ./venv/bin/activate && pip install -r requirements.txt
freeze: ## Freeze python dependencies
@. ./venv/bin/activate && pip freeze > requirements.txt

View File

@ -1,6 +1,5 @@
[![status-badge](https://ci.batsense.net/api/badges/mystiq/sso/status.svg)](https://ci.batsense.net/mystiq/sso)
[![Docker](https://img.shields.io/docker/pulls/realaravinth/mystiq-sso)](https://hub.docker.com/r/realaravinth/mystiq-sso)
# SSO

View File

@ -0,0 +1,46 @@
{% comment %} Login Page {% endcomment %}
{% block login %}
<h2>Log In</h2>
<form method="post" action="">
{% csrf_token %} {{ form.as_p }}
<label class="form__label" for="email">Email</label><br/>
<input
type="text"
id="email"
name="email"
autofocus
required/><br/><br/>
<label class="form__label" for="pwd">Password</label><br/>
<input
type="password"
required
id="password"
name="password"/><br/><br/>
<button type="submit">Log In</button>
<input type="hidden" name="next" value="{{ next }}">
{% for message in messages %}
<p id="messages">{{message}}</p>
{% endfor %}
</form>
{% comment %} <div class="form-group">
<label for="exampleInputEmail1">Email address</label>
<input type="email" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp" placeholder="Enter email">
<small id="emailHelp" class="form-text text-muted">We'll never share your email with anyone else.</small>
</div>
<div class="form-group">
<label for="exampleInputPassword1">Password</label>
<input type="password" class="form-control" id="exampleInputPassword1" placeholder="Password">
</div>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="exampleCheck1">
<label class="form-check-label" for="exampleCheck1">Check me out</label>
</div>
<button type="submit" class="btn btn-primary">Submit</button> {% endcomment %}
<p class="form__alt-action">
New to Mystiq?
<a href="{% url 'accounts:register' %}">Create an account</a>
</p>
{% endblock %}

View File

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Registration</title>
</head>
<body>
<!-- <form action="{% url 'accounts:register' %}" method="post">
{% csrf_token %}
<fieldset>
<legend><h1>{{ Registration }}</h1></legend>
</fieldset>
<fieldset>
<label for="email">
<b>Email</b>
<input type="text" placeholder="Enter Email" name="email" id="email" required>
</label>
</fieldset>
<fieldset>
<label for="psw">
<b>Password</b>
<input type="password" placeholder="Enter Password" name="psw" id="psw" required>
</label>
</fieldset>
<fieldset>
<label for="cnf-psw">
<b>Password</b>
<input type="password" placeholder="Confirn Password" name="cnf-psw" id="cnf-psw" required>
</label>
</fieldset>
<input type="submit" value="register">
</form> -->
<form action="{% url 'accounts:register' %}" method="post" class="form" accept-charset="utf-8">
{% if error %}
<div class="error__container">
<h3 class="error__title">ERROR: {{ error.title }}</h3>
<p class="error__message">{{ error.reason }}</p>
</div>
{% endif %}
{% csrf_token %}
<div class="container">
<h1>Register</h1>
<p>Welcome to mystiq registration.</p>
<hr>
<label for="email">
<b>Email</b>
<input
type="text"
placeholder="Enter Email"
name="email"
id="email"
required
{% if email %}
value={{ username }}
{% endif %}
>
</label>
<label for="password">
<b>Password</b>
<input type="password" placeholder="Enter Password" name="password" id="password" required>
</label>
<label for="password-confirm">
<b>Confirm Password</b>
<input type="password" placeholder="Confirm Password" name="password-confirm" id="password-confirm" required>
</label>
<hr>
<p>By creating an account you agree to our <a href="#">Terms & Privacy</a>.</p>
<button type="submit" class="form_submit">Register</button>
</div>
<div class="container signin">
<p>Already have an account? <a href="{% url 'accounts:login' %}">Sign in</a>.</p>
</div>
</form>
</body>
</html>

View File

@ -0,0 +1 @@
<b> Login Successful! <b>

View File

@ -1,6 +1,7 @@
# Create your tests here.
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
# Copyright © 2022 Alan Alexander Thomas <alan2000alex@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@ -36,10 +37,9 @@ from oauth2_provider.models import get_application_model
def register_util(t: TestCase, username: str):
t.password = "asdklfja;ldkfja;df"
t.username = username
t.email = f"{t.username}@example.org"
t.email = f"{username}@vitap.ac.in"
t.user = get_user_model().objects.create(
username=t.username,
username=username,
email=t.email,
)
t.user.set_password(t.password)
@ -48,10 +48,11 @@ def register_util(t: TestCase, username: str):
def login_util(t: TestCase, c: Client, redirect_to: str):
payload = {
"login": t.username,
"email": t.email,
"password": t.password,
}
resp = c.post(reverse("accounts.login"), payload)
resp = c.post(reverse("accounts:login"), payload)
print(resp.content)
t.assertEqual(resp.status_code, 302)
t.assertEqual(resp.headers["location"], reverse(redirect_to))
@ -102,3 +103,82 @@ class CreateOidCApplicaiton(TestCase):
self.assertEqual(app.client_type, "confidential")
self.assertEqual(app.authorization_grant_type, "authorization-code")
self.assertEqual(app.algorithm, "HS256")
class RegistrationTest(TestCase):
def setUp(self):
self.username = "register_user"
self.password = "2i3j4;1qlk2asdf"
self.email = "register_user@vitap.ac.in"
def test_register_template_works(self):
"""
Tests if register template renders
"""
resp = self.client.get(reverse("accounts:register"))
self.assertEqual(b"Register" in resp.content, True)
def test_register_works(self):
"""
Tests if register works
"""
c = Client()
# passwords don't match
msg = {
"password": self.password,
"email": self.email,
"password-confirm": self.email,
}
resp = c.post(reverse("accounts:register"), msg)
self.assertEqual(resp.status_code, 400)
# register user
msg["password-confirm"] = self.password
resp = c.post(reverse("accounts:register"), msg)
self.assertEqual(resp.status_code, 200)
user = get_user_model().objects.get(username=self.username)
# duplicate email
resp = c.post(reverse("accounts:register"), msg)
self.assertEqual(resp.status_code, 400)
self.assertEqual(b"This email is already registered." in resp.content, True)
msg["email"] = "12345@gmail.com"
resp = c.post(reverse("accounts:register"), msg)
self.assertEqual(resp.status_code, 400)
self.assertEqual(
b"We do not provide services for this domain yet." in resp.content, True
)
# Login Tests
class LoginTest(TestCase):
def setUp(self):
self.username = "register_user"
self.password = "2i3j4;1qlk2asdf"
self.email = "register_user@vitap.ac.in"
register_util(t=self, username=self.username)
def test_register_template_works(self):
"""
Tests if register template renders
"""
resp = self.client.get(reverse("accounts:login"))
self.assertEqual(b"Log" in resp.content, True)
def test_login_works(self):
"""
Tests if login works
"""
c = Client()
payload = {
"email": self.email,
"password": self.password,
}
resp = c.post(reverse("accounts:login"), payload)
self.assertEqual(resp.status_code, 302)
self.assertEqual(resp.headers["location"], reverse("accounts:success_page"))

28
accounts/urls.py Normal file
View File

@ -0,0 +1,28 @@
"""sso URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.1/topics/http/urls/
Exaimples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from . import views
app_name = "accounts"
urlpatterns = [
path("", views.index, name="accounts.index"),
path("register/", views.register, name="register"),
path("login/", views.login_user, name="login"),
path("success/", views.success_page, name="success_page"),
# path("accounts/login/", default_login_url, name="accounts.default_login_url"),
]

View File

@ -1,3 +1,165 @@
from django.shortcuts import render
# Copyright © 2022 Alan Alexander Thomas <alan2000alex@gmail.com>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
#
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from audioop import reverse
from multiprocessing import get_context
# Create your views here.
from re import template
from urllib import response
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.password_validation import validate_password
from django.contrib.auth import authenticate, login, logout, get_user_model
from django.http import HttpResponse, HttpResponseRedirect
from django.views.decorators.csrf import csrf_protect
from django.core.exceptions import ValidationError
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.urls import reverse
# Create your views here.
from django.http import HttpResponse
def index(request):
return HttpResponse("Hello World.")
# @csrf_protect
# def login(request):
# return render(request, 'accounts/login.html')
# login page
@csrf_protect
def login_user(request):
def default_login_ctx():
return {
"title": "Login",
"footer": footer_ctx(),
}
if request.method == "POST":
email = request.POST["email"]
password = request.POST["password"]
# domain_check = email.split("@")
# check user exists
User = get_user_model()
if not User.objects.filter(email=email).exists():
messages.info(request, "Username OR password is incorrect")
return redirect(reverse("accounts:login"))
username = User.objects.get(email=email).username
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
# Redirect to a success page.
print(reverse("accounts:success_page"))
if "next" in request.POST:
next = request.POST["next"]
if len(next) > 0:
return redirect(next)
return redirect(reverse("accounts:success_page"))
else:
# Return an 'invalid login' error message.
messages.info(request, "Username OR password is incorrect")
return redirect(reverse("accounts:login"))
else:
context = {}
if "next" in request.GET:
next = request.GET["next"]
context["next"] = next
return render(request, "accounts/login.html", context=context)
# success page
@login_required(login_url="/accounts/login/")
@csrf_protect
def success_page(request):
return render(request, "accounts/success.html")
# user registratoin
@csrf_protect
def register(request):
# response = "You are at the Registration Page."
get_context = {}
if request.method == "GET":
if "next" in request.GET:
get_context["next"] = request.GET["next"]
return render(request, "accounts/register.html", get_context)
context = {}
# variables
email = request.POST["email"]
password = request.POST["password"]
password_confirm = request.POST["password-confirm"]
# password matching
if password != password_confirm:
context["error"] = {
"title": "Registration Failed",
"reason": "Passwords do not match.",
}
return render(request, "accounts/register.html", status=400, context=context)
# domain verification
domain_check = email.split("@")
if domain_check[1] != "vitap.ac.in":
context["error"] = {
"title": "Registration Failed",
"reason": "We do not provide services for this domain yet.",
}
return render(request, "accounts/register.html", status=400, context=context)
# email verification
User = get_user_model()
if any(
[
User.objects.filter(email=email).exists(),
User.objects.filter(username=domain_check[0]).exists(),
]
):
context["error"] = {
"title": "Registration Failed",
"reason": "This email is already registered.",
}
return render(request, "accounts/register.html", status=400, context=context)
user = get_user_model()(
username=domain_check[0],
email=email,
)
user.set_password(password)
try:
user.full_clean()
validate_password(password, user=user)
except ValidationError as err:
reason = ""
for errors in err:
reason += errors + " "
context["error"] = {"title": "Registration Failed", "reason": reason}
print(reason)
return render(request, "accounts/register.html", status=400, context=context)
user.save()
return HttpResponse("New acc. can be registered.")

29
docker-compose.yml Normal file
View File

@ -0,0 +1,29 @@
version: "3.3"
services:
# db:
# image: postgres
# volumes:
# - ./data/db:/var/lib/postgresql/data
# environment:
# - POSTGRES_DB=postgres
# - POSTGRES_USER=postgres
# - POSTGRES_PASSWORD=postgres
web:
# build: .
image: realaravinth/mystiq-sso:latest
restart: always
command: >
sh -c "python manage.py makemigrations &&
python manage.py migrate &&
python manage.py runserver 0.0.0.0:8000"
volumes:
- ./sso/local_settings:/code/sso/local_settings/
ports:
- 8000:8000
# environment:
# - POSTGRES_NAME=postgres
# - POSTGRES_USER=postgres
# - POSTGRES_PASSWORD=postgres
# depends_on:
# - db

View File

@ -29,6 +29,7 @@ pylint==2.14.5
pynvim==0.4.3
requests==2.28.1
sqlparse==0.4.2
tblib==1.7.0
tomli==2.0.1
tomlkit==0.11.4
urllib3==1.26.11

View File

View File

@ -0,0 +1,30 @@
"""
Django settings for dashboard project.
Generated by 'django-admin startproject' using Django 4.0.3.
For more information on this file, see
https://docs.djangoproject.com/en/4.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.0/ref/settings/
"""
import environ
import os
env = environ.Env()
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
# A new SECRET_KEY can be generated by running the following command:
# openssl rand -hex 32
SECRET_KEY = "django-insecure-44zt@)$td7_yh(01q^hrce%h(311n!djn%%#s1b7$cvfy!pf7y"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
print("Finished importing local_settings.ci.py")

View File

@ -135,3 +135,11 @@ OAUTH2_PROVIDER = {
"openid": "OpenID Connect scope",
},
}
try:
from .local_settings.local_settings import *
print("Found local_settings")
except ModuleNotFoundError:
print("No local_settings available, using defaults")
pass

View File

@ -19,4 +19,5 @@ from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")),
path("accounts/", include("accounts.urls")),
]