improve rbac

This commit is contained in:
knst-kotov 2021-08-17 12:14:26 +03:00
parent 9618802c6b
commit b8b7854c4c
9 changed files with 132 additions and 62 deletions

View File

@ -63,6 +63,7 @@ class JWTAuthenticate(AuthenticationBackend):
return AuthCredentials(scopes=[], error_message=str(exc)), AuthUser(user_id=None)
scopes = User.get_permission(user_id=payload.user_id)
print(scopes)
return AuthCredentials(user_id=payload.user_id, scopes=scopes, logged_in=True), AuthUser(user_id=payload.user_id)

View File

@ -9,7 +9,7 @@ class Permission(BaseModel):
class AuthCredentials(BaseModel):
user_id: Optional[int] = None
scopes: Optional[set] = {}
scopes: Optional[dict] = {}
logged_in: bool = False
error_message: str = ""

0
create_crt.sh Executable file → Normal file
View File

View File

@ -1,4 +1,4 @@
from orm.rbac import Operation, Permission, Role
from orm.rbac import Organization, Operation, Resource, Permission, Role
from orm.user import User
from orm.message import Message
from orm.shout import Shout
@ -7,3 +7,5 @@ from orm.base import Base, engine
__all__ = ["User", "Role", "Operation", "Permission", "Message", "Shout"]
Base.metadata.create_all(engine)
Operation.init_table()
Resource.init_table()

View File

@ -3,63 +3,85 @@ import warnings
from typing import Type
from sqlalchemy import String, Column, ForeignKey, types, UniqueConstraint
from sqlalchemy.orm import relationship
from orm.base import Base, REGISTRY, engine
from orm.base import Base, REGISTRY, engine, local_session
class ClassType(types.TypeDecorator):
impl = types.String
impl = types.String
@property
def python_type(self):
return NotImplemented
@property
def python_type(self):
return NotImplemented
def process_literal_param(self, value, dialect):
return NotImplemented
def process_literal_param(self, value, dialect):
return NotImplemented
def process_bind_param(self, value, dialect):
return value.__name__ if isinstance(value, type) else str(value)
def process_bind_param(self, value, dialect):
return value.__name__ if isinstance(value, type) else str(value)
def process_result_value(self, value, dialect):
class_ = REGISTRY.get(value)
if class_ is None:
warnings.warn(f"Can't find class <{value}>,find it yourself 😊", stacklevel=2)
return class_
def process_result_value(self, value, dialect):
class_ = REGISTRY.get(value)
if class_ is None:
warnings.warn(f"Can't find class <{value}>,find it yourself 😊", stacklevel=2)
return class_
class Organization(Base):
__tablename__ = 'organization'
name: str = Column(String, nullable=False, unique=True, comment="Organization Name")
class Role(Base):
__tablename__ = 'role'
name: str = Column(String, nullable=False, unique=True, comment="Role Name")
__tablename__ = 'role'
name: str = Column(String, nullable=False, unique=True, comment="Role Name")
org_id: int = Column(ForeignKey("organization.id", ondelete="CASCADE"), nullable=False, comment="Organization")
permissions = relationship("Permission")
class Operation(Base):
__tablename__ = 'operation'
name: str = Column(String, nullable=False, unique=True, comment="Operation Name")
__tablename__ = 'operation'
name: str = Column(String, nullable=False, unique=True, comment="Operation Name")
@staticmethod
def init_table():
with local_session() as session:
edit_op = session.query(Operation).filter(Operation.name == "edit").first()
if not edit_op:
edit_op = Operation.create(name = "edit")
Operation.edit_id = edit_op.id
class Resource(Base):
__tablename__ = "resource"
resource_class: Type[Base] = Column(ClassType, nullable=False, unique=True, comment="Resource class")
name: str = Column(String, nullable=False, unique=True, comment="Resource name")
__tablename__ = "resource"
resource_class: Type[Base] = Column(ClassType, nullable=False, unique=True, comment="Resource class")
name: str = Column(String, nullable=False, unique=True, comment="Resource name")
@staticmethod
def init_table():
with local_session() as session:
shout_res = session.query(Resource).filter(Resource.name == "shout").first()
if not shout_res:
shout_res = Resource.create(name = "shout", resource_class = "shout")
Resource.shout_id = shout_res.id
class Permission(Base):
__tablename__ = "permission"
__table_args__ = (UniqueConstraint("role_id", "operation_id", "resource_id"), {"extend_existing": True})
__tablename__ = "permission"
__table_args__ = (UniqueConstraint("role_id", "operation_id", "resource_id"), {"extend_existing": True})
role_id: int = Column(ForeignKey("role.id", ondelete="CASCADE"), nullable=False, comment="Role")
operation_id: int = Column(ForeignKey("operation.id", ondelete="CASCADE"), nullable=False, comment="Operation")
resource_id: int = Column(ForeignKey("operation.id", ondelete="CASCADE"), nullable=False, comment="Resource")
role_id: int = Column(ForeignKey("role.id", ondelete="CASCADE"), nullable=False, comment="Role")
operation_id: int = Column(ForeignKey("operation.id", ondelete="CASCADE"), nullable=False, comment="Operation")
resource_id: int = Column(ForeignKey("operation.id", ondelete="CASCADE"), nullable=False, comment="Resource")
if __name__ == '__main__':
Base.metadata.create_all(engine)
ops = [
Permission(role_id=1, operation_id=1, resource_id=1),
Permission(role_id=1, operation_id=2, resource_id=1),
Permission(role_id=1, operation_id=3, resource_id=1),
Permission(role_id=1, operation_id=4, resource_id=1),
Permission(role_id=2, operation_id=4, resource_id=1)
]
global_session.add_all(ops)
global_session.commit()
Base.metadata.create_all(engine)
ops = [
Permission(role_id=1, operation_id=1, resource_id=1),
Permission(role_id=1, operation_id=2, resource_id=1),
Permission(role_id=1, operation_id=3, resource_id=1),
Permission(role_id=1, operation_id=4, resource_id=1),
Permission(role_id=2, operation_id=4, resource_id=1)
]
global_session.add_all(ops)
global_session.commit()

