Browse Source

added very simple password authentication

pull/1/head
Holger Frey 5 years ago
parent
commit
98cadf00a6
  1. 5
      development.ini
  2. 1
      pyproject.toml
  3. 16
      superx_budget/pyramid/__init__.py
  4. 11
      superx_budget/pyramid/overview.py
  5. 68
      superx_budget/pyramid/security.py
  6. 8
      superx_budget/pyramid/templates.py
  7. 56
      superx_budget/pyramid/templates/login.jinja2

5
development.ini

@ -6,6 +6,11 @@
[app:main] [app:main]
use = egg:superx_budget use = egg:superx_budget
pwd.db =
$argon2id$v=19$m=102400,t=2,p=8$f48xZqyVUsoZg5AyJmRszQ$5Bn/u67+2pHNBxe5g0UFnw
$argon2id$v=19$m=102400,t=2,p=8$vheCMKa0dq7V2nuPUWrtXQ$pfomI8eG74mKulf1Elp0JA
auth.secret = "change me in production"
session.secret = "change me in production" session.secret = "change me in production"
budgets.dir = %(here)s/test_data budgets.dir = %(here)s/test_data

1
pyproject.toml

@ -34,6 +34,7 @@ requires-python = ">=3.7"
[tool.flit.metadata.requires-extra] [tool.flit.metadata.requires-extra]
test = [ test = [
"passlib[argon2] >= 1.7.2",
"pytest >=4.0.0", "pytest >=4.0.0",
"pytest-cov", "pytest-cov",
"pytest-mock", "pytest-mock",

16
superx_budget/pyramid/__init__.py

@ -5,6 +5,7 @@ from pathlib import Path
from pyramid.view import notfound_view_config from pyramid.view import notfound_view_config
from pyramid.config import Configurator from pyramid.config import Configurator
from pyramid.session import JSONSerializer, SignedCookieSessionFactory from pyramid.session import JSONSerializer, SignedCookieSessionFactory
from pyramid.security import Allow, Everyone, Authenticated
from pyramid.httpexceptions import HTTPFound from pyramid.httpexceptions import HTTPFound
from ..overview import create_overview # noqa: F401 from ..overview import create_overview # noqa: F401
@ -13,22 +14,24 @@ from ..exceptions import BudgetParserError, SuperXParserError # noqa: F401
XLSX_CONTENT_TYPE = "application/vnd.ms-excel" XLSX_CONTENT_TYPE = "application/vnd.ms-excel"
def root_factory(request): class Root:
return {}
__acl__ = [(Allow, Everyone, "login"), (Allow, Authenticated, "view")]
def __init__(self, request):
pass
def main(global_config, **settings): def main(global_config, **settings):
""" This function returns a Pyramid WSGI application. """ """ This function returns a Pyramid WSGI application. """
with Configurator(settings=settings) as config: with Configurator(settings=settings) as config:
config.include("pyramid_jinja2")
session_factory = SignedCookieSessionFactory( session_factory = SignedCookieSessionFactory(
settings["session.secret"], serializer=JSONSerializer() settings["session.secret"], serializer=JSONSerializer()
) )
config.set_session_factory(session_factory) config.set_session_factory(session_factory)
config.set_root_factory(root_factory) config.set_root_factory(Root)
config.add_request_method( config.add_request_method(
lambda r: Path(settings["budgets.dir"]), "budgets_dir", reify=True, lambda r: Path(settings["budgets.dir"]), "budgets_dir", reify=True,
@ -37,6 +40,9 @@ def main(global_config, **settings):
age = int(settings.get("static_views.cache_max_age", 0)) age = int(settings.get("static_views.cache_max_age", 0))
config.add_static_view("static", "static", cache_max_age=age) config.add_static_view("static", "static", cache_max_age=age)
config.include("pyramid_jinja2")
config.include(".security")
config.scan() config.scan()
return config.make_wsgi_app() return config.make_wsgi_app()

11
superx_budget/pyramid/overview.py

@ -6,7 +6,7 @@ from pyramid.view import view_config
from pyramid.httpexceptions import HTTPFound from pyramid.httpexceptions import HTTPFound
from pyramid_mailer.message import Message, Attachment from pyramid_mailer.message import Message, Attachment
from . import XLSX_CONTENT_TYPE from . import XLSX_CONTENT_TYPE, Root
from ..budget import parse_budget_file from ..budget import parse_budget_file
from ..superx import parse_exported_file from ..superx import parse_exported_file
from ..helpers import find_recipients, find_budget_file, get_sheet_of_file from ..helpers import find_recipients, find_budget_file, get_sheet_of_file
@ -27,18 +27,20 @@ bei Fragen bitte an Holgi wenden
@view_config( @view_config(
context=dict, context=Root,
request_method="GET", request_method="GET",
renderer="superx_budget:pyramid/templates/start.jinja2", renderer="superx_budget:pyramid/templates/start.jinja2",
permission="view",
) )
def index(context, request): def index(context, request):
return {} return {}
@view_config( @view_config(
context=dict, context=Root,
request_method="POST", request_method="POST",
renderer="superx_budget:pyramid/templates/overview.jinja2", renderer="superx_budget:pyramid/templates/overview.jinja2",
permission="view",
) )
def superx_upload(context, request): def superx_upload(context, request):
upload = request.POST.get("superx") upload = request.POST.get("superx")
@ -98,10 +100,11 @@ def superx_upload(context, request):
@view_config( @view_config(
context=dict, context=Root,
name="send", name="send",
request_method="POST", request_method="POST",
renderer="superx_budget:pyramid/templates/sent.jinja2", renderer="superx_budget:pyramid/templates/sent.jinja2",
permission="view",
) )
def send_overview(context, request): def send_overview(context, request):
export_date = request.POST.get("export_date").strip() export_date = request.POST.get("export_date").strip()

68
superx_budget/pyramid/security.py

@ -0,0 +1,68 @@
from passlib.hash import argon2
from pyramid.view import view_config, forbidden_view_config
from pyramid.security import forget, remember
from pyramid.authorization import ACLAuthorizationPolicy
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.httpexceptions import HTTPFound
from . import Root
AUTHENTICATED_USER_ID = "authenticated"
class MyAuthenticationPolicy(AuthTktAuthenticationPolicy):
def authenticated_userid(self, request):
user = request.user
if user is not None:
return AUTHENTICATED_USER_ID
def get_user(request):
return request.unauthenticated_userid
@forbidden_view_config(
renderer="superx_budget:pyramid/templates/login.jinja2",
)
def forbidden_view(request):
return {"error": False}
@view_config(
context=Root,
name="login",
request_method="POST",
permission="login",
renderer="superx_budget:pyramid/templates/login.jinja2",
)
def login(request):
if request.check_password():
headers = remember(request, AUTHENTICATED_USER_ID, max_age=3600)
return HTTPFound("/", headers=headers)
return {"error": True}
@view_config(
context=Root, name="logout", permission="login",
)
def logout(request):
headers = forget(request)
return HTTPFound("/", headers=headers)
def includeme(config):
settings = config.get_settings()
authn_policy = MyAuthenticationPolicy(
settings["auth.secret"], hashalg="sha512",
)
config.set_authentication_policy(authn_policy)
config.set_authorization_policy(ACLAuthorizationPolicy())
hashes = [hash for hash in settings["pwd.db"].splitlines() if hash]
def check_password(request):
password = request.POST.get("password", "")
return any(argon2.verify(password, hash) for hash in hashes)
config.add_request_method(check_password, "check_password")
config.add_request_method(get_user, "user", reify=True)

8
superx_budget/pyramid/templates.py

@ -6,7 +6,7 @@ from pyramid.view import view_config
from pyramid.response import FileResponse from pyramid.response import FileResponse
from pyramid.httpexceptions import HTTPFound from pyramid.httpexceptions import HTTPFound
from . import XLSX_CONTENT_TYPE from . import XLSX_CONTENT_TYPE, Root
from ..budget import parse_budget_file from ..budget import parse_budget_file
from ..helpers import list_budget_files, is_budget_file_name from ..helpers import list_budget_files, is_budget_file_name
from ..overview import create_overview # noqa: F401 from ..overview import create_overview # noqa: F401
@ -14,10 +14,11 @@ from ..exceptions import BudgetParserError, SuperXParserError # noqa: F401
@view_config( @view_config(
context=dict, context=Root,
name="templates", name="templates",
request_method="GET", request_method="GET",
renderer="superx_budget:pyramid/templates/templates.jinja2", renderer="superx_budget:pyramid/templates/templates.jinja2",
permission="view",
) )
def templates(context, request): def templates(context, request):
if "f" in request.GET: if "f" in request.GET:
@ -43,10 +44,11 @@ def templates(context, request):
@view_config( @view_config(
context=dict, context=Root,
name="templates", name="templates",
request_method="POST", request_method="POST",
renderer="superx_budget:pyramid/templates/templates.jinja2", renderer="superx_budget:pyramid/templates/templates.jinja2",
permission="view",
) )
def templates_update(context, request): def templates_update(context, request):
upload = request.POST.get("budget") upload = request.POST.get("budget")

56
superx_budget/pyramid/templates/login.jinja2

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>SuperX -> Budget Overview</title>
<link href="{{request.static_url('superx_budget.pyramid:static/img/favicon.ico')}}" type="image/x-icon" rel="shortcut icon">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<link rel="stylesheet" href="{{request.static_url('superx_budget.pyramid:static/style.css')}}" type="text/css" media="screen" />
</head>
<body>
<div class="container">
<div class="row mb-4">
<div class="col">
<header>
<nav class="navbar navbar-expand navbar-light bg-light">
<span class="navbar-brand bg-info p-3 rounded">Budget Overview From SuperX</span>
</nav>
</header>
</div>
</div>
<div class="row">
<div class="col">
<h2 class="mt-3 mb-4">Please Log In</h2>
<form class="form" method="POST" action="/login">
<p>
<div class="input-group">
<label for="password" class="sr-only">Password:</label>
<input type="passwort" id="password" name="password" class="form-control {% if error %}is-invalid{% endif %}" required="required" placeholder="Password">
<div class="invalid-feedback">Password is invalid</div>
</div>
</p>
<p>
<button type="submit" name="submit" value="login" class="btn btn-primary">log in</button>
</p>
</form>
</div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row mt-3">
<div class="col">
<footer>
<p class="bg-light p-3">Any problems or questions? Please contact <a href="https://wiki.cpi.imtek.uni-freiburg.de/HolgerFrey">Holgi</a>.</p>
</footer>
</div>
</div>
</div></div></div>
</body>
</html>
Loading…
Cancel
Save