Skip to content

Commit

Permalink
release 0.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
ToraNova committed Mar 22, 2022
1 parent 5138e45 commit a11d71f
Show file tree
Hide file tree
Showing 18 changed files with 279 additions and 37 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ oldref/*
examples/*.db
examples/uploads/*
*.db
uploads/*
12 changes: 6 additions & 6 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
language: python
dist: bionic
python:
- "3.8"
- "3.8-dev" # 3.8 development branch
- "3.7-dev"
- "3.8-dev"
- "3.9"
- "3.9-dev" # 3.9 development branch
- "3.10"
- "3.10-dev" # 3.10
- "3.9-dev"
- "3.10" # 3.10
#- "nightly" # nightly build
# command to install dependencies
install:
Expand All @@ -15,4 +15,4 @@ install:
- pip install -e .
# command to run tests
script:
- bash runtest.sh
- bash test_run.sh
4 changes: 3 additions & 1 deletion examples/run.sh → dev_run.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#!/bin/sh

. ../bin/activate
. bin/activate

cd examples

#export FLASK_APP=auth_basic
#export FLASK_APP=auth_database
Expand Down
Binary file added doomer.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions examples/auth_withrole_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def test_admin_feature(client):
assert resp.status_code == 200
assert b'welcome jason, your role is admin' in resp.data


resp = client.post('/user/add', data={'username': 'newuser', 'password':'newpass', 'password_confirm': 'newpass', 'role': 'no role'}, follow_redirects=True)
assert len(resp.history) == 1
assert resp.status_code == 200
Expand All @@ -38,6 +39,13 @@ def test_admin_feature(client):
assert b'newuser (no role)' in resp.data
assert b'green">useradd successful' in resp.data

resp = client.get('/user/mod')
assert resp.status_code == 400

resp = client.get('/user/mod?id=newuser')
assert resp.status_code == 200
assert b'no role' in resp.data

resp = client.post('/user/mod?id=newuser', data={'role': 'admin'}, follow_redirects=True)
assert len(resp.history) == 1
assert resp.status_code == 200
Expand All @@ -47,6 +55,13 @@ def test_admin_feature(client):
assert b'newuser (admin)' in resp.data
assert b'green">usermod successful' in resp.data

resp = client.get('/user/del')
assert resp.status_code == 400

resp = client.get('/user/del?id=newuser')
assert resp.status_code == 200
assert b'remove user newuser?' in resp.data

resp = client.post('/user/del?id=newuser', follow_redirects=True)
assert len(resp.history) == 1
assert resp.status_code == 200
Expand Down
Binary file added examples/doomer.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions examples/starter_1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from .models import MyRole, MyUser, Project, my_declarative_base
from .auth import MyAuth

from sqlalchemy.ext.declarative import declarative_base

def create_app(test_config=None):
app = Flask(__name__, instance_relative_config=True)
app.secret_key = 'v3rypowerfuls3cret, or not. CHANGE THIS!@'
Expand Down
14 changes: 11 additions & 3 deletions examples/starter_1/models.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import os
import jwt
import json
from flask import current_app
from werkzeug.datastructures import FileStorage

from flask_arch.cms import SQLContent, DEFAULT
from flask_arch.cms import file_storage, SIZE_MB
from flask_arch.cms import file_storage, SIZE_MB, SIZE_KB
from flask_arch.user import SQLUserWithRole, SQLRole
from flask_arch.exceptions import UserError
from flask_arch.utils import parse_boolean
Expand All @@ -15,10 +16,17 @@

my_declarative_base = declarative_base()

# TODO: remove this in production, only used for unit-testing
upload_d = 'uploads'
if os.environ.get('UPLOAD_DIR'):
upload_d = os.environ['UPLOAD_DIR']
print(upload_d)

class MyRole(SQLRole, my_declarative_base):

def set_json_privileges(self, jsonstr):
po = json.loads(jsonstr)
self.privileges = '{}'
for k, v in po.items():
self.set_privilege(k, v)

Expand All @@ -34,7 +42,7 @@ def modify(self, rp, actor):


# using file_storage for storing profile picture (EXAMPLE OF SINGULAR FILE MANAGEMENT)
@file_storage(max_size=5*SIZE_MB, regex_whitelist=['jpe?g$', 'png$'])
@file_storage(upload_dir=upload_d, max_size=80*SIZE_KB, regex_whitelist=['jpe?g$', 'png$'])
class MyUser(SQLUserWithRole):
userid = 'email' # indicate the user will login with 'email' as its identifier

Expand Down Expand Up @@ -97,7 +105,7 @@ def profile_img(cls):
# only allow files ending in jpg, jpeg and png
# store the files separately for each content, with subdirectory created using the 'name' attribute
# EXAMPLE OF MULTIPLE FILE HANDLING
@file_storage(max_size=5*SIZE_MB, regex_whitelist=['jpe?g$', 'png$'], subdir_key='name')
@file_storage(upload_dir=upload_d, max_size=5*SIZE_MB, regex_whitelist=['jpe?g$', 'png$'], subdir_key='name')
class Project(SQLContent, my_declarative_base):
__tablename__ = "project"

Expand Down
1 change: 1 addition & 0 deletions examples/starter_1/templates/users/del.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

<h1>are you sure you want to remove user '{{target.name}}'?</h1>
<form method="post">
{% include "includes/csrf_token.html" %}
<button class="btn btn-danger mt-3" type="submit">delete account</button>
</form>

Expand Down
220 changes: 220 additions & 0 deletions examples/starter_1_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import pytest
import os
import re
import tempfile
import shutil

# hacky way to use a different upload_dir when testing
tmpd = tempfile.mkdtemp()
os.environ['UPLOAD_DIR'] = tmpd

from werkzeug.datastructures import FileStorage
from starter_1 import create_app

@pytest.fixture()
def app():
db_fd, db_file = tempfile.mkstemp()
db_uri = 'sqlite:///%s' % db_file
app = create_app({
'TESTING': True,
'DBURI': db_uri,
})
yield app
os.close(db_fd)
os.unlink(db_file)

@pytest.fixture()
def client(app):
return app.test_client()

def parse_csrf(resp):
mt = re.search(b'"csrf_token" value="(\\S+)"', resp.data)
assert mt
csrf = mt.group(1).decode()
return csrf

def login(client, email, passw):
resp = client.get('/auth/login')
csrf = parse_csrf(resp)

resp = client.post('/auth/login', data={'email':email, 'password':passw, 'csrf_token':csrf}, follow_redirects=True)
assert resp.status_code == 200

def logout(client):
resp = client.get('/auth/logout', follow_redirects=True)
assert resp.status_code == 200

#TODO: allow multi-testing by fixing 'is already defined for this MetaData instance'
def test_all(client):
login(client, '[email protected]', 'hunter2')

# test obtain profile picture
resp = client.get('/auth/profile')
mt = re.search(b'src="/auth/file\\?filename=(\\S+\\.jpg)"', resp.data)
assert mt
profimg_name = mt.group(1).decode()
resp = client.get(f'/auth/file?filename={profimg_name}')
assert resp.status_code == 200

test_file = FileStorage(stream=open('doomer.jpg', "rb"),)
resp = client.get('/auth/renew')
csrf = parse_csrf(resp)
resp = client.post('/auth/renew', data={'profile_img':test_file, 'csrf_token':csrf}, content_type='multipart/form-data', follow_redirects=True)
assert resp.status_code == 400
assert b'invalid file size' in resp.data

resp = client.get(f'/auth/file?filename={profimg_name}')
assert resp.status_code == 200

test_file = FileStorage(stream=open('wojak.jpg', "rb"),)
resp = client.get('/auth/renew')
csrf = parse_csrf(resp)
resp = client.post('/auth/renew', data={'profile_img':test_file, 'csrf_token':csrf}, content_type='multipart/form-data', follow_redirects=True)
assert resp.status_code == 200
mt = re.search(b'src="/auth/file\\?filename=(\\S+\\.jpg)"', resp.data)
assert mt
new_profimg_name = mt.group(1).decode()
assert new_profimg_name != profimg_name

resp = client.get(f'/auth/file?filename={profimg_name}')
assert resp.status_code == 404
assert b'Not Found' in resp.data

resp = client.get(f'/auth/file?filename={new_profimg_name}')
assert resp.status_code == 200

resp = client.get('/role/list')
assert b'&#34;project.view&#34;: 1' in resp.data

resp = client.get('/role/insert')
csrf = parse_csrf(resp)

resp = client.post('/role/insert', data={'name':'NEW_ROLE_TEST', 'privileges':'{"NEW_ROLE_PRIV": 1}', 'csrf_token':csrf}, follow_redirects=True)
assert b'NEW_ROLE_TEST' in resp.data and b'NEW_ROLE_PRIV' in resp.data

assert resp.status_code == 200

resp = client.get('/role/update')
assert resp.status_code == 400

resp = client.get('/role/delete')
assert resp.status_code == 400

resp = client.get('/project/list')
assert resp.status_code == 200

resp = client.get('/role/update?id=1')
csrf = parse_csrf(resp)

resp = client.post('/role/update?id=1', data={'privileges':'{"user.view": 1, "user.add": 1, "user.mod": 1, "user.del": 1, "role.view": 1, "role.insert": 1, "role.update": 1, "role.delete": 1, "project.view": 0, "project.insert": 1, "project.update": 1, "project.delete": 1}', 'csrf_token': csrf}, follow_redirects=True)
assert resp.status_code == 200

resp = client.get('/role/update?id=2')
csrf = parse_csrf(resp)
resp = client.post('/role/update?id=2', data={'privileges':'{}', 'csrf_token': csrf}, follow_redirects=True)
assert resp.status_code == 200
assert b'&#34;project.view&#34;' not in resp.data
csrf = parse_csrf(resp)

resp = client.get('/role/update?id=2')
csrf = parse_csrf(resp)
resp = client.post('/role/update?id=2', data={'privileges':'{"project.view":1, "project.insert":1, "project.update":1}', 'csrf_token': csrf}, follow_redirects=True)
assert resp.status_code == 200

resp = client.get('/project/list')
assert resp.status_code == 403

resp = client.post('/role/delete?id=3', data={'csrf_token': csrf}, follow_redirects=True)
assert resp.status_code == 200
assert b'NEW_ROLE_TEST' not in resp.data and b'NEW_ROLE_PRIV' not in resp.data

resp = client.post('/role/update?id=1', data={'privileges':'{"user.view": 1, "user.add": 1, "user.mod": 1, "user.del": 1, "role.view": 1, "role.insert": 1, "role.update": 1, "role.delete": 1, "project.view": 1, "project.insert": 1, "project.update": 1, "project.delete": 1}', 'csrf_token': csrf}, follow_redirects=True)
assert resp.status_code == 200

# MULTI-FILE UPLOAD
resp = client.get('/project/insert')
csrf = parse_csrf(resp)

invalid_file = FileStorage(stream=open('dev_run.sh', "rb"),)
resp = client.post('/project/insert', data={'name':'UPLOAD_TEST', 'csrf_token': csrf, 'project_files':[invalid_file]}, content_type='multipart/form-data', follow_redirects=True)
assert resp.status_code == 400
assert b'invalid file name' in resp.data

invalid_file = FileStorage(stream=open('dev_run.sh', "rb"),)

resp = client.get('/project/insert')
csrf = parse_csrf(resp)

test_file = FileStorage(stream=open('doomer.jpg', "rb"),)
resp = client.post('/project/insert', data={'name':'UPLOAD_TEST', 'csrf_token': csrf, 'project_files':[test_file]}, content_type='multipart/form-data', follow_redirects=True)
assert b'UPLOAD_TEST' in resp.data

resp = client.get('/project/view?id=1')
mt = re.search(b'\\.jpg">(\\S+\\.jpg)</a>', resp.data)
assert mt
filename = mt.group(1).decode()

resp = client.get(f'/project/file?id=1&filename={filename}')
assert resp.status_code == 200

resp = client.get('/project/update?id=1')
csrf = parse_csrf(resp)

test_file = FileStorage(stream=open('wojak.jpg', "rb"),)
resp = client.post('/project/update?id=1', data={'project_files':[test_file], 'csrf_token':csrf}, follow_redirects=True, content_type='multipart/form-data')
assert resp.status_code == 200

resp = client.get('/project/view?id=1')
mt = re.findall(b'\\.jpg">(\\S+\\.jpg)</a>', resp.data)
assert len(mt) == 2

resp = client.get('/project/update?id=1')
csrf = parse_csrf(resp)

resp = client.post('/project/update?id=1', data={filename:'delete', 'csrf_token':csrf}, follow_redirects=True, content_type='multipart/form-data')
assert resp.status_code == 200

resp = client.get('/project/view?id=1')
mt = re.findall(b'\\.jpg">(\\S+\\.jpg)</a>', resp.data)
assert len(mt) == 1
assert filename not in mt
left_over = mt[0]

logout(client)

login(client, '[email protected]', 'asdasd')

resp = client.get('/role/list')
assert resp.status_code == 403
assert b'403 Forbidden' in resp.data

resp = client.get('/user/list')
assert resp.status_code == 403
assert b'403 Forbidden' in resp.data

resp = client.get('/project/list')
csrf = parse_csrf(resp)
assert resp.status_code == 200

resp = client.get('/project/view?id=1', follow_redirects=True)
assert b'cannot view, no ownership' in resp.data
csrf = parse_csrf(resp)

resp = client.get('/project/update?id=1', follow_redirects=True)
assert b'cannot view, no ownership' in resp.data

resp = client.post('/project/update?id=1', data={left_over:'delete', 'csrf_token':csrf}, content_type='multipart/form-data', follow_redirects=True)
assert resp.status_code == 400
assert b'cannot modify, no ownership'

resp = client.get('/project/delete?id=1', follow_redirects=True)
assert b'403 Forbidden' in resp.data

resp = client.post('/project/delete?id=1', data={'csrf_token':csrf}, follow_redirects=True)
assert resp.status_code == 403
assert b'cannot delete, no ownership'

logout(client)

# dont with testing, remove temporary upload directory
shutil.rmtree(tmpd)
13 changes: 11 additions & 2 deletions flask_arch/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from .utils import ensure_type, ensure_callable

import traceback
from jinja2.exceptions import TemplateNotFound
from jinja2.exceptions import TemplateNotFound, UndefinedError
from flask import redirect, url_for, flash, render_template, request, abort, current_app, make_response

# late-binding vs. early binding
Expand Down Expand Up @@ -150,7 +150,13 @@ def _handle_user_error(self, e):
if e.reroute:
return self.reroute()
else:
return self.render(), e.code
try:
return self.render(), e.code
except UndefinedError:
# template variable undefined,
# likely something bad has happened
# (i.e., user posting to a route they're not supposed to)
self.abort(400)

def client_error(self, e):
if current_app.debug:
Expand All @@ -160,6 +166,9 @@ def client_error(self, e):
if isinstance(e, exceptions.UserError):
# is a user error
return self._handle_user_error(e)
elif isinstance(e, FileNotFoundError):
# file not found, 404
self.abort(404)

self.abort(400) # response 4xx

Expand Down
Loading

0 comments on commit a11d71f

Please sign in to comment.