View File

@ -12,7 +12,7 @@ class Shout(Base):
id = None
slug: str = Column(String, primary_key=True)
org: str = Column(String, nullable=False)
org_id: str = Column(ForeignKey("organization.id"), nullable=False)
author_id: str = Column(ForeignKey("user.id"), nullable=False, comment="Author")
body: str = Column(String, nullable=False, comment="Body")
createdAt: str = Column(DateTime, nullable=False, default = datetime.now, comment="Created at")

View File

@ -1,11 +1,19 @@
from typing import List
from sqlalchemy import Column, Integer, String, ForeignKey #, relationship
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from orm import Permission
from orm.base import Base, local_session
class UserRole(Base):
__tablename__ = 'user_role'
id = None
user_id: int = Column(ForeignKey("user.id"), primary_key = True)
role_id: int = Column(ForeignKey("role.id"), primary_key = True)
class User(Base):
__tablename__ = 'user'
@ -13,16 +21,19 @@ class User(Base):
username: str = Column(String, nullable=False, comment="Name")
password: str = Column(String, nullable=True, comment="Password")
role_id: list = Column(ForeignKey("role.id"), nullable=True, comment="Role")
# roles = relationship("Role") TODO: one to many, see schema.graphql
oauth_id: str = Column(String, nullable=True)
roles = relationship("Role", secondary=UserRole.__table__)
@classmethod
def get_permission(cls, user_id):
scope = {}
with local_session() as session:
perms: List[Permission] = session.query(Permission).join(User, User.role_id == Permission.role_id).filter(
User.id == user_id).all()
return {f"{p.operation_id}-{p.resource_id}" for p in perms}
user = session.query(User).filter(User.id == user_id).first()
for role in user.roles:
for p in role.permissions:
scope[p.resource_id] = p.operation_id
return scope
if __name__ == '__main__':

View File

@ -1,4 +1,4 @@
from orm import Shout, User
from orm import Shout, User, Organization
from orm.base import local_session
from resolvers.base import mutation, query
@ -15,10 +15,10 @@ class GitTask:
queue = asyncio.Queue()
def __init__(self, input, username, user_email, comment):
def __init__(self, input, org, username, user_email, comment):
self.slug = input["slug"];
self.org = input["org"];
self.shout_body = input["body"];
self.org = org;
self.username = username;
self.user_email = user_email;
self.comment = comment;
@ -84,12 +84,19 @@ async def create_shout(_, info, input):
auth = info.context["request"].auth
user_id = auth.user_id
org_id = org = input["org_id"]
with local_session() as session:
user = session.query(User).filter(User.id == user_id).first()
org = session.query(Organization).filter(Organization.id == org_id).first()
if not org:
return {
"error" : "invalid organization"
}
new_shout = Shout.create(
slug = input["slug"],
org = input["org"],
org_id = org_id,
author_id = user_id,
body = input["body"],
replyTo = input.get("replyTo"),
@ -100,6 +107,7 @@ async def create_shout(_, info, input):
task = GitTask(
input,
org.name,
user.username,
user.email,
"new shout %s" % (new_shout.slug)
@ -109,5 +117,30 @@ async def create_shout(_, info, input):
"shout" : new_shout
}
@mutation.field("updateShout")
@login_required
async def update_shout(_, info, shout_id, input):
auth = info.context["request"].auth
user_id = auth.user_id
with local_session() as session:
user = session.query(User).filter(User.id == user_id).first()
shout = session.query(Shout).filter(Shout.id == shout_id).first()
if not shout:
return {
"error" : "shout not found"
}
if shout.author_id != user_id:
scope = info.context["request"].scope
if not Resource.shout_id in scope:
return {
"error" : "access denied"
}
return {
"shout" : shout
}
# TODO: paginate, get, update, delete

View File

@ -23,7 +23,7 @@ type MessageResult {
}
input ShoutInput {
org: String!
org_id: Int!
slug: String!
body: String!
replyTo: String # another shout
@ -61,10 +61,11 @@ type Mutation {
# invalidateTokenById(id: Int!): Boolean!
# requestEmailConfirmation: User!
# requestPasswordReset(email: String!): Boolean!
registerUser(email: String!, password: String!): AuthResult!
registerUser(email: String!, password: String!): AuthResult!
# shout
createShout(input: ShoutInput!): ShoutResult!
updateShout(input: ShoutInput!): ShoutResult!
deleteShout(slug: String!): Result!
rateShout(slug: String!, value: Int!): Result!
@ -151,7 +152,7 @@ type Message {
# is publication
type Shout {
org: String!
org_id: Int!
slug: String!
author: Int!
body: String!