permissions models

This commit is contained in:
SvyatoslavArtymovych 2023-05-24 17:06:33 +03:00
parent cfd05b8cd9
commit 102a7f3577
18 changed files with 247 additions and 61 deletions

View File

@ -11,6 +11,7 @@
"**/.venv/*/**": true
},
"cSpell.words": [
"backref",
"bookname",
"Btns",
"flowbite",

View File

@ -1,10 +1,14 @@
from app import models as m
from app.logger import log
def get_or_create_permission(access: int, entity: m.Permission.Entity):
def get_or_create_permission(access: int, entity_type: m.Permission.Entity):
permission: m.Permission = m.Permission.query.filter_by(
access=access, entity=entity
access=access, entity_type=entity_type
).first()
if not permission:
permission: m.Permission = m.Permission(access=access, entity=entity).save()
log(log.INFO, "Create permission [%d] for entity [%s]", access, entity_type)
permission: m.Permission = m.Permission(
access=access, entity_type=entity_type
).save()
return permission

View File

@ -0,0 +1,29 @@
from app import models as m
from app.logger import log
from .get_or_create_permission import get_or_create_permission
def create_moderator_group():
log(log.INFO, "Create moderator access group")
group: m.AccessGroup = m.AccessGroup(name="moderator").save()
interpretation_DA = get_or_create_permission(
access=m.Permission.Access.D | m.Permission.Access.A,
entity_type=m.Permission.Entity.INTERPRETATION,
)
comment_DA = get_or_create_permission(
access=m.Permission.Access.D | m.Permission.Access.A,
entity_type=m.Permission.Entity.COMMENT,
)
log(log.INFO, "Add permission [%d] to group[%d]", interpretation_DA.id, group.id)
m.PermissionAccessGroups(
permission_id=interpretation_DA.id, access_group_id=group.id
).save()
log(log.INFO, "Add permission [%d] to group[%d]", comment_DA.id, group.id)
m.PermissionAccessGroups(
permission_id=comment_DA.id, access_group_id=group.id
).save()
return group

View File

@ -18,4 +18,8 @@ from .permission import (
AccessGroup,
UserAccessGroups,
PermissionAccessGroups,
BookAccessGroups,
CollectionAccessGroups,
SectionAccessGroups,
InterpretationAccessGroups,
)

View File

@ -19,6 +19,13 @@ class Book(BaseModel):
stars = db.relationship("User", secondary="books_stars", back_populates="stars")
contributors = db.relationship("BookContributor")
versions = db.relationship("BookVersion", order_by="asc(BookVersion.id)")
list_access_groups = db.relationship(
"AccessGroup"
) # all access_groups in current book(in nested entities)
access_groups = db.relationship(
"AccessGroup",
secondary="books_access_groups",
) # access_groups related to current entity
def __repr__(self):
return f"<{self.id}: {self.label}>"

View File

@ -25,6 +25,10 @@ class Collection(BaseModel):
order_by="asc(Collection.id)",
)
sections = db.relationship("Section")
access_groups = db.relationship(
"AccessGroup",
secondary="collections_access_groups",
) # access_groups related to current entity
def __repr__(self):
return f"<{self.id}: {self.label}>"

View File

@ -25,6 +25,10 @@ class Interpretation(BaseModel):
secondary="interpretation_tags",
back_populates="interpretations",
)
access_groups = db.relationship(
"AccessGroup",
secondary="interpretations_access_groups",
) # access_groups related to current entity
@property
def vote_count(self):

View File

@ -3,6 +3,10 @@ from .access_group import AccessGroup
from .permission import Permission
from .user_access_groups import UserAccessGroups
from .permission_access_groups import PermissionAccessGroups
from .book_access_groups import BookAccessGroups
from .collection_access_groups import CollectionAccessGroups
from .section_access_groups import SectionAccessGroups
from .interpretation_access_groups import InterpretationAccessGroups
# access groups
# moderators(by default empty) -> root collection -> CRUD Interpretation, Comment
@ -26,6 +30,7 @@ from .permission_access_groups import PermissionAccessGroups
# Book
# Version:
# Root Collection
# Collection A
# Section
@ -40,3 +45,13 @@ from .permission_access_groups import PermissionAccessGroups
# If the user has CRUD access to Collection B it means that
# it has access to all nested entities(SubCollection B.1/B.2, Sections)
# 1) Create moderator_AG and editor_AG on book create
# 2) Inherit parent's access groups
# TODO many to many
# book -> access_group
# collections -> access_group
# section -> access_group
# interpretation -> access_group

View File

