made permissions more flexible
This commit is contained in:
parent
96e12c33da
commit
548ccdb892
@ -1,11 +1,13 @@
|
|||||||
attrs==22.2.0
|
attrs==22.2.0
|
||||||
bcrypt==4.0.1
|
bcrypt==4.0.1
|
||||||
|
bitmask @ git+https://github.com/dogeystamp/bitmask@8524113fcdc22a570bda77d440374f5f269fdb79
|
||||||
click==8.1.3
|
click==8.1.3
|
||||||
coverage==7.2.1
|
coverage==7.2.1
|
||||||
exceptiongroup==1.1.0
|
exceptiongroup==1.1.0
|
||||||
Flask==2.2.3
|
Flask==2.2.3
|
||||||
Flask-Bcrypt==1.0.1
|
Flask-Bcrypt==1.0.1
|
||||||
Flask-Cors==3.0.10
|
Flask-Cors==3.0.10
|
||||||
|
flask-marshmallow==0.14.0
|
||||||
Flask-Script==2.0.6
|
Flask-Script==2.0.6
|
||||||
Flask-SQLAlchemy==3.0.3
|
Flask-SQLAlchemy==3.0.3
|
||||||
Flask-Testing==0.8.1
|
Flask-Testing==0.8.1
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import click
|
import click
|
||||||
from sachet.server import app, db
|
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 sachet.server.users import manage
|
||||||
from flask.cli import AppGroup
|
from flask.cli import AppGroup
|
||||||
|
from bitmask import Bitmask
|
||||||
|
|
||||||
|
|
||||||
db_cli = AppGroup("db")
|
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).")
|
help="Sets the user's password (for security, avoid setting this from the command line).")
|
||||||
def create_user(admin, username, password):
|
def create_user(admin, username, password):
|
||||||
"""Create a user directly in the database."""
|
"""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")
|
@user_cli.command("delete")
|
||||||
@click.argument("username")
|
@click.argument("username")
|
||||||
|
@ -1,8 +1,21 @@
|
|||||||
from sachet.server import app, db, ma, bcrypt
|
from sachet.server import app, db, ma, bcrypt
|
||||||
|
from marshmallow import fields, ValidationError
|
||||||
from flask import request, jsonify
|
from flask import request, jsonify
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import datetime
|
import datetime
|
||||||
import jwt
|
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):
|
class User(db.Model):
|
||||||
__tablename__ = "users"
|
__tablename__ = "users"
|
||||||
@ -10,15 +23,44 @@ class User(db.Model):
|
|||||||
username = db.Column(db.String(255), unique=True, nullable=False, primary_key=True)
|
username = db.Column(db.String(255), unique=True, nullable=False, primary_key=True)
|
||||||
password = db.Column(db.String(255), nullable=False)
|
password = db.Column(db.String(255), nullable=False)
|
||||||
register_date = db.Column(db.DateTime, 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(
|
self.password = bcrypt.generate_password_hash(
|
||||||
password, app.config.get("BCRYPT_LOG_ROUNDS")
|
password, app.config.get("BCRYPT_LOG_ROUNDS")
|
||||||
).decode()
|
).decode()
|
||||||
self.username = username
|
self.username = username
|
||||||
self.register_date = datetime.datetime.now()
|
self.register_date = datetime.datetime.now()
|
||||||
self.admin = admin
|
|
||||||
|
|
||||||
def encode_token(self, jti=None):
|
def encode_token(self, jti=None):
|
||||||
"""Generates an authentication token"""
|
"""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 UserSchema(ma.SQLAlchemySchema):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
|
|
||||||
username = ma.auto_field()
|
username = ma.auto_field()
|
||||||
register_date = ma.auto_field()
|
register_date = ma.auto_field()
|
||||||
admin = ma.auto_field()
|
permissions = PermissionField(data_key="permissions")
|
||||||
|
|
||||||
|
|
||||||
class BlacklistToken(db.Model):
|
class BlacklistToken(db.Model):
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from sachet.server import app, db
|
from sachet.server import app, db
|
||||||
from sachet.server.models import User
|
from sachet.server.models import User
|
||||||
|
|
||||||
def create_user(admin, username, password):
|
def create_user(permissions, username, password):
|
||||||
# to reduce confusion with API endpoints
|
# to reduce confusion with API endpoints
|
||||||
forbidden = {"login", "logout", "extend"}
|
forbidden = {"login", "logout", "extend"}
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ def create_user(admin, username, password):
|
|||||||
user = User(
|
user = User(
|
||||||
username=username,
|
username=username,
|
||||||
password=password,
|
password=password,
|
||||||
admin=admin
|
permissions=permissions
|
||||||
)
|
)
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import jwt
|
import jwt
|
||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from flask.views import MethodView
|
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
|
from sachet.server import bcrypt, db
|
||||||
|
|
||||||
user_schema = UserSchema()
|
user_schema = UserSchema()
|
||||||
@ -66,7 +66,7 @@ class LogoutAPI(MethodView):
|
|||||||
except jwt.InvalidTokenError:
|
except jwt.InvalidTokenError:
|
||||||
return jsonify({"status": "fail", "message": "Invalid auth token."}), 400
|
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)
|
entry = BlacklistToken(token=token)
|
||||||
db.session.add(entry)
|
db.session.add(entry)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -109,7 +109,7 @@ class UserAPI(MethodView):
|
|||||||
@auth_required
|
@auth_required
|
||||||
def get(user, self, username):
|
def get(user, self, username):
|
||||||
info_user = User.query.filter_by(username=username).first()
|
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 = {
|
resp = {
|
||||||
"status": "fail",
|
"status": "fail",
|
||||||
"message": "You are not authorized to view this page."
|
"message": "You are not authorized to view this page."
|
||||||
|
@ -3,6 +3,10 @@ import yaml
|
|||||||
from sachet.server.users import manage
|
from sachet.server.users import manage
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
from sachet.server import app, db
|
from sachet.server import app, db
|
||||||
|
from sachet.server.models import Permissions, UserSchema
|
||||||
|
from bitmask import Bitmask
|
||||||
|
|
||||||
|
user_schema = UserSchema()
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def client():
|
def client():
|
||||||
@ -32,21 +36,21 @@ def users(client):
|
|||||||
|
|
||||||
Returns a dictionary with all the info for each user.
|
Returns a dictionary with all the info for each user.
|
||||||
"""
|
"""
|
||||||
userinfo = {
|
userinfo = dict(
|
||||||
"jeff": {
|
jeff = dict(
|
||||||
"password": "1234",
|
password = "1234",
|
||||||
"admin": False
|
permissions = Bitmask()
|
||||||
},
|
),
|
||||||
"administrator": {
|
administrator = dict(
|
||||||
"password": "4321",
|
password = "4321",
|
||||||
"admin": True
|
permissions = Bitmask(Permissions.ADMIN)
|
||||||
}
|
),
|
||||||
}
|
)
|
||||||
|
|
||||||
for user, info in userinfo.items():
|
for user, info in userinfo.items():
|
||||||
info["username"] = user
|
info["username"] = user
|
||||||
manage.create_user(
|
manage.create_user(
|
||||||
info["admin"],
|
info["permissions"],
|
||||||
info["username"],
|
info["username"],
|
||||||
info["password"]
|
info["password"]
|
||||||
)
|
)
|
||||||
@ -56,14 +60,16 @@ def users(client):
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def validate_info(users):
|
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 = [
|
verify_fields = [
|
||||||
"username",
|
"username",
|
||||||
"admin",
|
"permissions",
|
||||||
]
|
]
|
||||||
|
|
||||||
def _validate(user, info):
|
def _validate(user, info):
|
||||||
|
info = user_schema.load(info)
|
||||||
|
|
||||||
for k in verify_fields:
|
for k in verify_fields:
|
||||||
assert users[user][k] == info[k]
|
assert users[user][k] == info[k]
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user