First attempt at online permissions.

master
voussoir 2023-02-04 13:49:42 -08:00
parent e78a667ee3
commit ce30077013
10 changed files with 181 additions and 13 deletions

View File

@ -4,29 +4,46 @@ Use etiquette_flask_dev.py or etiquette_flask_prod.py.
''' '''
import flask; from flask import request import flask; from flask import request
import functools import functools
import json
import mimetypes import mimetypes
import traceback import traceback
from voussoirkit import bytestring from voussoirkit import bytestring
from voussoirkit import configlayers
from voussoirkit import flasktools from voussoirkit import flasktools
from voussoirkit import pathclass from voussoirkit import pathclass
from voussoirkit import vlogging
import etiquette import etiquette
from . import client_caching from . import client_caching
from . import jinja_filters from . import jinja_filters
from . import permissions
from . import sessions from . import sessions
log = vlogging.getLogger(__name__)
# Constants ########################################################################################
DEFAULT_SERVER_CONFIG = {
'anonymous_read': True,
'anonymous_write': True,
}
BROWSER_CACHE_DURATION = 180
# Flask init ####################################################################################### # Flask init #######################################################################################
# __file__ = .../etiquette_flask/backend/common.py # __file__ = .../etiquette_flask/backend/common.py
# root_dir = .../etiquette_flask # root_dir = .../etiquette_flask
root_dir = pathclass.Path(__file__).parent.parent root_dir = pathclass.Path(__file__).parent.parent
P = None
TEMPLATE_DIR = root_dir.with_child('templates') TEMPLATE_DIR = root_dir.with_child('templates')
STATIC_DIR = root_dir.with_child('static') STATIC_DIR = root_dir.with_child('static')
FAVICON_PATH = STATIC_DIR.with_child('favicon.png') FAVICON_PATH = STATIC_DIR.with_child('favicon.png')
BROWSER_CACHE_DURATION = 180 SERVER_CONFIG_FILENAME = 'etiquette_flask_config.json'
site = flask.Flask( site = flask.Flask(
__name__, __name__,
@ -37,6 +54,7 @@ site.config.update(
SEND_FILE_MAX_AGE_DEFAULT=BROWSER_CACHE_DURATION, SEND_FILE_MAX_AGE_DEFAULT=BROWSER_CACHE_DURATION,
TEMPLATES_AUTO_RELOAD=True, TEMPLATES_AUTO_RELOAD=True,
) )
site.server_config = None
site.jinja_env.add_extension('jinja2.ext.do') site.jinja_env.add_extension('jinja2.ext.do')
site.jinja_env.trim_blocks = True site.jinja_env.trim_blocks = True
site.jinja_env.lstrip_blocks = True site.jinja_env.lstrip_blocks = True
@ -49,6 +67,7 @@ file_etag_manager = client_caching.FileEtagManager(
max_filesize=5 * bytestring.MEBIBYTE, max_filesize=5 * bytestring.MEBIBYTE,
max_age=BROWSER_CACHE_DURATION, max_age=BROWSER_CACHE_DURATION,
) )
permission_manager = permissions.PermissionManager(site)
# Response wrappers ################################################################################ # Response wrappers ################################################################################
@ -80,10 +99,18 @@ def before_request():
if site.localhost_only and not request.is_localhost: if site.localhost_only and not request.is_localhost:
return flask.abort(403) return flask.abort(403)
# Since we don't define this route, I can't just add this where it belongs.
# Sorry.
if request.url_rule.rule == '/static/<path:filename>':
permission_manager.global_public()
session_manager._before_request(request) session_manager._before_request(request)
@site.after_request @site.after_request
def after_request(response): def after_request(response):
if response.status_code < 400 and not hasattr(request, 'checked_permissions'):
log.error('You forgot to set checked_permissions for ' + request.path)
return flask.abort(500)
response = flasktools.gzip_response(request, response) response = flasktools.gzip_response(request, response)
response = session_manager._after_request(response) response = session_manager._after_request(response)
return response return response
@ -277,3 +304,22 @@ def send_file(filepath, override_mimetype=None):
def init_photodb(*args, **kwargs): def init_photodb(*args, **kwargs):
global P global P
P = etiquette.photodb.PhotoDB.closest_photodb(*args, **kwargs) P = etiquette.photodb.PhotoDB.closest_photodb(*args, **kwargs)
load_config()
def load_config() -> None:
log.debug('Loading server config file.')
config_file = P.data_directory.with_child(SERVER_CONFIG_FILENAME)
(config, needs_rewrite) = configlayers.load_file(
filepath=config_file,
default_config=DEFAULT_SERVER_CONFIG,
)
site.server_config = config
if needs_rewrite:
save_config()
def save_config() -> None:
log.debug('Saving server config file.')
config_file = P.data_directory.with_child(SERVER_CONFIG_FILENAME)
with config_file.open('w', encoding='utf-8') as handle:
handle.write(json.dumps(site.server_config, indent=4, sort_keys=True))

View File