@ -5,9 +5,13 @@ from app.models.utils import BaseModel
class AccessGroup(BaseModel):
__tablename__ = "access_groups"
name = db.Column(db.String(32), unique=True, nullable=False)
name = db.Column(db.String(32), nullable=False)
# Foreign Keys
book_id = db.Column(db.Integer, db.ForeignKey("books.id"))
# Relationships
book = db.relationship("Book", viewonly=True)
permissions = db.relationship(
"Permission",
secondary="permissions_access_groups",

View File

@ -0,0 +1,13 @@
from app import db
from app.models.utils import BaseModel
class BookAccessGroups(BaseModel):
__tablename__ = "books_access_groups"
# Foreign keys
book_id = db.Column(db.Integer, db.ForeignKey("books.id"))
access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id"))
def __repr__(self):
return f"<b:{self.book_id} to a_g:{self.access_group_id}"

View File

@ -0,0 +1,13 @@
from app import db
from app.models.utils import BaseModel
class CollectionAccessGroups(BaseModel):
__tablename__ = "collections_access_groups"
# Foreign keys
collection_id = db.Column(db.Integer, db.ForeignKey("collections.id"))
access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id"))
def __repr__(self):
return f"<c:{self.collection_id} to a_g:{self.access_group_id}"

View File

@ -0,0 +1,13 @@
from app import db
from app.models.utils import BaseModel
class InterpretationAccessGroups(BaseModel):
__tablename__ = "interpretations_access_groups"
# Foreign keys
interpretation_id = db.Column(db.Integer, db.ForeignKey("interpretations.id"))
access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id"))
def __repr__(self):
return f"<c:{self.interpretation_id} to a_g:{self.access_group_id}"

View File

@ -8,10 +8,10 @@ class Permission(BaseModel):
__tablename__ = "permissions"
class Access(IntEnum):
C = 1 # 0b0001
R = 2 # 0b0010
U = 4 # 0b0100
D = 8 # 0b1000
C = 1 # 0b0001 - Create
U = 2 # 0b0010 - Update
D = 4 # 0b0100 - Delete
A = 8 # 0b1000 - Approve
# sum = 0b1111
class Entity(IntEnum):
@ -22,8 +22,8 @@ class Permission(BaseModel):
INTERPRETATION = 4
COMMENT = 5
access = db.Column(db.Integer(), default=Access.C | Access.R | Access.U | Access.D)
entity = db.Column(db.Enum(Entity), default=Entity.UNKNOWN)
access = db.Column(db.Integer(), default=Access.C | Access.U | Access.D | Access.A)
entity_type = db.Column(db.Enum(Entity), default=Entity.UNKNOWN)
# Relationships
access_groups = db.relationship(

View File

@ -0,0 +1,13 @@
from app import db
from app.models.utils import BaseModel
class SectionAccessGroups(BaseModel):
__tablename__ = "sections_access_groups"
# Foreign keys
section_id = db.Column(db.Integer, db.ForeignKey("sections.id"))
access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id"))
def __repr__(self):
return f"<s:{self.section_id} to a_g:{self.access_group_id}"

View File

@ -26,6 +26,10 @@ class Section(BaseModel):
interpretations = db.relationship(
"Interpretation", viewonly=True, order_by="desc(Interpretation.id)"
)
access_groups = db.relationship(
"AccessGroup",
secondary="sections_access_groups",
) # access_groups related to current entity
@property
def path(self):

View File

@ -1137,10 +1137,6 @@ input:checked + .toggle-bg {
top: 2.5rem;
}
.top-3 {
top: 0.75rem;
}
.top-32 {
top: 8rem;
}
@ -1149,8 +1145,8 @@ input:checked + .toggle-bg {
top: 11rem;
}
.-z-10 {
z-index: -10;
.top-3 {
top: 0.75rem;
}
.z-0 {
@ -1189,6 +1185,10 @@ input:checked + .toggle-bg {
z-index: 55;
}
.-z-10 {
z-index: -10;
}
.col-span-6 {
grid-column: span 6 / span 6;
}
@ -1438,6 +1438,10 @@ input:checked + .toggle-bg {
height: 1rem;
}
.h-40 {
height: 10rem;
}
.h-5 {
height: 1.25rem;
}
@ -1446,18 +1450,10 @@ input:checked + .toggle-bg {
height: 1.5rem;
}
.h-64 {
height: 16rem;
}
.h-8 {
height: 2rem;
}
.h-80 {
height: 20rem;
}
.h-9 {
height: 2.25rem;
}
@ -1483,6 +1479,14 @@ input:checked + .toggle-bg {
height: 100vh;
}
.h-64 {
height: 16rem;
}
.h-80 {
height: 20rem;
}
.max-h-40 {
max-height: 10rem;
}
@ -1591,14 +1595,6 @@ input:checked + .toggle-bg {
max-width: 72rem;
}
.max-w-\[230\] {
max-width: 230;
}
.max-w-\[280\] {
max-width: 280;
}
.max-w-full {
max-width: 100%;
}
@ -1607,6 +1603,14 @@ input:checked + .toggle-bg {
max-width: 20rem;
}
.max-w-\[230\] {
max-width: 230;
}
.max-w-\[280\] {
max-width: 280;
}
.flex-1 {
flex: 1 1 0%;
}
@ -1632,11 +1636,6 @@ input:checked + .toggle-bg {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.-translate-y-6 {
--tw-translate-y: -1.5rem;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.-translate-y-full {
--tw-translate-y: -100%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@ -1662,6 +1661,11 @@ input:checked + .toggle-bg {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.-translate-y-6 {
--tw-translate-y: -1.5rem;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.rotate-180 {
--tw-rotate: 180deg;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@ -1773,6 +1777,10 @@ input:checked + .toggle-bg {
justify-content: space-between;
}
.gap-1 {
gap: 0.25rem;
}
.gap-2 {
gap: 0.5rem;
}
@ -1841,12 +1849,6 @@ input:checked + .toggle-bg {
margin-bottom: calc(2rem * var(--tw-space-y-reverse));
}
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1rem * var(--tw-space-x-reverse));
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
}
.divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
@ -2119,6 +2121,11 @@ input:checked + .toggle-bg {
background-color: rgb(200 30 30 / var(--tw-bg-opacity));
}
.bg-sky-300 {
--tw-bg-opacity: 1;
background-color: rgb(125 211 252 / var(--tw-bg-opacity));
}
.bg-slate-300 {
--tw-bg-opacity: 1;
background-color: rgb(203 213 225 / var(--tw-bg-opacity));
@ -2185,6 +2192,10 @@ input:checked + .toggle-bg {
stroke: #F05252;
}
.\!p-0 {
padding: 0px !important;
}
.p-0 {
padding: 0px;
}
@ -2226,11 +2237,6 @@ input:checked + .toggle-bg {
padding-right: 0px !important;
}
.px-0 {
padding-left: 0px;
padding-right: 0px;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
@ -2286,6 +2292,11 @@ input:checked + .toggle-bg {
padding-bottom: 1rem;
}
.px-0 {
padding-left: 0px;
padding-right: 0px;
}
.pb-3 {
padding-bottom: 0.75rem;
}
@ -2466,6 +2477,11 @@ input:checked + .toggle-bg {
color: rgb(14 159 110 / var(--tw-text-opacity));
}
.text-orange-500 {
--tw-text-opacity: 1;
color: rgb(255 90 31 / var(--tw-text-opacity));
}
.text-red-500 {
--tw-text-opacity: 1;
color: rgb(240 82 82 / var(--tw-text-opacity));
@ -2489,6 +2505,10 @@ input:checked + .toggle-bg {
text-decoration-line: line-through;
}
.\!no-underline {
text-decoration-line: none !important;
}
.opacity-0 {
opacity: 0;
}
@ -2626,6 +2646,11 @@ input:checked + .toggle-bg {
background-color: rgb(155 28 28 / var(--tw-bg-opacity));
}
.hover\:bg-sky-400:hover {
--tw-bg-opacity: 1;
background-color: rgb(56 189 248 / var(--tw-bg-opacity));
}
.hover\:bg-white:hover {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
@ -2680,12 +2705,6 @@ input:checked + .toggle-bg {
outline-offset: 2px;
}
.focus\:ring-0:focus {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.focus\:ring-2:focus {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
@ -2698,6 +2717,12 @@ input:checked + .toggle-bg {
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.focus\:ring-0:focus {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.focus\:ring-blue-100:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(225 239 254 / var(--tw-ring-opacity));

View File

@ -4,27 +4,29 @@ from app.controllers.get_or_create_permission import get_or_create_permission
def test_get_or_create_permission(client):
access = m.Permission.Access
entity = m.Permission.Entity
entity_type = m.Permission.Entity
book_u: m.Permission = m.Permission.query.filter_by(
access=access.U, entity=entity.BOOK
access=access.U, entity_type=entity_type.BOOK
).first()
assert not book_u
assert not m.Permission.query.count()
book_u: m.Permission = get_or_create_permission(access=access.U, entity=entity.BOOK)
book_u: m.Permission = get_or_create_permission(
access=access.U, entity_type=entity_type.BOOK
)
assert book_u
assert book_u.access == access.U
assert book_u.entity == entity.BOOK
assert book_u.entity_type == entity_type.BOOK
assert m.Permission.query.count() == 1
book_u: m.Permission = m.Permission.query.filter_by(
access=access.U, entity=entity.BOOK
access=access.U, entity_type=entity_type.BOOK
).first()
assert book_u
assert book_u.access == access.U
assert book_u.entity == entity.BOOK
assert book_u.entity_type == entity_type.BOOK
get_or_create_permission(access=access.U, entity=entity.BOOK)
get_or_create_permission(access=access.U, entity_type=entity_type.BOOK)
assert m.Permission.query.count() == 1

View File

@ -0,0 +1,31 @@
from app.controllers.init_access_groups import create_moderator_group
from app import models as m
def test_init_moderator_group(client):
create_moderator_group()
group: m.AccessGroup = m.AccessGroup.query.filter_by(name="moderator").first()
assert group
assert not group.users
assert group.permissions
permissions = group.permissions
access = m.Permission.Access
interpretation_DA: m.Permission = m.Permission.query.filter_by(
access=access.D | access.A, entity_type=m.Permission.Entity.INTERPRETATION
).first()
assert interpretation_DA
assert interpretation_DA in permissions
comment_DA: m.Permission = m.Permission.query.filter_by(
access=access.D | access.A, entity_type=m.Permission.Entity.COMMENT
).first()
assert comment_DA
assert comment_DA in permissions
create_moderator_group()
groups: list[m.AccessGroup] = m.AccessGroup.query.filter_by(name="moderator").all()
assert len(groups) == 2