diff --git a/frontends/etiquette_flask/backend/common.py b/frontends/etiquette_flask/backend/common.py index f53d1c8..da7e13e 100644 --- a/frontends/etiquette_flask/backend/common.py +++ b/frontends/etiquette_flask/backend/common.py @@ -4,29 +4,46 @@ Use etiquette_flask_dev.py or etiquette_flask_prod.py. ''' import flask; from flask import request import functools +import json import mimetypes import traceback from voussoirkit import bytestring +from voussoirkit import configlayers from voussoirkit import flasktools from voussoirkit import pathclass +from voussoirkit import vlogging import etiquette from . import client_caching from . import jinja_filters +from . import permissions from . import sessions +log = vlogging.getLogger(__name__) + +# Constants ######################################################################################## + +DEFAULT_SERVER_CONFIG = { + 'anonymous_read': True, + 'anonymous_write': True, +} + +BROWSER_CACHE_DURATION = 180 + # Flask init ####################################################################################### # __file__ = .../etiquette_flask/backend/common.py # root_dir = .../etiquette_flask root_dir = pathclass.Path(__file__).parent.parent +P = None + TEMPLATE_DIR = root_dir.with_child('templates') STATIC_DIR = root_dir.with_child('static') FAVICON_PATH = STATIC_DIR.with_child('favicon.png') -BROWSER_CACHE_DURATION = 180 +SERVER_CONFIG_FILENAME = 'etiquette_flask_config.json' site = flask.Flask( __name__, @@ -37,6 +54,7 @@ site.config.update( SEND_FILE_MAX_AGE_DEFAULT=BROWSER_CACHE_DURATION, TEMPLATES_AUTO_RELOAD=True, ) +site.server_config = None site.jinja_env.add_extension('jinja2.ext.do') site.jinja_env.trim_blocks = True site.jinja_env.lstrip_blocks = True @@ -49,6 +67,7 @@ file_etag_manager = client_caching.FileEtagManager( max_filesize=5 * bytestring.MEBIBYTE, max_age=BROWSER_CACHE_DURATION, ) +permission_manager = permissions.PermissionManager(site) # Response wrappers ################################################################################ @@ -80,10 +99,18 @@ def before_request(): if site.localhost_only and not request.is_localhost: 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/': + permission_manager.global_public() + session_manager._before_request(request) @site.after_request 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 = session_manager._after_request(response) return response @@ -277,3 +304,22 @@ def send_file(filepath, override_mimetype=None): def init_photodb(*args, **kwargs): global P 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)) diff --git a/frontends/etiquette_flask/backend/endpoints/admin_endpoints.py b/frontends/etiquette_flask/backend/endpoints/admin_endpoints.py index 66569ad..d8a523c 100644 --- a/frontends/etiquette_flask/backend/endpoints/admin_endpoints.py +++ b/frontends/etiquette_flask/backend/endpoints/admin_endpoints.py @@ -15,8 +15,7 @@ session_manager = common.session_manager @site.route('/admin') def get_admin(): - if not request.is_localhost: - flask.abort(403) + common.permission_manager.admin() counts = dotdict.DotDict({ 'albums': common.P.get_album_count(), @@ -36,8 +35,7 @@ def get_admin(): @site.route('/admin/dbdownload') def get_dbdump(): - if not request.is_localhost: - flask.abort(403) + common.permission_manager.admin() with common.P.transaction: binary = common.P.database_filepath.read('rb') @@ -52,24 +50,23 @@ def get_dbdump(): @site.route('/admin/clear_sessions', methods=['POST']) def post_clear_sessions(): - if not request.is_localhost: - return flasktools.json_response({}, status=403) + common.permission_manager.admin() session_manager.clear() return flasktools.json_response({}) @site.route('/admin/reload_config', methods=['POST']) def post_reload_config(): - if not request.is_localhost: - return flasktools.json_response({}, status=403) + common.permission_manager.admin() common.P.load_config() + common.load_config() + return flasktools.json_response({}) @site.route('/admin/uncache', methods=['POST']) def post_uncache(): - if not request.is_localhost: - return flasktools.json_response({}, status=403) + common.permission_manager.admin() with common.P.transaction: for cache in common.P.caches.values(): diff --git a/frontends/etiquette_flask/backend/endpoints/album_endpoints.py b/frontends/etiquette_flask/backend/endpoints/album_endpoints.py index 22a24c9..2b29408 100644 --- a/frontends/etiquette_flask/backend/endpoints/album_endpoints.py +++ b/frontends/etiquette_flask/backend/endpoints/album_endpoints.py @@ -17,6 +17,7 @@ session_manager = common.session_manager @site.route('/album/') def get_album_html(album_id): + common.permission_manager.basic() album = common.P_album(album_id, response_type='html') response = common.render_template( request, @@ -28,12 +29,14 @@ def get_album_html(album_id): @site.route('/album/.json') def get_album_json(album_id): + common.permission_manager.basic() album = common.P_album(album_id, response_type='json') album = album.jsonify() return flasktools.json_response(album) @site.route('/album/.zip') def get_album_zip(album_id): + common.permission_manager.basic() album = common.P_album(album_id, response_type='html') recursive = request.args.get('recursive', True) @@ -58,6 +61,7 @@ def get_album_zip(album_id): @site.route('/album//add_child', methods=['POST']) @flasktools.required_fields(['child_id'], forbid_whitespace=True) def post_album_add_child(album_id): + common.permission_manager.basic() child_ids = stringtools.comma_space_split(request.form['child_id']) with common.P.transaction: album = common.P_album(album_id, response_type='json') @@ -71,6 +75,7 @@ def post_album_add_child(album_id): @site.route('/album//remove_child', methods=['POST']) @flasktools.required_fields(['child_id'], forbid_whitespace=True) def post_album_remove_child(album_id): + common.permission_manager.basic() child_ids = stringtools.comma_space_split(request.form['child_id']) with common.P.transaction: album = common.P_album(album_id, response_type='json') @@ -81,6 +86,7 @@ def post_album_remove_child(album_id): @site.route('/album//remove_thumbnail_photo', methods=['POST']) def post_album_remove_thumbnail_photo(album_id): + common.permission_manager.basic() with common.P.transaction: album = common.P_album(album_id, response_type='json') album.set_thumbnail_photo(None) @@ -88,6 +94,7 @@ def post_album_remove_thumbnail_photo(album_id): @site.route('/album//refresh_directories', methods=['POST']) def post_album_refresh_directories(album_id): + common.permission_manager.basic() with common.P.transaction: album = common.P_album(album_id, response_type='json') for directory in album.get_associated_directories(): @@ -100,6 +107,7 @@ def post_album_refresh_directories(album_id): @site.route('/album//set_thumbnail_photo', methods=['POST']) @flasktools.required_fields(['photo_id'], forbid_whitespace=True) def post_album_set_thumbnail_photo(album_id): + common.permission_manager.basic() with common.P.transaction: album = common.P_album(album_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. ''' - + common.permission_manager.basic() photo_ids = stringtools.comma_space_split(request.form['photo_id']) with common.P.transaction: 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. ''' + common.permission_manager.basic() photo_ids = stringtools.comma_space_split(request.form['photo_id']) with common.P.transaction: 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. ''' + common.permission_manager.basic() response = {} with common.P.transaction: album = common.P_album(album_id, response_type='json') @@ -168,6 +178,7 @@ def post_album_edit(album_id): ''' Edit the title / description. ''' + common.permission_manager.basic() title = request.form.get('title', None) description = request.form.get('description', None) @@ -180,6 +191,7 @@ def post_album_edit(album_id): @site.route('/album//show_in_folder', methods=['POST']) def post_album_show_in_folder(album_id): + common.permission_manager.basic() if not request.is_localhost: flask.abort(403) @@ -199,6 +211,7 @@ def post_album_show_in_folder(album_id): # Album listings ################################################################################### @site.route('/all_albums.json') +@common.permission_manager.basic_decorator @flasktools.cached_endpoint(max_age=15) def get_all_album_names(): 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') def get_albums_html(): + common.permission_manager.basic() albums = list(common.P.get_root_albums()) albums.sort(key=lambda x: x.display_name.lower()) response = common.render_template( @@ -219,6 +233,7 @@ def get_albums_html(): @site.route('/albums.json') def get_albums_json(): + common.permission_manager.basic() albums = list(common.P.get_albums()) albums.sort(key=lambda x: x.display_name.lower()) 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']) def post_albums_create(): + common.permission_manager.basic() title = request.form.get('title', None) description = request.form.get('description', None) parent_id = request.form.get('parent_id', None) @@ -246,6 +262,7 @@ def post_albums_create(): @site.route('/album//delete', methods=['POST']) def post_album_delete(album_id): + common.permission_manager.basic() with common.P.transaction: album = common.P_album(album_id, response_type='json') album.delete() diff --git a/frontends/etiquette_flask/backend/endpoints/basic_endpoints.py b/frontends/etiquette_flask/backend/endpoints/basic_endpoints.py index 3e1c13c..b06cb65 100644 --- a/frontends/etiquette_flask/backend/endpoints/basic_endpoints.py +++ b/frontends/etiquette_flask/backend/endpoints/basic_endpoints.py @@ -10,10 +10,12 @@ session_manager = common.session_manager @site.route('/') def root(): + common.permission_manager.global_public() motd = random.choice(common.P.config['motd_strings']) return common.render_template(request, 'root.html', motd=motd) @site.route('/favicon.ico') @site.route('/favicon.png') def favicon(): + common.permission_manager.global_public() return flask.send_file(common.FAVICON_PATH.absolute_path) diff --git a/frontends/etiquette_flask/backend/endpoints/bookmark_endpoints.py b/frontends/etiquette_flask/backend/endpoints/bookmark_endpoints.py index 195382a..4ef791c 100644 --- a/frontends/etiquette_flask/backend/endpoints/bookmark_endpoints.py +++ b/frontends/etiquette_flask/backend/endpoints/bookmark_endpoints.py @@ -13,12 +13,14 @@ session_manager = common.session_manager @site.route('/bookmark/.json') def get_bookmark_json(bookmark_id): + common.permission_manager.basic() bookmark = common.P_bookmark(bookmark_id, response_type='json') response = bookmark.jsonify() return flasktools.json_response(response) @site.route('/bookmark//edit', methods=['POST']) def post_bookmark_edit(bookmark_id): + common.permission_manager.basic() with common.P.transaction: bookmark = common.P_bookmark(bookmark_id, response_type='json') # Emptystring is okay for titles, but not for URL. @@ -34,6 +36,7 @@ def post_bookmark_edit(bookmark_id): @site.route('/bookmarks.atom') def get_bookmarks_atom(): + common.permission_manager.basic() bookmarks = common.P.get_bookmarks() response = etiquette.helpers.make_atom_feed( bookmarks, @@ -45,11 +48,13 @@ def get_bookmarks_atom(): @site.route('/bookmarks') def get_bookmarks_html(): + common.permission_manager.basic() bookmarks = list(common.P.get_bookmarks()) return common.render_template(request, 'bookmarks.html', bookmarks=bookmarks) @site.route('/bookmarks.json') def get_bookmarks_json(): + common.permission_manager.basic() bookmarks = [b.jsonify() for b in common.P.get_bookmarks()] return flasktools.json_response(bookmarks) @@ -58,6 +63,7 @@ def get_bookmarks_json(): @site.route('/bookmarks/create_bookmark', methods=['POST']) @flasktools.required_fields(['url'], forbid_whitespace=True) def post_bookmark_create(): + common.permission_manager.basic() url = request.form['url'] title = request.form.get('title', None) user = session_manager.get(request).user @@ -69,6 +75,7 @@ def post_bookmark_create(): @site.route('/bookmark//delete', methods=['POST']) def post_bookmark_delete(bookmark_id): + common.permission_manager.basic() with common.P.transaction: bookmark = common.P_bookmark(bookmark_id, response_type='json') bookmark.delete() diff --git a/frontends/etiquette_flask/backend/endpoints/photo_endpoints.py b/frontends/etiquette_flask/backend/endpoints/photo_endpoints.py index 5133521..fcf9db3 100644 --- a/frontends/etiquette_flask/backend/endpoints/photo_endpoints.py +++ b/frontends/etiquette_flask/backend/endpoints/photo_endpoints.py @@ -25,11 +25,13 @@ photo_download_zip_tokens = cacheclass.Cache(maxlen=100) @site.route('/photo/') def get_photo_html(photo_id): + common.permission_manager.basic() photo = common.P_photo(photo_id, response_type='html') return common.render_template(request, 'photo.html', photo=photo) @site.route('/photo/.json') def get_photo_json(photo_id): + common.permission_manager.basic() photo = common.P_photo(photo_id, response_type='json') photo = photo.jsonify() photo = flasktools.json_response(photo) @@ -38,6 +40,7 @@ def get_photo_json(photo_id): @site.route('/photo//download') @site.route('/photo//download/') def get_file(photo_id, basename=None): + common.permission_manager.basic() photo_id = photo_id.split('.')[0] photo = common.P.get_photo(photo_id) @@ -63,6 +66,7 @@ def get_file(photo_id, basename=None): @site.route('/photo//thumbnail') @site.route('/photo//thumbnail/') +@common.permission_manager.basic_decorator @flasktools.cached_endpoint(max_age=common.BROWSER_CACHE_DURATION) def get_thumbnail(photo_id, basename=None): photo_id = photo_id.split('.')[0] @@ -90,6 +94,7 @@ def get_thumbnail(photo_id, basename=None): @site.route('/photo//delete', methods=['POST']) def post_photo_delete(photo_id): + common.permission_manager.basic() delete_file = request.form.get('delete_file', False) delete_file = stringtools.truthystring(delete_file) with common.P.transaction: @@ -122,6 +127,7 @@ def post_photo_add_tag(photo_id): ''' Add a tag to this photo. ''' + common.permission_manager.basic() response = post_photo_add_remove_tag_core( photo_ids=photo_id, tagname=request.form['tagname'], @@ -135,6 +141,7 @@ def post_photo_copy_tags(photo_id): ''' Copy the tags from another photo. ''' + common.permission_manager.basic() with common.P.transaction: photo = common.P_photo(photo_id, 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. ''' + common.permission_manager.basic() response = post_photo_add_remove_tag_core( photo_ids=photo_id, tagname=request.form['tagname'], @@ -157,6 +165,7 @@ def post_photo_remove_tag(photo_id): @site.route('/batch/photos/add_tag', methods=['POST']) @flasktools.required_fields(['photo_ids', 'tagname'], forbid_whitespace=True) def post_batch_photos_add_tag(): + common.permission_manager.basic() response = post_photo_add_remove_tag_core( photo_ids=request.form['photo_ids'], tagname=request.form['tagname'], @@ -167,6 +176,7 @@ def post_batch_photos_add_tag(): @site.route('/batch/photos/remove_tag', methods=['POST']) @flasktools.required_fields(['photo_ids', 'tagname'], forbid_whitespace=True) def post_batch_photos_remove_tag(): + common.permission_manager.basic() response = post_photo_add_remove_tag_core( photo_ids=request.form['photo_ids'], tagname=request.form['tagname'], @@ -178,6 +188,7 @@ def post_batch_photos_remove_tag(): @site.route('/photo//generate_thumbnail', methods=['POST']) def post_photo_generate_thumbnail(photo_id): + common.permission_manager.basic() special = request.form.to_dict() with common.P.transaction: @@ -212,17 +223,20 @@ def post_photo_refresh_metadata_core(photo_ids): @site.route('/photo//refresh_metadata', methods=['POST']) def post_photo_refresh_metadata(photo_id): + common.permission_manager.basic() response = post_photo_refresh_metadata_core(photo_ids=photo_id) return response @site.route('/batch/photos/refresh_metadata', methods=['POST']) @flasktools.required_fields(['photo_ids'], forbid_whitespace=True) def post_batch_photos_refresh_metadata(): + common.permission_manager.basic() response = post_photo_refresh_metadata_core(photo_ids=request.form['photo_ids']) return response @site.route('/photo//set_searchhidden', methods=['POST']) def post_photo_set_searchhidden(photo_id): + common.permission_manager.basic() with common.P.transaction: photo = common.P_photo(photo_id, response_type='json') photo.set_searchhidden(True) @@ -230,6 +244,7 @@ def post_photo_set_searchhidden(photo_id): @site.route('/photo//unset_searchhidden', methods=['POST']) def post_photo_unset_searchhidden(photo_id): + common.permission_manager.basic() with common.P.transaction: photo = common.P_photo(photo_id, response_type='json') photo.set_searchhidden(False) @@ -249,6 +264,7 @@ def post_batch_photos_searchhidden_core(photo_ids, searchhidden): @site.route('/photo//show_in_folder', methods=['POST']) def post_photo_show_in_folder(photo_id): + common.permission_manager.basic() if not request.is_localhost: flask.abort(403) @@ -267,6 +283,7 @@ def post_photo_show_in_folder(photo_id): @site.route('/batch/photos/set_searchhidden', methods=['POST']) @flasktools.required_fields(['photo_ids'], forbid_whitespace=True) def post_batch_photos_set_searchhidden(): + common.permission_manager.basic() photo_ids = request.form['photo_ids'] response = post_batch_photos_searchhidden_core(photo_ids=photo_ids, searchhidden=True) return response @@ -274,6 +291,7 @@ def post_batch_photos_set_searchhidden(): @site.route('/batch/photos/unset_searchhidden', methods=['POST']) @flasktools.required_fields(['photo_ids'], forbid_whitespace=True) def post_batch_photos_unset_searchhidden(): + common.permission_manager.basic() photo_ids = request.form['photo_ids'] response = post_batch_photos_searchhidden_core(photo_ids=photo_ids, searchhidden=False) return response @@ -282,6 +300,7 @@ def post_batch_photos_unset_searchhidden(): @site.route('/clipboard') def get_clipboard_page(): + common.permission_manager.basic() return common.render_template(request, 'clipboard.html') @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. ''' + common.permission_manager.basic() photo_ids = request.form['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']) @flasktools.required_fields(['photo_ids'], forbid_whitespace=True) def post_batch_photos_photo_cards(): + common.permission_manager.basic() photo_ids = request.form['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 that zip file. ''' + common.permission_manager.basic() zip_token = zip_token.split('.')[0] try: 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 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 = stringtools.comma_space_split(photo_ids) @@ -436,6 +459,7 @@ def get_search_core(): @site.route('/search_embed') def get_search_embed(): + common.permission_manager.basic() search = get_search_core() response = common.render_template( request, @@ -447,6 +471,8 @@ def get_search_embed(): @site.route('/search') def get_search_html(): + common.permission_manager.basic() + search = get_search_core() search.kwargs.view = request.args.get('view', 'grid') @@ -496,6 +522,7 @@ def get_search_html(): @site.route('/search.atom') def get_search_atom(): + common.permission_manager.basic() search = get_search_core() soup = etiquette.helpers.make_atom_feed( search.results, @@ -508,6 +535,7 @@ def get_search_atom(): @site.route('/search.json') def get_search_json(): + common.permission_manager.basic() search = get_search_core() response = search.jsonify() return flasktools.json_response(response) @@ -516,5 +544,6 @@ def get_search_json(): @site.route('/swipe') def get_swipe(): + common.permission_manager.basic() response = common.render_template(request, 'swipe.html') return response diff --git a/frontends/etiquette_flask/backend/endpoints/tag_endpoints.py b/frontends/etiquette_flask/backend/endpoints/tag_endpoints.py index e73b7ed..403edd0 100644 --- a/frontends/etiquette_flask/backend/endpoints/tag_endpoints.py +++ b/frontends/etiquette_flask/backend/endpoints/tag_endpoints.py @@ -15,11 +15,13 @@ session_manager = common.session_manager @site.route('/tags/') @site.route('/tags/.json') def get_tags_specific_redirect(specific_tag): + common.permission_manager.basic() return flask.redirect(request.url.replace('/tags/', '/tag/')) @site.route('/tagid/') @site.route('/tagid/.json') def get_tag_id_redirect(tag_id): + common.permission_manager.basic() if request.path.endswith('.json'): tag = common.P_tag_id(tag_id, response_type='json') else: @@ -31,6 +33,7 @@ def get_tag_id_redirect(tag_id): @site.route('/tag/.json') def get_tag_json(specific_tag_name): + common.permission_manager.basic() specific_tag = common.P_tag(specific_tag_name, response_type='json') if specific_tag.name != specific_tag_name: 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//edit', methods=['POST']) def post_tag_edit(tagname): + common.permission_manager.basic() with common.P.transaction: tag = common.P_tag(tagname, response_type='json') name = request.form.get('name', '').strip() @@ -59,6 +63,7 @@ def post_tag_edit(tagname): @site.route('/tag//add_child', methods=['POST']) @flasktools.required_fields(['child_name'], forbid_whitespace=True) def post_tag_add_child(tagname): + common.permission_manager.basic() with common.P.transaction: parent = common.P_tag(tagname, 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//add_synonym', methods=['POST']) @flasktools.required_fields(['syn_name'], forbid_whitespace=True) def post_tag_add_synonym(tagname): + common.permission_manager.basic() syn_name = request.form['syn_name'] with common.P.transaction: @@ -81,6 +87,7 @@ def post_tag_add_synonym(tagname): @site.route('/tag//remove_child', methods=['POST']) @flasktools.required_fields(['child_name'], forbid_whitespace=True) def post_tag_remove_child(tagname): + common.permission_manager.basic() with common.P.transaction: parent = common.P_tag(tagname, 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//remove_synonym', methods=['POST']) @flasktools.required_fields(['syn_name'], forbid_whitespace=True) def post_tag_remove_synonym(tagname): + common.permission_manager.basic() syn_name = request.form['syn_name'] with common.P.transaction: @@ -103,6 +111,7 @@ def post_tag_remove_synonym(tagname): # Tag listings ##################################################################################### @site.route('/all_tags.json') +@common.permission_manager.basic_decorator @flasktools.cached_endpoint(max_age=15) def 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/') @site.route('/tags') def get_tags_html(specific_tag_name=None): + common.permission_manager.basic() if specific_tag_name is None: specific_tag = None else: @@ -151,6 +161,7 @@ def get_tags_html(specific_tag_name=None): @site.route('/tags.json') def get_tags_json(): + common.permission_manager.basic() include_synonyms = request.args.get('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']) @flasktools.required_fields(['name'], forbid_whitespace=True) def post_tag_create(): + common.permission_manager.basic() name = request.form['name'] description = request.form.get('description', None) @@ -175,6 +187,7 @@ def post_tag_create(): @site.route('/tags/easybake', methods=['POST']) @flasktools.required_fields(['easybake_string'], forbid_whitespace=True) def post_tag_easybake(): + common.permission_manager.basic() easybake_string = request.form['easybake_string'] with common.P.transaction: @@ -184,6 +197,7 @@ def post_tag_easybake(): @site.route('/tag//delete', methods=['POST']) def post_tag_delete(tagname): + common.permission_manager.basic() with common.P.transaction: tag = common.P_tag(tagname, response_type='json') tag.delete() diff --git a/frontends/etiquette_flask/backend/endpoints/user_endpoints.py b/frontends/etiquette_flask/backend/endpoints/user_endpoints.py index 7c5848c..ce6d0f3 100644 --- a/frontends/etiquette_flask/backend/endpoints/user_endpoints.py +++ b/frontends/etiquette_flask/backend/endpoints/user_endpoints.py @@ -14,11 +14,13 @@ session_manager = common.session_manager @site.route('/user/') def get_user_html(username): + common.permission_manager.basic() user = common.P_user(username, response_type='html') return common.render_template(request, 'user.html', user=user) @site.route('/user/.json') def get_user_json(username): + common.permission_manager.basic() user = common.P_user(username, response_type='json') user = user.jsonify() return flasktools.json_response(user) @@ -26,6 +28,7 @@ def get_user_json(username): @site.route('/userid/') @site.route('/userid/.json') def get_user_id_redirect(user_id): + common.permission_manager.basic() if request.path.endswith('.json'): user = common.P_user_id(user_id, response_type='json') else: @@ -37,6 +40,7 @@ def get_user_id_redirect(user_id): @site.route('/user//edit', methods=['POST']) def post_user_edit(username): + common.permission_manager.basic() if not request.session: return flasktools.json_response(etiquette.exceptions.Unauthorized().jsonify(), status=403) user = common.P_user(username, response_type='json') @@ -54,6 +58,7 @@ def post_user_edit(username): @site.route('/login', methods=['GET']) def get_login(): + common.permission_manager.global_public() response = common.render_template( request, 'login.html', @@ -66,6 +71,7 @@ def get_login(): @site.route('/login', methods=['POST']) @flasktools.required_fields(['username', 'password']) def post_login(): + common.permission_manager.global_public() if request.session.user: exc = etiquette.exceptions.AlreadySignedIn() response = exc.jsonify() @@ -96,6 +102,7 @@ def post_login(): @site.route('/logout', methods=['POST']) def post_logout(): + common.permission_manager.basic() session_manager.remove(request) response = flasktools.json_response({}) return response @@ -104,11 +111,13 @@ def post_logout(): @site.route('/register', methods=['GET']) def get_register(): + common.permission_manager.global_public() return flask.redirect('/login') @site.route('/register', methods=['POST']) @flasktools.required_fields(['username', 'password_1', 'password_2']) def post_register(): + common.permission_manager.global_public() if request.session.user: exc = etiquette.exceptions.AlreadySignedIn() response = exc.jsonify() diff --git a/frontends/etiquette_flask/backend/permissions.py b/frontends/etiquette_flask/backend/permissions.py new file mode 100644 index 0000000..c774f2c --- /dev/null +++ b/frontends/etiquette_flask/backend/permissions.py @@ -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 diff --git a/frontends/etiquette_flask/etiquette_flask_dev.py b/frontends/etiquette_flask/etiquette_flask_dev.py index 33ee26d..19334e7 100644 --- a/frontends/etiquette_flask/etiquette_flask_dev.py +++ b/frontends/etiquette_flask/etiquette_flask_dev.py @@ -1,6 +1,9 @@ ''' 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 werkzeug.middleware.proxy_fix @@ -11,6 +14,7 @@ import sys from voussoirkit import betterhelp from voussoirkit import pathclass +from voussoirkit import operatornotify from voussoirkit import vlogging log = vlogging.getLogger(__name__, 'etiquette_flask_dev') @@ -79,7 +83,7 @@ def etiquette_flask_launch_argparse(args): use_https=args.use_https, ) -@vlogging.main_decorator +@operatornotify.main_decorator(subject='etiquette_flask_dev', notify_every_line=True) def main(argv): parser = argparse.ArgumentParser( description='''