@ -15,8 +15,7 @@ session_manager = common.session_manager
@site.route('/admin') @site.route('/admin')
def get_admin(): def get_admin():
if not request.is_localhost: common.permission_manager.admin()
flask.abort(403)
counts = dotdict.DotDict({ counts = dotdict.DotDict({
'albums': common.P.get_album_count(), 'albums': common.P.get_album_count(),
@ -36,8 +35,7 @@ def get_admin():
@site.route('/admin/dbdownload') @site.route('/admin/dbdownload')
def get_dbdump(): def get_dbdump():
if not request.is_localhost: common.permission_manager.admin()
flask.abort(403)
with common.P.transaction: with common.P.transaction:
binary = common.P.database_filepath.read('rb') binary = common.P.database_filepath.read('rb')
@ -52,24 +50,23 @@ def get_dbdump():
@site.route('/admin/clear_sessions', methods=['POST']) @site.route('/admin/clear_sessions', methods=['POST'])
def post_clear_sessions(): def post_clear_sessions():
if not request.is_localhost: common.permission_manager.admin()
return flasktools.json_response({}, status=403)
session_manager.clear() session_manager.clear()
return flasktools.json_response({}) return flasktools.json_response({})
@site.route('/admin/reload_config', methods=['POST']) @site.route('/admin/reload_config', methods=['POST'])
def post_reload_config(): def post_reload_config():
if not request.is_localhost: common.permission_manager.admin()
return flasktools.json_response({}, status=403)
common.P.load_config() common.P.load_config()
common.load_config()
return flasktools.json_response({}) return flasktools.json_response({})
@site.route('/admin/uncache', methods=['POST']) @site.route('/admin/uncache', methods=['POST'])
def post_uncache(): def post_uncache():
if not request.is_localhost: common.permission_manager.admin()
return flasktools.json_response({}, status=403)
with common.P.transaction: with common.P.transaction:
for cache in common.P.caches.values(): for cache in common.P.caches.values():

View File

@ -17,6 +17,7 @@ session_manager = common.session_manager
@site.route('/album/<album_id>') @site.route('/album/<album_id>')
def get_album_html(album_id): def get_album_html(album_id):
common.permission_manager.basic()
album = common.P_album(album_id, response_type='html') album = common.P_album(album_id, response_type='html')
response = common.render_template( response = common.render_template(
request, request,
@ -28,12 +29,14 @@ def get_album_html(album_id):
@site.route('/album/<album_id>.json') @site.route('/album/<album_id>.json')
def get_album_json(album_id): def get_album_json(album_id):
common.permission_manager.basic()
album = common.P_album(album_id, response_type='json') album = common.P_album(album_id, response_type='json')
album = album.jsonify() album = album.jsonify()
return flasktools.json_response(album) return flasktools.json_response(album)
@site.route('/album/<album_id>.zip') @site.route('/album/<album_id>.zip')
def get_album_zip(album_id): def get_album_zip(album_id):
common.permission_manager.basic()
album = common.P_album(album_id, response_type='html') album = common.P_album(album_id, response_type='html')
recursive = request.args.get('recursive', True) recursive = request.args.get('recursive', True)
@ -58,6 +61,7 @@ def get_album_zip(album_id):
@site.route('/album/<album_id>/add_child', methods=['POST']) @site.route('/album/<album_id>/add_child', methods=['POST'])
@flasktools.required_fields(['child_id'], forbid_whitespace=True) @flasktools.required_fields(['child_id'], forbid_whitespace=True)
def post_album_add_child(album_id): def post_album_add_child(album_id):
common.permission_manager.basic()
child_ids = stringtools.comma_space_split(request.form['child_id']) child_ids = stringtools.comma_space_split(request.form['child_id'])
with common.P.transaction: with common.P.transaction:
album = common.P_album(album_id, response_type='json') album = common.P_album(album_id, response_type='json')
@ -71,6 +75,7 @@ def post_album_add_child(album_id):
@site.route('/album/<album_id>/remove_child', methods=['POST']) @site.route('/album/<album_id>/remove_child', methods=['POST'])
@flasktools.required_fields(['child_id'], forbid_whitespace=True) @flasktools.required_fields(['child_id'], forbid_whitespace=True)
def post_album_remove_child(album_id): def post_album_remove_child(album_id):
common.permission_manager.basic()
child_ids = stringtools.comma_space_split(request.form['child_id']) child_ids = stringtools.comma_space_split(request.form['child_id'])
with common.P.transaction: with common.P.transaction:
album = common.P_album(album_id, response_type='json') album = common.P_album(album_id, response_type='json')
@ -81,6 +86,7 @@ def post_album_remove_child(album_id):
@site.route('/album/<album_id>/remove_thumbnail_photo', methods=['POST']) @site.route('/album/<album_id>/remove_thumbnail_photo', methods=['POST'])
def post_album_remove_thumbnail_photo(album_id): def post_album_remove_thumbnail_photo(album_id):
common.permission_manager.basic()
with common.P.transaction: with common.P.transaction:
album = common.P_album(album_id, response_type='json') album = common.P_album(album_id, response_type='json')
album.set_thumbnail_photo(None) album.set_thumbnail_photo(None)
@ -88,6 +94,7 @@ def post_album_remove_thumbnail_photo(album_id):
@site.route('/album/<album_id>/refresh_directories', methods=['POST']) @site.route('/album/<album_id>/refresh_directories', methods=['POST'])
def post_album_refresh_directories(album_id): def post_album_refresh_directories(album_id):
common.permission_manager.basic()
with common.P.transaction: with common.P.transaction:
album = common.P_album(album_id, response_type='json') album = common.P_album(album_id, response_type='json')
for directory in album.get_associated_directories(): for directory in album.get_associated_directories():
@ -100,6 +107,7 @@ def post_album_refresh_directories(album_id):
@site.route('/album/<album_id>/set_thumbnail_photo', methods=['POST']) @site.route('/album/<album_id>/set_thumbnail_photo', methods=['POST'])
@flasktools.required_fields(['photo_id'], forbid_whitespace=True) @flasktools.required_fields(['photo_id'], forbid_whitespace=True)
def post_album_set_thumbnail_photo(album_id): def post_album_set_thumbnail_photo(album_id):
common.permission_manager.basic()
with common.P.transaction: with common.P.transaction:
album = common.P_album(album_id, response_type='json') album = common.P_album(album_id, response_type='json')
photo = common.P_photo(request.form['photo_id'], response_type='json') photo = common.P_photo(request.form['photo_id'], response_type='json')
@ -114,7 +122,7 @@ def post_album_add_photo(album_id):
''' '''
Add a photo or photos to this album. Add a photo or photos to this album.
''' '''
common.permission_manager.basic()
photo_ids = stringtools.comma_space_split(request.form['photo_id']) photo_ids = stringtools.comma_space_split(request.form['photo_id'])
with common.P.transaction: with common.P.transaction:
album = common.P_album(album_id, response_type='json') album = common.P_album(album_id, response_type='json')
@ -129,6 +137,7 @@ def post_album_remove_photo(album_id):
''' '''
Remove a photo or photos from this album. Remove a photo or photos from this album.
''' '''
common.permission_manager.basic()
photo_ids = stringtools.comma_space_split(request.form['photo_id']) photo_ids = stringtools.comma_space_split(request.form['photo_id'])
with common.P.transaction: with common.P.transaction:
album = common.P_album(album_id, response_type='json') album = common.P_album(album_id, response_type='json')
@ -144,6 +153,7 @@ def post_album_add_tag(album_id):
''' '''
Apply a tag to every photo in the album. Apply a tag to every photo in the album.
''' '''
common.permission_manager.basic()
response = {} response = {}
with common.P.transaction: with common.P.transaction:
album = common.P_album(album_id, response_type='json') album = common.P_album(album_id, response_type='json')
@ -168,6 +178,7 @@ def post_album_edit(album_id):
''' '''
Edit the title / description. Edit the title / description.
''' '''
common.permission_manager.basic()
title = request.form.get('title', None) title = request.form.get('title', None)
description = request.form.get('description', None) description = request.form.get('description', None)
@ -180,6 +191,7 @@ def post_album_edit(album_id):
@site.route('/album/<album_id>/show_in_folder', methods=['POST']) @site.route('/album/<album_id>/show_in_folder', methods=['POST'])
def post_album_show_in_folder(album_id): def post_album_show_in_folder(album_id):
common.permission_manager.basic()
if not request.is_localhost: if not request.is_localhost:
flask.abort(403) flask.abort(403)
@ -199,6 +211,7 @@ def post_album_show_in_folder(album_id):
# Album listings ################################################################################### # Album listings ###################################################################################
@site.route('/all_albums.json') @site.route('/all_albums.json')
@common.permission_manager.basic_decorator
@flasktools.cached_endpoint(max_age=15) @flasktools.cached_endpoint(max_age=15)
def get_all_album_names(): def get_all_album_names():
all_albums = {album.id: album.display_name for album in common.P.get_albums()} all_albums = {album.id: album.display_name for album in common.P.get_albums()}
@ -207,6 +220,7 @@ def get_all_album_names():
@site.route('/albums') @site.route('/albums')
def get_albums_html(): def get_albums_html():
common.permission_manager.basic()
albums = list(common.P.get_root_albums()) albums = list(common.P.get_root_albums())
albums.sort(key=lambda x: x.display_name.lower()) albums.sort(key=lambda x: x.display_name.lower())
response = common.render_template( response = common.render_template(
@ -219,6 +233,7 @@ def get_albums_html():
@site.route('/albums.json') @site.route('/albums.json')
def get_albums_json(): def get_albums_json():
common.permission_manager.basic()
albums = list(common.P.get_albums()) albums = list(common.P.get_albums())
albums.sort(key=lambda x: x.display_name.lower()) albums.sort(key=lambda x: x.display_name.lower())
albums = [album.jsonify(include_photos=False) for album in albums] albums = [album.jsonify(include_photos=False) for album in albums]
@ -228,6 +243,7 @@ def get_albums_json():
@site.route('/albums/create_album', methods=['POST']) @site.route('/albums/create_album', methods=['POST'])
def post_albums_create(): def post_albums_create():
common.permission_manager.basic()
title = request.form.get('title', None) title = request.form.get('title', None)
description = request.form.get('description', None) description = request.form.get('description', None)
parent_id = request.form.get('parent_id', None) parent_id = request.form.get('parent_id', None)
@ -246,6 +262,7 @@ def post_albums_create():
@site.route('/album/<album_id>/delete', methods=['POST']) @site.route('/album/<album_id>/delete', methods=['POST'])
def post_album_delete(album_id): def post_album_delete(album_id):
common.permission_manager.basic()
with common.P.transaction: with common.P.transaction:
album = common.P_album(album_id, response_type='json') album = common.P_album(album_id, response_type='json')
album.delete() album.delete()

View File

@ -10,10 +10,12 @@ session_manager = common.session_manager
@site.route('/') @site.route('/')
def root(): def root():
common.permission_manager.global_public()
motd = random.choice(common.P.config['motd_strings']) motd = random.choice(common.P.config['motd_strings'])
return common.render_template(request, 'root.html', motd=motd) return common.render_template(request, 'root.html', motd=motd)
@site.route('/favicon.ico') @site.route('/favicon.ico')
@site.route('/favicon.png') @site.route('/favicon.png')
def favicon(): def favicon():
common.permission_manager.global_public()
return flask.send_file(common.FAVICON_PATH.absolute_path) return flask.send_file(common.FAVICON_PATH.absolute_path)

View File

@ -13,12 +13,14 @@ session_manager = common.session_manager
@site.route('/bookmark/<bookmark_id>.json') @site.route('/bookmark/<bookmark_id>.json')
def get_bookmark_json(bookmark_id): def get_bookmark_json(bookmark_id):
common.permission_manager.basic()
bookmark = common.P_bookmark(bookmark_id, response_type='json') bookmark = common.P_bookmark(bookmark_id, response_type='json')
response = bookmark.jsonify() response = bookmark.jsonify()
return flasktools.json_response(response) return flasktools.json_response(response)
@site.route('/bookmark/<bookmark_id>/edit', methods=['POST']) @site.route('/bookmark/<bookmark_id>/edit', methods=['POST'])
def post_bookmark_edit(bookmark_id): def post_bookmark_edit(bookmark_id):
common.permission_manager.basic()
with common.P.transaction: with common.P.transaction:
bookmark = common.P_bookmark(bookmark_id, response_type='json') bookmark = common.P_bookmark(bookmark_id, response_type='json')
# Emptystring is okay for titles, but not for URL. # Emptystring is okay for titles, but not for URL.
@ -34,6 +36,7 @@ def post_bookmark_edit(bookmark_id):
@site.route('/bookmarks.atom') @site.route('/bookmarks.atom')
def get_bookmarks_atom(): def get_bookmarks_atom():
common.permission_manager.basic()
bookmarks = common.P.get_bookmarks() bookmarks = common.P.get_bookmarks()
response = etiquette.helpers.make_atom_feed( response = etiquette.helpers.make_atom_feed(
bookmarks, bookmarks,
@ -45,11 +48,13 @@ def get_bookmarks_atom():
@site.route('/bookmarks') @site.route('/bookmarks')
def get_bookmarks_html(): def get_bookmarks_html():
common.permission_manager.basic()
bookmarks = list(common.P.get_bookmarks()) bookmarks = list(common.P.get_bookmarks())
return common.render_template(request, 'bookmarks.html', bookmarks=bookmarks) return common.render_template(request, 'bookmarks.html', bookmarks=bookmarks)
@site.route('/bookmarks.json') @site.route('/bookmarks.json')
def get_bookmarks_json(): def get_bookmarks_json():
common.permission_manager.basic()
bookmarks = [b.jsonify() for b in common.P.get_bookmarks()] bookmarks = [b.jsonify() for b in common.P.get_bookmarks()]
return flasktools.json_response(bookmarks) return flasktools.json_response(bookmarks)
@ -58,6 +63,7 @@ def get_bookmarks_json():
@site.route('/bookmarks/create_bookmark', methods=['POST']) @site.route('/bookmarks/create_bookmark', methods=['POST'])
@flasktools.required_fields(['url'], forbid_whitespace=True) @flasktools.required_fields(['url'], forbid_whitespace=True)
def post_bookmark_create(): def post_bookmark_create():
common.permission_manager.basic()
url = request.form['url'] url = request.form['url']
title = request.form.get('title', None) title = request.form.get('title', None)
user = session_manager.get(request).user user = session_manager.get(request).user
@ -69,6 +75,7 @@ def post_bookmark_create():
@site.route('/bookmark/<bookmark_id>/delete', methods=['POST']) @site.route('/bookmark/<bookmark_id>/delete', methods=['POST'])
def post_bookmark_delete(bookmark_id): def post_bookmark_delete(bookmark_id):
common.permission_manager.basic()
with common.P.transaction: with common.P.transaction:
bookmark = common.P_bookmark(bookmark_id, response_type='json') bookmark = common.P_bookmark(bookmark_id, response_type='json')
bookmark.delete() bookmark.delete()

View File

@ -25,11 +25,13 @@ photo_download_zip_tokens = cacheclass.Cache(maxlen=100)
@site.route('/photo/<photo_id>') @site.route('/photo/<photo_id>')
def get_photo_html(photo_id): def get_photo_html(photo_id):
common.permission_manager.basic()
photo = common.P_photo(photo_id, response_type='html') photo = common.P_photo(photo_id, response_type='html')
return common.render_template(request, 'photo.html', photo=photo) return common.render_template(request, 'photo.html', photo=photo)
@site.route('/photo/<photo_id>.json') @site.route('/photo/<photo_id>.json')
def get_photo_json(photo_id): def get_photo_json(photo_id):
common.permission_manager.basic()
photo = common.P_photo(photo_id, response_type='json') photo = common.P_photo(photo_id, response_type='json')
photo = photo.jsonify() photo = photo.jsonify()
photo = flasktools.json_response(photo) photo = flasktools.json_response(photo)
@ -38,6 +40,7 @@ def get_photo_json(photo_id):
@site.route('/photo/<photo_id>/download') @site.route('/photo/<photo_id>/download')
@site.route('/photo/<photo_id>/download/<basename>') @site.route('/photo/<photo_id>/download/<basename>')
def get_file(photo_id, basename=None): def get_file(photo_id, basename=None):
common.permission_manager.basic()
photo_id = photo_id.split('.')[0] photo_id = photo_id.split('.')[0]
photo = common.P.get_photo(photo_id) photo = common.P.get_photo(photo_id)
@ -63,6 +66,7 @@ def get_file(photo_id, basename=None):
@site.route('/photo/<photo_id>/thumbnail') @site.route('/photo/<photo_id>/thumbnail')
@site.route('/photo/<photo_id>/thumbnail/<basename>') @site.route('/photo/<photo_id>/thumbnail/<basename>')
@common.permission_manager.basic_decorator
@flasktools.cached_endpoint(max_age=common.BROWSER_CACHE_DURATION) @flasktools.cached_endpoint(max_age=common.BROWSER_CACHE_DURATION)
def get_thumbnail(photo_id, basename=None): def get_thumbnail(photo_id, basename=None):
photo_id = photo_id.split('.')[0] photo_id = photo_id.split('.')[0]
@ -90,6 +94,7 @@ def get_thumbnail(photo_id, basename=None):
@site.route('/photo/<photo_id>/delete', methods=['POST']) @site.route('/photo/<photo_id>/delete', methods=['POST'])
def post_photo_delete(photo_id): def post_photo_delete(photo_id):
common.permission_manager.basic()
delete_file = request.form.get('delete_file', False) delete_file = request.form.get('delete_file', False)
delete_file = stringtools.truthystring(delete_file) delete_file = stringtools.truthystring(delete_file)
with common.P.transaction: with common.P.transaction:
@ -122,6 +127,7 @@ def post_photo_add_tag(photo_id):
''' '''
Add a tag to this photo. Add a tag to this photo.
''' '''
common.permission_manager.basic()
response = post_photo_add_remove_tag_core( response = post_photo_add_remove_tag_core(
photo_ids=photo_id, photo_ids=photo_id,
tagname=request.form['tagname'], tagname=request.form['tagname'],
@ -135,6 +141,7 @@ def post_photo_copy_tags(photo_id):
''' '''
Copy the tags from another photo. Copy the tags from another photo.
''' '''
common.permission_manager.basic()
with common.P.transaction: with common.P.transaction:
photo = common.P_photo(photo_id, response_type='json') photo = common.P_photo(photo_id, response_type='json')
other = common.P_photo(request.form['other_photo'], response_type='json') other = common.P_photo(request.form['other_photo'], response_type='json')
@ -147,6 +154,7 @@ def post_photo_remove_tag(photo_id):
''' '''
Remove a tag from this photo. Remove a tag from this photo.
''' '''
common.permission_manager.basic()
response = post_photo_add_remove_tag_core( response = post_photo_add_remove_tag_core(
photo_ids=photo_id, photo_ids=photo_id,
tagname=request.form['tagname'], tagname=request.form['tagname'],
@ -157,6 +165,7 @@ def post_photo_remove_tag(photo_id):
@site.route('/batch/photos/add_tag', methods=['POST']) @site.route('/batch/photos/add_tag', methods=['POST'])
@flasktools.required_fields(['photo_ids', 'tagname'], forbid_whitespace=True) @flasktools.required_fields(['photo_ids', 'tagname'], forbid_whitespace=True)
def post_batch_photos_add_tag(): def post_batch_photos_add_tag():
common.permission_manager.basic()
response = post_photo_add_remove_tag_core( response = post_photo_add_remove_tag_core(
photo_ids=request.form['photo_ids'], photo_ids=request.form['photo_ids'],
tagname=request.form['tagname'], tagname=request.form['tagname'],
@ -167,6 +176,7 @@ def post_batch_photos_add_tag():
@site.route('/batch/photos/remove_tag', methods=['POST']) @site.route('/batch/photos/remove_tag', methods=['POST'])
@flasktools.required_fields(['photo_ids', 'tagname'], forbid_whitespace=True) @flasktools.required_fields(['photo_ids', 'tagname'], forbid_whitespace=True)
def post_batch_photos_remove_tag(): def post_batch_photos_remove_tag():
common.permission_manager.basic()
response = post_photo_add_remove_tag_core( response = post_photo_add_remove_tag_core(
photo_ids=request.form['photo_ids'], photo_ids=request.form['photo_ids'],
tagname=request.form['tagname'], tagname=request.form['tagname'],
@ -178,6 +188,7 @@ def post_batch_photos_remove_tag():
@site.route('/photo/<photo_id>/generate_thumbnail', methods=['POST']) @site.route('/photo/<photo_id>/generate_thumbnail', methods=['POST'])
def post_photo_generate_thumbnail(photo_id): def post_photo_generate_thumbnail(photo_id):
common.permission_manager.basic()
special = request.form.to_dict() special = request.form.to_dict()
with common.P.transaction: with common.P.transaction:
@ -212,17 +223,20 @@ def post_photo_refresh_metadata_core(photo_ids):
@site.route('/photo/<photo_id>/refresh_metadata', methods=['POST']) @site.route('/photo/<photo_id>/refresh_metadata', methods=['POST'])
def post_photo_refresh_metadata(photo_id): def post_photo_refresh_metadata(photo_id):
common.permission_manager.basic()
response = post_photo_refresh_metadata_core(photo_ids=photo_id) response = post_photo_refresh_metadata_core(photo_ids=photo_id)
return response return response
@site.route('/batch/photos/refresh_metadata', methods=['POST']) @site.route('/batch/photos/refresh_metadata', methods=['POST'])
@flasktools.required_fields(['photo_ids'], forbid_whitespace=True) @flasktools.required_fields(['photo_ids'], forbid_whitespace=True)
def post_batch_photos_refresh_metadata(): def post_batch_photos_refresh_metadata():
common.permission_manager.basic()
response = post_photo_refresh_metadata_core(photo_ids=request.form['photo_ids']) response = post_photo_refresh_metadata_core(photo_ids=request.form['photo_ids'])
return response return response
@site.route('/photo/<photo_id>/set_searchhidden', methods=['POST']) @site.route('/photo/<photo_id>/set_searchhidden', methods=['POST'])
def post_photo_set_searchhidden(photo_id): def post_photo_set_searchhidden(photo_id):
common.permission_manager.basic()
with common.P.transaction: with common.P.transaction:
photo = common.P_photo(photo_id, response_type='json') photo = common.P_photo(photo_id, response_type='json')
photo.set_searchhidden(True) photo.set_searchhidden(True)
@ -230,6 +244,7 @@ def post_photo_set_searchhidden(photo_id):
@site.route('/photo/<photo_id>/unset_searchhidden', methods=['POST']) @site.route('/photo/<photo_id>/unset_searchhidden', methods=['POST'])
def post_photo_unset_searchhidden(photo_id): def post_photo_unset_searchhidden(photo_id):
common.permission_manager.basic()
with common.P.transaction: with common.P.transaction:
photo = common.P_photo(photo_id, response_type='json') photo = common.P_photo(photo_id, response_type='json')
photo.set_searchhidden(False) photo.set_searchhidden(False)
@ -249,6 +264,7 @@ def post_batch_photos_searchhidden_core(photo_ids, searchhidden):
@site.route('/photo/<photo_id>/show_in_folder', methods=['POST']) @site.route('/photo/<photo_id>/show_in_folder', methods=['POST'])
def post_photo_show_in_folder(photo_id): def post_photo_show_in_folder(photo_id):
common.permission_manager.basic()
if not request.is_localhost: if not request.is_localhost:
flask.abort(403) flask.abort(403)
@ -267,6 +283,7 @@ def post_photo_show_in_folder(photo_id):
@site.route('/batch/photos/set_searchhidden', methods=['POST']) @site.route('/batch/photos/set_searchhidden', methods=['POST'])
@flasktools.required_fields(['photo_ids'], forbid_whitespace=True) @flasktools.required_fields(['photo_ids'], forbid_whitespace=True)
def post_batch_photos_set_searchhidden(): def post_batch_photos_set_searchhidden():
common.permission_manager.basic()
photo_ids = request.form['photo_ids'] photo_ids = request.form['photo_ids']
response = post_batch_photos_searchhidden_core(photo_ids=photo_ids, searchhidden=True) response = post_batch_photos_searchhidden_core(photo_ids=photo_ids, searchhidden=True)
return response return response
@ -274,6 +291,7 @@ def post_batch_photos_set_searchhidden():
@site.route('/batch/photos/unset_searchhidden', methods=['POST']) @site.route('/batch/photos/unset_searchhidden', methods=['POST'])
@flasktools.required_fields(['photo_ids'], forbid_whitespace=True) @flasktools.required_fields(['photo_ids'], forbid_whitespace=True)
def post_batch_photos_unset_searchhidden(): def post_batch_photos_unset_searchhidden():
common.permission_manager.basic()
photo_ids = request.form['photo_ids'] photo_ids = request.form['photo_ids']
response = post_batch_photos_searchhidden_core(photo_ids=photo_ids, searchhidden=False) response = post_batch_photos_searchhidden_core(photo_ids=photo_ids, searchhidden=False)
return response return response
@ -282,6 +300,7 @@ def post_batch_photos_unset_searchhidden():
@site.route('/clipboard') @site.route('/clipboard')
def get_clipboard_page(): def get_clipboard_page():
common.permission_manager.basic()
return common.render_template(request, 'clipboard.html') return common.render_template(request, 'clipboard.html')
@site.route('/batch/photos', methods=['POST']) @site.route('/batch/photos', methods=['POST'])
@ -290,6 +309,7 @@ def post_batch_photos():
''' '''
Return a list of photo.jsonify() for each requested photo id. Return a list of photo.jsonify() for each requested photo id.
''' '''
common.permission_manager.basic()
photo_ids = request.form['photo_ids'] photo_ids = request.form['photo_ids']
photo_ids = stringtools.comma_space_split(photo_ids) photo_ids = stringtools.comma_space_split(photo_ids)
@ -302,6 +322,7 @@ def post_batch_photos():
@site.route('/batch/photos/photo_card', methods=['POST']) @site.route('/batch/photos/photo_card', methods=['POST'])
@flasktools.required_fields(['photo_ids'], forbid_whitespace=True) @flasktools.required_fields(['photo_ids'], forbid_whitespace=True)
def post_batch_photos_photo_cards(): def post_batch_photos_photo_cards():
common.permission_manager.basic()
photo_ids = request.form['photo_ids'] photo_ids = request.form['photo_ids']
photo_ids = stringtools.comma_space_split(photo_ids) photo_ids = stringtools.comma_space_split(photo_ids)
@ -333,6 +354,7 @@ def get_batch_photos_download_zip(zip_token):
After the user has generated their zip token, they can retrieve After the user has generated their zip token, they can retrieve
that zip file. that zip file.
''' '''
common.permission_manager.basic()
zip_token = zip_token.split('.')[0] zip_token = zip_token.split('.')[0]
try: try:
photo_ids = photo_download_zip_tokens[zip_token] photo_ids = photo_download_zip_tokens[zip_token]
@ -362,6 +384,7 @@ def post_batch_photos_download_zip():
so the way this works is we generate a token representing the photoset so the way this works is we generate a token representing the photoset
that they want, and then they can retrieve the zip itself via GET. that they want, and then they can retrieve the zip itself via GET.
''' '''
common.permission_manager.basic()
photo_ids = request.form['photo_ids'] photo_ids = request.form['photo_ids']
photo_ids = stringtools.comma_space_split(photo_ids) photo_ids = stringtools.comma_space_split(photo_ids)
@ -436,6 +459,7 @@ def get_search_core():
@site.route('/search_embed') @site.route('/search_embed')
def get_search_embed(): def get_search_embed():
common.permission_manager.basic()
search = get_search_core() search = get_search_core()
response = common.render_template( response = common.render_template(
request, request,
@ -447,6 +471,8 @@ def get_search_embed():
@site.route('/search') @site.route('/search')
def get_search_html(): def get_search_html():
common.permission_manager.basic()
search = get_search_core() search = get_search_core()
search.kwargs.view = request.args.get('view', 'grid') search.kwargs.view = request.args.get('view', 'grid')
@ -496,6 +522,7 @@ def get_search_html():
@site.route('/search.atom') @site.route('/search.atom')
def get_search_atom(): def get_search_atom():
common.permission_manager.basic()
search = get_search_core() search = get_search_core()
soup = etiquette.helpers.make_atom_feed( soup = etiquette.helpers.make_atom_feed(
search.results, search.results,
@ -508,6 +535,7 @@ def get_search_atom():
@site.route('/search.json') @site.route('/search.json')
def get_search_json(): def get_search_json():
common.permission_manager.basic()
search = get_search_core() search = get_search_core()
response = search.jsonify() response = search.jsonify()
return flasktools.json_response(response) return flasktools.json_response(response)
@ -516,5 +544,6 @@ def get_search_json():
@site.route('/swipe') @site.route('/swipe')
def get_swipe(): def get_swipe():
common.permission_manager.basic()
response = common.render_template(request, 'swipe.html') response = common.render_template(request, 'swipe.html')
return response return response

View File

@ -15,11 +15,13 @@ session_manager = common.session_manager
@site.route('/tags/<specific_tag>') @site.route('/tags/<specific_tag>')
@site.route('/tags/<specific_tag>.json') @site.route('/tags/<specific_tag>.json')
def get_tags_specific_redirect(specific_tag): def get_tags_specific_redirect(specific_tag):
common.permission_manager.basic()
return flask.redirect(request.url.replace('/tags/', '/tag/')) return flask.redirect(request.url.replace('/tags/', '/tag/'))
@site.route('/tagid/<tag_id>') @site.route('/tagid/<tag_id>')
@site.route('/tagid/<tag_id>.json') @site.route('/tagid/<tag_id>.json')
def get_tag_id_redirect(tag_id): def get_tag_id_redirect(tag_id):
common.permission_manager.basic()
if request.path.endswith('.json'): if request.path.endswith('.json'):
tag = common.P_tag_id(tag_id, response_type='json') tag = common.P_tag_id(tag_id, response_type='json')
else: else:
@ -31,6 +33,7 @@ def get_tag_id_redirect(tag_id):
@site.route('/tag/<specific_tag_name>.json') @site.route('/tag/<specific_tag_name>.json')
def get_tag_json(specific_tag_name): def get_tag_json(specific_tag_name):
common.permission_manager.basic()
specific_tag = common.P_tag(specific_tag_name, response_type='json') specific_tag = common.P_tag(specific_tag_name, response_type='json')
if specific_tag.name != specific_tag_name: if specific_tag.name != specific_tag_name:
new_url = f'/tag/{specific_tag.name}.json' + request.query_string.decode('utf-8') new_url = f'/tag/{specific_tag.name}.json' + request.query_string.decode('utf-8')
@ -44,6 +47,7 @@ def get_tag_json(specific_tag_name):
@site.route('/tag/<tagname>/edit', methods=['POST']) @site.route('/tag/<tagname>/edit', methods=['POST'])
def post_tag_edit(tagname): def post_tag_edit(tagname):
common.permission_manager.basic()
with common.P.transaction: with common.P.transaction:
tag = common.P_tag(tagname, response_type='json') tag = common.P_tag(tagname, response_type='json')
name = request.form.get('name', '').strip() name = request.form.get('name', '').strip()
@ -59,6 +63,7 @@ def post_tag_edit(tagname):
@site.route('/tag/<tagname>/add_child', methods=['POST']) @site.route('/tag/<tagname>/add_child', methods=['POST'])
@flasktools.required_fields(['child_name'], forbid_whitespace=True) @flasktools.required_fields(['child_name'], forbid_whitespace=True)
def post_tag_add_child(tagname): def post_tag_add_child(tagname):
common.permission_manager.basic()
with common.P.transaction: with common.P.transaction:
parent = common.P_tag(tagname, response_type='json') parent = common.P_tag(tagname, response_type='json')
child = common.P_tag(request.form['child_name'], response_type='json') child = common.P_tag(request.form['child_name'], response_type='json')
@ -69,6 +74,7 @@ def post_tag_add_child(tagname):
@site.route('/tag/<tagname>/add_synonym', methods=['POST']) @site.route('/tag/<tagname>/add_synonym', methods=['POST'])
@flasktools.required_fields(['syn_name'], forbid_whitespace=True) @flasktools.required_fields(['syn_name'], forbid_whitespace=True)
def post_tag_add_synonym(tagname): def post_tag_add_synonym(tagname):
common.permission_manager.basic()
syn_name = request.form['syn_name'] syn_name = request.form['syn_name']
with common.P.transaction: with common.P.transaction:
@ -81,6 +87,7 @@ def post_tag_add_synonym(tagname):
@site.route('/tag/<tagname>/remove_child', methods=['POST']) @site.route('/tag/<tagname>/remove_child', methods=['POST'])
@flasktools.required_fields(['child_name'], forbid_whitespace=True) @flasktools.required_fields(['child_name'], forbid_whitespace=True)
def post_tag_remove_child(tagname): def post_tag_remove_child(tagname):
common.permission_manager.basic()
with common.P.transaction: with common.P.transaction:
parent = common.P_tag(tagname, response_type='json') parent = common.P_tag(tagname, response_type='json')
child = common.P_tag(request.form['child_name'], response_type='json') child = common.P_tag(request.form['child_name'], response_type='json')
@ -91,6 +98,7 @@ def post_tag_remove_child(tagname):
@site.route('/tag/<tagname>/remove_synonym', methods=['POST']) @site.route('/tag/<tagname>/remove_synonym', methods=['POST'])
@flasktools.required_fields(['syn_name'], forbid_whitespace=True) @flasktools.required_fields(['syn_name'], forbid_whitespace=True)
def post_tag_remove_synonym(tagname): def post_tag_remove_synonym(tagname):
common.permission_manager.basic()
syn_name = request.form['syn_name'] syn_name = request.form['syn_name']
with common.P.transaction: with common.P.transaction:
@ -103,6 +111,7 @@ def post_tag_remove_synonym(tagname):
# Tag listings ##################################################################################### # Tag listings #####################################################################################
@site.route('/all_tags.json') @site.route('/all_tags.json')
@common.permission_manager.basic_decorator
@flasktools.cached_endpoint(max_age=15) @flasktools.cached_endpoint(max_age=15)
def get_all_tag_names(): def get_all_tag_names():
all_tags = list(common.P.get_all_tag_names()) all_tags = list(common.P.get_all_tag_names())
@ -113,6 +122,7 @@ def get_all_tag_names():
@site.route('/tag/<specific_tag_name>') @site.route('/tag/<specific_tag_name>')
@site.route('/tags') @site.route('/tags')
def get_tags_html(specific_tag_name=None): def get_tags_html(specific_tag_name=None):
common.permission_manager.basic()
if specific_tag_name is None: if specific_tag_name is None:
specific_tag = None specific_tag = None
else: else:
@ -151,6 +161,7 @@ def get_tags_html(specific_tag_name=None):
@site.route('/tags.json') @site.route('/tags.json')
def get_tags_json(): def get_tags_json():
common.permission_manager.basic()
include_synonyms = request.args.get('synonyms') include_synonyms = request.args.get('synonyms')
include_synonyms = include_synonyms is None or stringtools.truthystring(include_synonyms) include_synonyms = include_synonyms is None or stringtools.truthystring(include_synonyms)
@ -164,6 +175,7 @@ def get_tags_json():
@site.route('/tags/create_tag', methods=['POST']) @site.route('/tags/create_tag', methods=['POST'])
@flasktools.required_fields(['name'], forbid_whitespace=True) @flasktools.required_fields(['name'], forbid_whitespace=True)
def post_tag_create(): def post_tag_create():
common.permission_manager.basic()
name = request.form['name'] name = request.form['name']
description = request.form.get('description', None) description = request.form.get('description', None)
@ -175,6 +187,7 @@ def post_tag_create():
@site.route('/tags/easybake', methods=['POST']) @site.route('/tags/easybake', methods=['POST'])
@flasktools.required_fields(['easybake_string'], forbid_whitespace=True) @flasktools.required_fields(['easybake_string'], forbid_whitespace=True)
def post_tag_easybake(): def post_tag_easybake():
common.permission_manager.basic()
easybake_string = request.form['easybake_string'] easybake_string = request.form['easybake_string']
with common.P.transaction: with common.P.transaction:
@ -184,6 +197,7 @@ def post_tag_easybake():
@site.route('/tag/<tagname>/delete', methods=['POST']) @site.route('/tag/<tagname>/delete', methods=['POST'])
def post_tag_delete(tagname): def post_tag_delete(tagname):
common.permission_manager.basic()
with common.P.transaction: with common.P.transaction:
tag = common.P_tag(tagname, response_type='json') tag = common.P_tag(tagname, response_type='json')
tag.delete() tag.delete()

View File

@ -14,11 +14,13 @@ session_manager = common.session_manager
@site.route('/user/<username>') @site.route('/user/<username>')
def get_user_html(username): def get_user_html(username):
common.permission_manager.basic()
user = common.P_user(username, response_type='html') user = common.P_user(username, response_type='html')
return common.render_template(request, 'user.html', user=user) return common.render_template(request, 'user.html', user=user)
@site.route('/user/<username>.json') @site.route('/user/<username>.json')
def get_user_json(username): def get_user_json(username):
common.permission_manager.basic()
user = common.P_user(username, response_type='json') user = common.P_user(username, response_type='json')
user = user.jsonify() user = user.jsonify()
return flasktools.json_response(user) return flasktools.json_response(user)
@ -26,6 +28,7 @@ def get_user_json(username):
@site.route('/userid/<user_id>') @site.route('/userid/<user_id>')
@site.route('/userid/<user_id>.json') @site.route('/userid/<user_id>.json')
def get_user_id_redirect(user_id): def get_user_id_redirect(user_id):
common.permission_manager.basic()
if request.path.endswith('.json'): if request.path.endswith('.json'):
user = common.P_user_id(user_id, response_type='json') user = common.P_user_id(user_id, response_type='json')
else: else:
@ -37,6 +40,7 @@ def get_user_id_redirect(user_id):
@site.route('/user/<username>/edit', methods=['POST']) @site.route('/user/<username>/edit', methods=['POST'])
def post_user_edit(username): def post_user_edit(username):
common.permission_manager.basic()
if not request.session: if not request.session:
return flasktools.json_response(etiquette.exceptions.Unauthorized().jsonify(), status=403) return flasktools.json_response(etiquette.exceptions.Unauthorized().jsonify(), status=403)
user = common.P_user(username, response_type='json') user = common.P_user(username, response_type='json')
@ -54,6 +58,7 @@ def post_user_edit(username):
@site.route('/login', methods=['GET']) @site.route('/login', methods=['GET'])
def get_login(): def get_login():
common.permission_manager.global_public()
response = common.render_template( response = common.render_template(
request, request,
'login.html', 'login.html',
@ -66,6 +71,7 @@ def get_login():
@site.route('/login', methods=['POST']) @site.route('/login', methods=['POST'])
@flasktools.required_fields(['username', 'password']) @flasktools.required_fields(['username', 'password'])
def post_login(): def post_login():
common.permission_manager.global_public()
if request.session.user: if request.session.user:
exc = etiquette.exceptions.AlreadySignedIn() exc = etiquette.exceptions.AlreadySignedIn()
response = exc.jsonify() response = exc.jsonify()
@ -96,6 +102,7 @@ def post_login():
@site.route('/logout', methods=['POST']) @site.route('/logout', methods=['POST'])
def post_logout(): def post_logout():
common.permission_manager.basic()
session_manager.remove(request) session_manager.remove(request)
response = flasktools.json_response({}) response = flasktools.json_response({})
return response return response
@ -104,11 +111,13 @@ def post_logout():
@site.route('/register', methods=['GET']) @site.route('/register', methods=['GET'])
def get_register(): def get_register():
common.permission_manager.global_public()
return flask.redirect('/login') return flask.redirect('/login')
@site.route('/register', methods=['POST']) @site.route('/register', methods=['POST'])
@flasktools.required_fields(['username', 'password_1', 'password_2']) @flasktools.required_fields(['username', 'password_1', 'password_2'])
def post_register(): def post_register():
common.permission_manager.global_public()
if request.session.user: if request.session.user:
exc = etiquette.exceptions.AlreadySignedIn() exc = etiquette.exceptions.AlreadySignedIn()
response = exc.jsonify() response = exc.jsonify()

View File

@ -0,0 +1,43 @@
import flask; from flask import request
import functools
from voussoirkit import vlogging
log = vlogging.getLogger(__name__)
class PermissionManager:
def __init__(self, site):
self.site = site
def admin(self):
if request.is_localhost:
request.checked_permissions = True
return True
else:
return flask.abort(403)
def basic(self):
if request.method not in {'GET', 'POST'}:
return flask.abort(405)
elif request.is_localhost:
request.checked_permissions = True
return True
elif request.method == 'GET' and self.site.server_config['anonymous_read'] or request.session.user:
request.checked_permissions = True
return True
elif request.method == 'POST' and self.site.server_config['anonymous_write'] or request.session.user:
request.checked_permissions = True
return True
else:
return flask.abort(403)
def basic_decorator(self, endpoint):
log.debug('Decorating %s with basic_decorator.', endpoint)
@functools.wraps(endpoint)
def wrapped(*args, **kwargs):
self.basic()
return endpoint(*args, **kwargs)
return wrapped
def global_public(self):
request.checked_permissions = True

View File

@ -1,6 +1,9 @@
''' '''
This file is the gevent launcher for local / development use. This file is the gevent launcher for local / development use.
''' '''
from voussoirkit import vlogging
vlogging.earlybird_config()
import gevent.monkey; gevent.monkey.patch_all() import gevent.monkey; gevent.monkey.patch_all()
import werkzeug.middleware.proxy_fix import werkzeug.middleware.proxy_fix
@ -11,6 +14,7 @@ import sys
from voussoirkit import betterhelp from voussoirkit import betterhelp
from voussoirkit import pathclass from voussoirkit import pathclass
from voussoirkit import operatornotify
from voussoirkit import vlogging from voussoirkit import vlogging
log = vlogging.getLogger(__name__, 'etiquette_flask_dev') log = vlogging.getLogger(__name__, 'etiquette_flask_dev')
@ -79,7 +83,7 @@ def etiquette_flask_launch_argparse(args):
use_https=args.use_https, use_https=args.use_https,
) )
@vlogging.main_decorator @operatornotify.main_decorator(subject='etiquette_flask_dev', notify_every_line=True)
def main(argv): def main(argv):
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description=''' description='''