/files/: add anon permissions

This commit is contained in:
dogeystamp 2023-04-27 20:32:46 -04:00
parent 2388bac57f
commit f97cfbbe33
Signed by: dogeystamp
GPG Key ID: 7225FE3592EFFA38
5 changed files with 177 additions and 26 deletions

View File

@ -21,10 +21,18 @@ function sachet_set_perms -d "setup permissions"
http --session=sachet-admin patch $BASENAME/users/user permissions:='["READ", "CREATE", "LIST", "DELETE"]'
end
function sachet_anon -d "setup anonymous user's permissions"
if test -z $argv
set argv '["READ", "CREATE", "LIST", "DELETE"]'
end
http --session=sachet-admin patch $BASENAME/admin/settings default_permissions:=$argv
end
function sachet_upload -d "uploads a file"
argparse 's/session=?' -- $argv
set FNAME (basename $argv)
set URL (http --session=sachet-user post $BASENAME/files file_name=$FNAME | jq -r .url)
http --session=sachet-user -f post $BASENAME/$URL/content upload@$argv
set URL (http --session=$_flag_session post $BASENAME/files file_name=$FNAME | jq -r .url)
http --session=$_flag_session -f post $BASENAME/$URL/content upload@$argv
end
function sachet_upload_meme -d "uploads a random meme"
@ -33,19 +41,21 @@ function sachet_upload_meme -d "uploads a random meme"
end
function sachet_list -d "lists files on a given page"
argparse 'P/per-page=!_validate_int' -- $argv
argparse 'P/per-page=!_validate_int' 's/session=?' -- $argv
if not set -q _flag_per_page
set _flag_per_page 5
end
http --session=sachet-user get localhost:5000/files per_page=$_flag_per_page page=$argv[1]
http --session=$_flag_session get localhost:5000/files per_page=$_flag_per_page page=$argv[1]
end
function sachet_download -d "downloads a given file id"
http --session=sachet-user -f -d get $BASENAME/files/$argv/content
argparse 's/session=?' -- $argv
http --session=$_flag_session -f -d get $BASENAME/files/$argv/content
end
function sachet_delete -d "delete given file ids"
argparse 's/session=?' -- $argv
for file in $argv
http --session=sachet-user delete $BASENAME/files/$file
http --session=$_flag_session delete $BASENAME/files/$file
end
end

View File

@ -10,22 +10,22 @@ files_blueprint = Blueprint("files_blueprint", __name__)
class FilesMetadataAPI(ModelAPI):
@auth_required(required_permissions=(Permissions.READ,))
@auth_required(required_permissions=(Permissions.READ,), allow_anonymous=True)
def get(self, share_id, auth_user=None):
share = Share.query.filter_by(share_id=share_id).first()
return super().get(share)
@auth_required(required_permissions=(Permissions.MODIFY,))
@auth_required(required_permissions=(Permissions.MODIFY,), allow_anonymous=True)
def patch(self, share_id, auth_user=None):
share = Share.query.filter_by(share_id=share_id).first()
return super().patch(share)
@auth_required(required_permissions=(Permissions.MODIFY,))
@auth_required(required_permissions=(Permissions.MODIFY,), allow_anonymous=True)
def put(self, share_id, auth_user=None):
share = Share.query.filter_by(share_id=share_id).first()
return super().put(share)
@auth_required(required_permissions=(Permissions.DELETE,))
@auth_required(required_permissions=(Permissions.DELETE,), allow_anonymous=True)
def delete(self, share_id, auth_user=None):
try:
uuid.UUID(share_id)
@ -43,13 +43,14 @@ files_blueprint.add_url_rule(
class FilesAPI(ModelListAPI):
@auth_required(required_permissions=(Permissions.CREATE,))
@auth_required(required_permissions=(Permissions.CREATE,), allow_anonymous=True)
def post(self, auth_user=None):
data = request.get_json()
data["owner_name"] = auth_user.username
if auth_user:
data["owner_name"] = auth_user.username
return super().post(Share, data)
@auth_required(required_permissions=(Permissions.LIST,))
@auth_required(required_permissions=(Permissions.LIST,), allow_anonymous=True)
def get(self, auth_user=None):
return super().get(Share)
@ -62,7 +63,7 @@ files_blueprint.add_url_rule(
class FileContentAPI(ModelAPI):
@auth_required(required_permissions=(Permissions.CREATE,))
@auth_required(required_permissions=(Permissions.CREATE,), allow_anonymous=True)
def post(self, share_id, auth_user=None):
share = Share.query.filter_by(share_id=uuid.UUID(share_id)).first()
@ -108,7 +109,7 @@ class FileContentAPI(ModelAPI):
201,
)
@auth_required(required_permissions=(Permissions.MODIFY,))
@auth_required(required_permissions=(Permissions.MODIFY,), allow_anonymous=True)
def put(self, share_id, auth_user=None):
share = Share.query.filter_by(share_id=share_id).first()
if not share:
@ -139,7 +140,7 @@ class FileContentAPI(ModelAPI):
200,
)
@auth_required(required_permissions=(Permissions.READ,))
@auth_required(required_permissions=(Permissions.READ,), allow_anonymous=True)
def get(self, share_id, auth_user=None):
share = Share.query.filter_by(share_id=share_id).first()
if not share:

