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 "**/.venv/*/**": true
}, },
"cSpell.words": [ "cSpell.words": [
"backref",
"bookname", "bookname",
"Btns", "Btns",
"flowbite", "flowbite",

View File

@ -1,10 +1,14 @@
from app import models as m 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( permission: m.Permission = m.Permission.query.filter_by(
access=access, entity=entity access=access, entity_type=entity_type
).first() ).first()
if not permission: 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 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, AccessGroup,
UserAccessGroups, UserAccessGroups,
PermissionAccessGroups, PermissionAccessGroups,
BookAccessGroups,
CollectionAccessGroups,
SectionAccessGroups,
InterpretationAccessGroups,
) )

View File

@ -19,6 +19,13 @@ class Book(BaseModel):
stars = db.relationship("User", secondary="books_stars", back_populates="stars") stars = db.relationship("User", secondary="books_stars", back_populates="stars")
contributors = db.relationship("BookContributor") contributors = db.relationship("BookContributor")
versions = db.relationship("BookVersion", order_by="asc(BookVersion.id)") 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): def __repr__(self):
return f"<{self.id}: {self.label}>" return f"<{self.id}: {self.label}>"

View File

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

View File

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

View File

@ -3,6 +3,10 @@ from .access_group import AccessGroup
from .permission import Permission from .permission import Permission
from .user_access_groups import UserAccessGroups from .user_access_groups import UserAccessGroups
from .permission_access_groups import PermissionAccessGroups 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 # access groups
# moderators(by default empty) -> root collection -> CRUD Interpretation, Comment # moderators(by default empty) -> root collection -> CRUD Interpretation, Comment
@ -26,6 +30,7 @@ from .permission_access_groups import PermissionAccessGroups
# Book # Book
# Version:
# Root Collection # Root Collection
# Collection A # Collection A
# Section # Section
@ -40,3 +45,13 @@ from .permission_access_groups import PermissionAccessGroups
# If the user has CRUD access to Collection B it means that # 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) # 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): class AccessGroup(BaseModel):
__tablename__ = "access_groups" __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 # Relationships
book = db.relationship("Book", viewonly=True)
permissions = db.relationship( permissions = db.relationship(
"Permission", "Permission",
secondary="permissions_access_groups", 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" __tablename__ = "permissions"
class Access(IntEnum): class Access(IntEnum):
C = 1 # 0b0001 C = 1 # 0b0001 - Create
R = 2 # 0b0010 U = 2 # 0b0010 - Update
U = 4 # 0b0100 D = 4 # 0b0100 - Delete
D = 8 # 0b1000 A = 8 # 0b1000 - Approve
# sum = 0b1111 # sum = 0b1111
class Entity(IntEnum): class Entity(IntEnum):
@ -22,8 +22,8 @@ class Permission(BaseModel):
INTERPRETATION = 4 INTERPRETATION = 4
COMMENT = 5 COMMENT = 5
access = db.Column(db.Integer(), default=Access.C | Access.R | Access.U | Access.D) access = db.Column(db.Integer(), default=Access.C | Access.U | Access.D | Access.A)
entity = db.Column(db.Enum(Entity), default=Entity.UNKNOWN) entity_type = db.Column(db.Enum(Entity), default=Entity.UNKNOWN)
# Relationships # Relationships
access_groups = db.relationship( 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( interpretations = db.relationship(
"Interpretation", viewonly=True, order_by="desc(Interpretation.id)" "Interpretation", viewonly=True, order_by="desc(Interpretation.id)"
) )
access_groups = db.relationship(
"AccessGroup",
secondary="sections_access_groups",
) # access_groups related to current entity
@property @property
def path(self): def path(self):

View File

@ -1137,10 +1137,6 @@ input:checked + .toggle-bg {
top: 2.5rem; top: 2.5rem;
} }
.top-3 {
top: 0.75rem;
}
.top-32 { .top-32 {
top: 8rem; top: 8rem;
} }
@ -1149,8 +1145,8 @@ input:checked + .toggle-bg {
top: 11rem; top: 11rem;
} }
.-z-10 { .top-3 {
z-index: -10; top: 0.75rem;
} }
.z-0 { .z-0 {
@ -1189,6 +1185,10 @@ input:checked + .toggle-bg {
z-index: 55; z-index: 55;
} }
.-z-10 {
z-index: -10;
}
.col-span-6 { .col-span-6 {
grid-column: span 6 / span 6; grid-column: span 6 / span 6;
} }
@ -1438,6 +1438,10 @@ input:checked + .toggle-bg {
height: 1rem; height: 1rem;
} }
.h-40 {
height: 10rem;
}
.h-5 { .h-5 {
height: 1.25rem; height: 1.25rem;
} }
@ -1446,18 +1450,10 @@ input:checked + .toggle-bg {
height: 1.5rem; height: 1.5rem;
} }
.h-64 {
height: 16rem;
}
.h-8 { .h-8 {
height: 2rem; height: 2rem;
} }
.h-80 {
height: 20rem;
}
.h-9 { .h-9 {
height: 2.25rem; height: 2.25rem;
} }
@ -1483,6 +1479,14 @@ input:checked + .toggle-bg {
height: 100vh; height: 100vh;
} }
.h-64 {
height: 16rem;
}
.h-80 {
height: 20rem;
}
.max-h-40 { .max-h-40 {
max-height: 10rem; max-height: 10rem;
} }
@ -1591,14 +1595,6 @@ input:checked + .toggle-bg {
max-width: 72rem; max-width: 72rem;
} }
.max-w-\[230\] {
max-width: 230;
}
.max-w-\[280\] {
max-width: 280;
}
.max-w-full { .max-w-full {
max-width: 100%; max-width: 100%;
} }
@ -1607,6 +1603,14 @@ input:checked + .toggle-bg {
max-width: 20rem; max-width: 20rem;
} }
.max-w-\[230\] {
max-width: 230;
}
.max-w-\[280\] {
max-width: 280;
}
.flex-1 { .flex-1 {
flex: 1 1 0%; 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)); 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 { .-translate-y-full {
--tw-translate-y: -100%; --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)); 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)); 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 { .rotate-180 {
--tw-rotate: 180deg; --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)); 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; justify-content: space-between;
} }
.gap-1 {
gap: 0.25rem;
}
.gap-2 { .gap-2 {
gap: 0.5rem; gap: 0.5rem;
} }
@ -1841,12 +1849,6 @@ input:checked + .toggle-bg {
margin-bottom: calc(2rem * var(--tw-space-y-reverse)); 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]) { .divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0; --tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); 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)); 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 { .bg-slate-300 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(203 213 225 / var(--tw-bg-opacity)); background-color: rgb(203 213 225 / var(--tw-bg-opacity));
@ -2185,6 +2192,10 @@ input:checked + .toggle-bg {
stroke: #F05252; stroke: #F05252;
} }
.\!p-0 {
padding: 0px !important;
}
.p-0 { .p-0 {
padding: 0px; padding: 0px;
} }
@ -2226,11 +2237,6 @@ input:checked + .toggle-bg {
padding-right: 0px !important; padding-right: 0px !important;
} }
.px-0 {
padding-left: 0px;
padding-right: 0px;
}
.px-2 { .px-2 {
padding-left: 0.5rem; padding-left: 0.5rem;
padding-right: 0.5rem; padding-right: 0.5rem;
@ -2286,6 +2292,11 @@ input:checked + .toggle-bg {
padding-bottom: 1rem; padding-bottom: 1rem;
} }
.px-0 {
padding-left: 0px;
padding-right: 0px;
}
.pb-3 { .pb-3 {
padding-bottom: 0.75rem; padding-bottom: 0.75rem;
} }
@ -2466,6 +2477,11 @@ input:checked + .toggle-bg {
color: rgb(14 159 110 / var(--tw-text-opacity)); 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 { .text-red-500 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(240 82 82 / var(--tw-text-opacity)); color: rgb(240 82 82 / var(--tw-text-opacity));
@ -2489,6 +2505,10 @@ input:checked + .toggle-bg {
text-decoration-line: line-through; text-decoration-line: line-through;
} }
.\!no-underline {
text-decoration-line: none !important;
}
.opacity-0 { .opacity-0 {
opacity: 0; opacity: 0;
} }
@ -2626,6 +2646,11 @@ input:checked + .toggle-bg {
background-color: rgb(155 28 28 / var(--tw-bg-opacity)); 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 { .hover\:bg-white:hover {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity)); background-color: rgb(255 255 255 / var(--tw-bg-opacity));
@ -2680,12 +2705,6 @@ input:checked + .toggle-bg {
outline-offset: 2px; 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 { .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-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); --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); 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 { .focus\:ring-blue-100:focus {
--tw-ring-opacity: 1; --tw-ring-opacity: 1;
--tw-ring-color: rgb(225 239 254 / var(--tw-ring-opacity)); --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): def test_get_or_create_permission(client):
access = m.Permission.Access access = m.Permission.Access
entity = m.Permission.Entity entity_type = m.Permission.Entity
book_u: m.Permission = m.Permission.query.filter_by( book_u: m.Permission = m.Permission.query.filter_by(
access=access.U, entity=entity.BOOK access=access.U, entity_type=entity_type.BOOK
).first() ).first()
assert not book_u assert not book_u
assert not m.Permission.query.count() 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
assert book_u.access == access.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 assert m.Permission.query.count() == 1
book_u: m.Permission = m.Permission.query.filter_by( book_u: m.Permission = m.Permission.query.filter_by(
access=access.U, entity=entity.BOOK access=access.U, entity_type=entity_type.BOOK
).first() ).first()
assert book_u assert book_u
assert book_u.access == access.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 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