/files/: add anon permissions
This commit is contained in:
parent
2388bac57f
commit
f97cfbbe33
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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
117
tests/test_anonymous.py
Normal 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
|
Loading…
Reference in New Issue
Block a user