View File

@ -236,9 +236,10 @@ class Share(db.Model):
file_name = db.Column(db.String, nullable=False)
def __init__(self, owner_name, file_name=None):
def __init__(self, owner_name=None, file_name=None):
self.owner = User.query.filter_by(username=owner_name).first()
self.owner_name = self.owner.username
if self.owner:
self.owner_name = self.owner.username
self.share_id = uuid.uuid4()
self.url = url_for("files_blueprint.files_metadata_api", share_id=self.share_id)
self.create_date = datetime.datetime.now()

View File

@ -1,6 +1,6 @@
from flask import request, jsonify
from flask.views import MethodView
from sachet.server.models import Permissions, User, BlacklistToken
from sachet.server.models import Permissions, User, BlacklistToken, ServerSettings
from sachet.server import db
from functools import wraps
from marshmallow import ValidationError
@ -8,20 +8,21 @@ from bitmask import Bitmask
import jwt
def auth_required(func=None, *, required_permissions=()):
"""Decorator to require authentication.
# https://stackoverflow.com/questions/3888158/making-decorators-with-optional-arguments
def auth_required(func=None, *, required_permissions=(), allow_anonymous=False):
"""Require specific authentication.
Passes an argument 'user' to the function, with a User object corresponding
Passes an argument `user` to the function, with a User object corresponding
to the authenticated session.
Parameters
----------
required_permissions : tuple of Permissions
required_permissions : tuple of Permissions, optional
Permissions required to access this endpoint.
allow_anonymous : bool, optional
Allow anonymous authentication. This means the `user` parameter might be None.
"""
# see https://stackoverflow.com/questions/3888158/making-decorators-with-optional-arguments
def _decorate(f):
@wraps(f)
def decorator(*args, **kwargs):
@ -38,7 +39,28 @@ def auth_required(func=None, *, required_permissions=()):
return jsonify(resp), 401
if not token:
return jsonify({"status": "fail", "message": "Missing auth token"}), 401
if allow_anonymous:
server_settings = ServerSettings.query.first()
if (
Bitmask(AllFlags=Permissions, *required_permissions)
not in server_settings.default_permissions
):
return (
jsonify(
{
"status": "fail",
"message": "Missing permissions to access this page.",
}
),
403,
)
kwargs["auth_user"] = None
return f(*args, **kwargs)
else:
return (
jsonify({"status": "fail", "message": "Missing auth token"}),
401,
)
try:
data, user = User.read_token(token)

117
tests/test_anonymous.py Normal file
View File

@ -0,0 +1,117 @@
import pytest
from io import BytesIO
from werkzeug.datastructures import FileStorage
import uuid
"""Test anonymous authentication to endpoints."""
def test_files(client, auth, rand):
# set create perm for anon users
resp = client.patch(
"/admin/settings",
headers=auth("administrator"),
json={"default_permissions": ["CREATE"]},
)
assert resp.status_code == 200
# create share
resp = client.post("/files", json={"file_name": "content.bin"})
assert resp.status_code == 201
data = resp.get_json()
url = data.get("url")
assert url is not None
assert "/files/" in url
upload_data = rand.randbytes(4000)
# upload file to share
resp = client.post(
url + "/content",
data={"upload": FileStorage(stream=BytesIO(upload_data), filename="upload")},
content_type="multipart/form-data",
)
assert resp.status_code == 201
# set read perm for anon users
resp = client.patch(
"/admin/settings",
headers=auth("administrator"),
json={"default_permissions": ["READ"]},
)
assert resp.status_code == 200
# read file
resp = client.get(
url + "/content",
)
assert resp.data == upload_data
assert "filename=content.bin" in resp.headers["Content-Disposition"].split("; ")
# set modify perm for anon users
resp = client.patch(
"/admin/settings",
headers=auth("administrator"),
json={"default_permissions": ["MODIFY"]},
)
assert resp.status_code == 200
# modify share
upload_data = rand.randbytes(4000)
resp = client.put(
url + "/content",
data={"upload": FileStorage(stream=BytesIO(upload_data), filename="upload")},
content_type="multipart/form-data",
)
assert resp.status_code == 200
resp = client.patch(
url,
json={"file_name": "new_bin.bin"},
)
assert resp.status_code == 200
resp = client.get(
url + "/content",
)
assert resp.data == upload_data
assert "filename=new_bin.bin" in resp.headers["Content-Disposition"].split("; ")
# set list perm for anon users
resp = client.patch(
"/admin/settings",
headers=auth("administrator"),
json={"default_permissions": ["LIST"]},
)
assert resp.status_code == 200
# test listing file
resp = client.get(
"/files",
json={},
)
data = resp.get_json()
assert resp.status_code == 200
listed = data.get("data")
assert len(listed) == 1
assert listed[0].get("initialized") is True
# set delete perm for anon users
resp = client.patch(
"/admin/settings",
headers=auth("administrator"),
json={"default_permissions": ["DELETE"]},
)
assert resp.status_code == 200
# test deletion
resp = client.delete(
url,
)
assert resp.status_code == 200
# file shouldn't exist anymore
resp = client.get(
url + "/content",
)
assert resp.status_code == 404