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 @@ @@ -6,6 +6,11 @@
[app:main]
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"
budgets.dir = %(here)s/test_data

1
pyproject.toml

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

16
superx_budget/pyramid/__init__.py

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

11
superx_budget/pyramid/overview.py

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

68
superx_budget/pyramid/security.py

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

56
superx_budget/pyramid/templates/login.jinja2

@ -0,0 +1,56 @@ @@ -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