/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"]' http --session=sachet-admin patch $BASENAME/users/user permissions:='["READ", "CREATE", "LIST", "DELETE"]'
end 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" function sachet_upload -d "uploads a file"
argparse 's/session=?' -- $argv
set FNAME (basename $argv) set FNAME (basename $argv)
set URL (http --session=sachet-user post $BASENAME/files file_name=$FNAME | jq -r .url) set URL (http --session=$_flag_session post $BASENAME/files file_name=$FNAME | jq -r .url)
http --session=sachet-user -f post $BASENAME/$URL/content upload@$argv http --session=$_flag_session -f post $BASENAME/$URL/content upload@$argv
end end
function sachet_upload_meme -d "uploads a random meme" function sachet_upload_meme -d "uploads a random meme"
@ -33,19 +41,21 @@ function sachet_upload_meme -d "uploads a random meme"
end end
function sachet_list -d "lists files on a given page" 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 if not set -q _flag_per_page
set _flag_per_page 5 set _flag_per_page 5
end 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 end
function sachet_download -d "downloads a given file id" 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 end
function sachet_delete -d "delete given file ids" function sachet_delete -d "delete given file ids"
argparse 's/session=?' -- $argv
for file in $argv for file in $argv
http --session=sachet-user delete $BASENAME/files/$file http --session=$_flag_session delete $BASENAME/files/$file
end end
end end

View File

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

View File

@ -236,9 +236,10 @@ class Share(db.Model):
file_name = db.Column(db.String, nullable=False) 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 = 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.share_id = uuid.uuid4()
self.url = url_for("files_blueprint.files_metadata_api", share_id=self.share_id) self.url = url_for("files_blueprint.files_metadata_api", share_id=self.share_id)
self.create_date = datetime.datetime.now() self.create_date = datetime.datetime.now()

View File

@ -1,6 +1,6 @@
from flask import request, jsonify from flask import request, jsonify
from flask.views import MethodView 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 sachet.server import db
from functools import wraps from functools import wraps
from marshmallow import ValidationError from marshmallow import ValidationError
@ -8,20 +8,21 @@ from bitmask import Bitmask
import jwt import jwt
def auth_required(func=None, *, required_permissions=()): # https://stackoverflow.com/questions/3888158/making-decorators-with-optional-arguments
"""Decorator to require authentication. 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. to the authenticated session.
Parameters Parameters
---------- ----------
required_permissions : tuple of Permissions, optional
required_permissions : tuple of Permissions
Permissions required to access this endpoint. 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): def _decorate(f):
@wraps(f) @wraps(f)
def decorator(*args, **kwargs): def decorator(*args, **kwargs):
@ -38,7 +39,28 @@ def auth_required(func=None, *, required_permissions=()):
return jsonify(resp), 401 return jsonify(resp), 401
if not token: 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: try:
data, user = User.read_token(token) 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