made permissions more flexible
This commit is contained in:
parent
96e12c33da
commit
548ccdb892
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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."
|
||||
|
@ -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]
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user