made permissions more flexible

This commit is contained in:
dogeystamp 2023-03-27 21:54:20 -04:00
parent 96e12c33da
commit 548ccdb892
Signed by: dogeystamp
GPG Key ID: 7225FE3592EFFA38
6 changed files with 102 additions and 24 deletions

View File

@ -1,11 +1,13 @@
attrs==22.2.0
bcrypt==4.0.1
bitmask @ git+https://github.com/dogeystamp/bitmask@8524113fcdc22a570bda77d440374f5f269fdb79
click==8.1.3
coverage==7.2.1
exceptiongroup==1.1.0
Flask==2.2.3
Flask-Bcrypt==1.0.1
Flask-Cors==3.0.10
flask-marshmallow==0.14.0
Flask-Script==2.0.6
Flask-SQLAlchemy==3.0.3
Flask-Testing==0.8.1

View File

@ -1,8 +1,9 @@
import click
from sachet.server import app, db
from sachet.server.models import User
from sachet.server.models import User, Permissions
from sachet.server.users import manage
from flask.cli import AppGroup
from bitmask import Bitmask
db_cli = AppGroup("db")
@ -32,7 +33,10 @@ user_cli = AppGroup("user")
help="Sets the user's password (for security, avoid setting this from the command line).")
def create_user(admin, username, password):
"""Create a user directly in the database."""
manage.create_user(admin, username, password)
perms = Bitmask()
if admin:
perms.add(Permissions.ADMIN)
manage.create_user(perms, username, password)
@user_cli.command("delete")
@click.argument("username")

View File

@ -1,8 +1,21 @@
from sachet.server import app, db, ma, bcrypt
from marshmallow import fields, ValidationError
from flask import request, jsonify
from functools import wraps
import datetime
import jwt
from bitmask import Bitmask
from enum import IntFlag
class Permissions(IntFlag):
CREATE = 1
MODIFY = 1<<1
DELETE = 1<<2
LOCK = 1<<3
LIST = 1<<4
ADMIN = 1<<5
class User(db.Model):
__tablename__ = "users"
@ -10,15 +23,44 @@ class User(db.Model):
username = db.Column(db.String(255), unique=True, nullable=False, primary_key=True)
password = db.Column(db.String(255), nullable=False)
register_date = db.Column(db.DateTime, nullable=False)
admin = db.Column(db.Boolean, nullable=False, default=False)
def __init__(self, username, password, admin=False):
permissions_number = db.Column(db.BigInteger, nullable=False, default=0)
@property
def permissions(self):
"""
Bitmask listing all permissions.
See the Permissions class for all possible permissions.
Also, see https://github.com/dogeystamp/bitmask for information on how
to use this field.
"""
mask = Bitmask()
mask.AllFlags = Permissions
mask.value = self.permissions_number
return mask
@permissions.setter
def permissions(self, value):
mask = Bitmask()
mask.AllFlags = Permissions
mask += value
self.permissions_number = mask.value
db.session.commit()
def __init__(self, username, password, permissions):
permissions.AllFlags = Permissions
self.permissions = permissions
self.password = bcrypt.generate_password_hash(
password, app.config.get("BCRYPT_LOG_ROUNDS")
).decode()
self.username = username
self.register_date = datetime.datetime.now()
self.admin = admin
def encode_token(self, jti=None):
"""Generates an authentication token"""
@ -35,13 +77,37 @@ class User(db.Model):
)
class PermissionField(fields.Field):
"""Field that serializes a Permissions bitmask to an array of strings."""
def _serialize(self, value, attr, obj, **kwargs):
mask = Bitmask()
mask.AllFlags = Permissions
mask += value
return [flag.name for flag in mask]
def _deserialize(self, value, attr, data, **kwargs):
mask = Bitmask()
mask.AllFlags = Permissions
flags = value
try:
for flag in flags:
mask.add(Permissions[flag])
except KeyError as e:
raise ValidationError("Invalid permission.") from e
return mask
class UserSchema(ma.SQLAlchemySchema):
class Meta:
model = User
username = ma.auto_field()
register_date = ma.auto_field()
admin = ma.auto_field()
permissions = PermissionField(data_key="permissions")
class BlacklistToken(db.Model):

View File

@ -1,7 +1,7 @@
from sachet.server import app, db
from sachet.server.models import User
def create_user(admin, username, password):
def create_user(permissions, username, password):
# to reduce confusion with API endpoints
forbidden = {"login", "logout", "extend"}
@ -13,7 +13,7 @@ def create_user(admin, username, password):
user = User(
username=username,
password=password,
admin=admin
permissions=permissions
)
db.session.add(user)
db.session.commit()

View File

@ -1,7 +1,7 @@
import jwt
from flask import Blueprint, request, jsonify
from flask.views import MethodView
from sachet.server.models import auth_required, read_token, User, UserSchema, BlacklistToken
from sachet.server.models import auth_required, read_token, Permissions, User, UserSchema, BlacklistToken
from sachet.server import bcrypt, db
user_schema = UserSchema()
@ -66,7 +66,7 @@ class LogoutAPI(MethodView):
except jwt.InvalidTokenError:
return jsonify({"status": "fail", "message": "Invalid auth token."}), 400
if user == token_user or user.admin == True:
if user == token_user or Permissions.ADMIN in user.permissions:
entry = BlacklistToken(token=token)
db.session.add(entry)
db.session.commit()
@ -109,7 +109,7 @@ class UserAPI(MethodView):
@auth_required
def get(user, self, username):
info_user = User.query.filter_by(username=username).first()
if (not info_user) or (info_user != user and not user.admin):
if (not info_user) or (info_user != user and Permissions.ADMIN not in user.permissions):
resp = {
"status": "fail",
"message": "You are not authorized to view this page."

View File

@ -3,6 +3,10 @@ import yaml
from sachet.server.users import manage
from click.testing import CliRunner
from sachet.server import app, db
from sachet.server.models import Permissions, UserSchema
from bitmask import Bitmask
user_schema = UserSchema()
@pytest.fixture
def client():
@ -32,21 +36,21 @@ def users(client):
Returns a dictionary with all the info for each user.
"""
userinfo = {
"jeff": {
"password": "1234",
"admin": False
},
"administrator": {
"password": "4321",
"admin": True
}
}
userinfo = dict(
jeff = dict(
password = "1234",
permissions = Bitmask()
),
administrator = dict(
password = "4321",
permissions = Bitmask(Permissions.ADMIN)
),
)
for user, info in userinfo.items():
info["username"] = user
manage.create_user(
info["admin"],
info["permissions"],
info["username"],
info["password"]
)
@ -56,14 +60,16 @@ def users(client):
@pytest.fixture
def validate_info(users):
"""Given a dictionary, validate the information against a given user's info."""
"""Given a response, deserialize and validate the information against a given user's info."""
verify_fields = [
"username",
"admin",
"permissions",
]
def _validate(user, info):
info = user_schema.load(info)
for k in verify_fields:
assert users[user][k] == info[k]