diff --git a/mitama/app/router.py b/mitama/app/router.py index 26e9c4b..04e7959 100644 --- a/mitama/app/router.py +++ b/mitama/app/router.py @@ -2,8 +2,6 @@ import inspect import re -from .http import Request, Response - class RoutingError(Exception): pass @@ -87,7 +85,7 @@ def match(self, request): for route in self.routes: request.subpath = path result = route.match(request) - if result != False: + if result is not False: request, result, method = result def get_response_handler(result, method): @@ -104,7 +102,10 @@ def handle(request): def result(request): return inst(request, method) - if i >= len(self.middlewares) or len(self.middlewares) == 0: + if ( + i >= len(self.middlewares) or + len(self.middlewares) == 0 + ): if callable(result): return result(request) else: @@ -113,7 +114,9 @@ def result(request): ) else: if hasattr(request, "app"): - middleware = self.middlewares[i](request.app) + middleware = self.middlewares[i]( + request.app + ) else: middleware = self.middlewares[i]() i += 1 diff --git a/mitama/db/__init__.py b/mitama/db/__init__.py index 49bc861..173e3e6 100644 --- a/mitama/db/__init__.py +++ b/mitama/db/__init__.py @@ -25,6 +25,7 @@ from .driver.sqlite3 import get_test_engine from .model import Model + class _QueryProperty: def __init__(self, db): self.db = db @@ -51,7 +52,11 @@ def test(cls): def set_engine(cls, engine): cls.engine = engine cls.metadata = MetaData(cls.engine) - cls.Model = declarative_base(cls=Model, name="Model", metadata=cls.metadata) + cls.Model = declarative_base( + cls=Model, + name="Model", + metadata=cls.metadata + ) @classmethod def start_session(cls): @@ -98,10 +103,12 @@ def __init__(self, model=None, metadata=None, query_class=Query): self.Model = self.make_declarative_base(model, metadata) def make_declarative_base(self, model=None, metadata=None): - if model == None: + if model is None: model = self.manager.Model if not isinstance(model, DeclarativeMeta): - model = declarative_base(cls=model, name="Model", metadata=metadata) + model = declarative_base( + cls=model, name="Model", metadata=metadata + ) if metadata is not None and model.metadata is not metadata: model.metadata = metadata else: @@ -135,10 +142,10 @@ class BaseDatabase(_Database): def __init__(self, prefix=None, model=None, metadata=None, query_class=Query): super().__init__( - model = model, - metadata = metadata, - query_class = query_class + model=model, + metadata=metadata, + query_class=query_class ) - if prefix == None: + if prefix is None: prefix = _inspect.getmodule(self.__class__).__package__ self.Model.prefix = prefix diff --git a/mitama/db/model.py b/mitama/db/model.py index 54c5b3a..d87f148 100755 --- a/mitama/db/model.py +++ b/mitama/db/model.py @@ -10,16 +10,17 @@ import uuid -from sqlalchemy.ext.declarative import declarative_base, declared_attr +from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.orm import ColumnProperty, class_mapper from sqlalchemy.types import TypeDecorator from mitama._extra import _classproperty, tosnake -from .types import Column, Group, Integer, LargeBinary, Node, String +from .types import Column, Integer, String from .event import Event -def UUID(prefix = None): + +def UUID(prefix=None): def genUUID(): s = str(uuid.uuid4()) if prefix is not None: @@ -27,6 +28,7 @@ def genUUID(): return s return genUUID + class Model: prefix = None _id = Column(String(64), default=UUID(), primary_key=True, nullable=False) @@ -56,13 +58,13 @@ class Type(TypeDecorator): impl = Integer def process_bind_param(self, value, dialect): - if value == None: + if value is None: return None else: return value._id def process_result_value(self, value, dialect): - if value == None: + if value is None: return None else: user = cls.retrieve(value) @@ -72,7 +74,11 @@ def process_result_value(self, value, dialect): @declared_attr def __tablename__(cls): - return ("" if cls.prefix is None else cls.prefix + "_") + tosnake(cls.__name__) + return ( + "" + if cls.prefix is None + else cls.prefix + "_" + ) + tosnake(cls.__name__) def create(self): self.query.session.add(self) @@ -105,15 +111,15 @@ def listen(cls, evt): setattr(cls, evt, Event()) @classmethod - def list(cls, cond=None): - if cond != None: - return cls.query.filter(cond).all() + def list(cls, *args): + if len(args) > 0: + return cls.query.filter(args).all() else: return cls.query.filter().all() @classmethod def retrieve(cls, id=None, **kwargs): - if id != None: + if id is not None: node = cls.query.filter(cls._id == id).one() elif len(kwargs) > 0: q = cls.query diff --git a/mitama/models/__init__.py b/mitama/models/__init__.py index 4bb01a4..f180b5e 100755 --- a/mitama/models/__init__.py +++ b/mitama/models/__init__.py @@ -9,21 +9,21 @@ * sqlalchemy用にUser型とGroup型を作って、↓のクラスをそのまま使ってDB呼び出しできるようにしたい """ -from sqlalchemy.ext.declarative import declarative_base, declared_attr -from sqlalchemy.schema import UniqueConstraint -from sqlalchemy import event - -from mitama.db import BaseDatabase, func, ForeignKey, relationship, Table, backref -from mitama.db.types import Column, Group, Integer, LargeBinary -from mitama.db.types import Node as NodeType -from mitama.db.types import String -from mitama.noimage import load_noimage_group, load_noimage_user -from mitama._extra import _classproperty - from .core_db import db -from .nodes import User, Group, Node, UserGroup, UserInvite, AuthorizationError, Role, InnerRole, PushSubscription +from .nodes import ( + User, + Group, + Node, + UserGroup, + UserInvite, + AuthorizationError, + Role, + InnerRole, + PushSubscription +) from .permissions import permission, inner_permission + Permission = permission(db, [ { "name": "権限管理", @@ -78,7 +78,26 @@ } ]) + def is_admin(node): return Permission.is_accepted('admin', node) + db.create_all() + + +__all__ = [ + User, + Group, + Node, + UserGroup, + UserInvite, + AuthorizationError, + Role, + InnerRole, + Permission, + InnerPermission, + PushSubscription, + permission, + inner_permission, +] diff --git a/mitama/models/core_db.py b/mitama/models/core_db.py index a4b4ec1..83cb260 100644 --- a/mitama/models/core_db.py +++ b/mitama/models/core_db.py @@ -1,6 +1,8 @@ from mitama.db import BaseDatabase + class Database(BaseDatabase): pass + db = Database(prefix='mitama') diff --git a/mitama/models/nodes.py b/mitama/models/nodes.py index 077433a..b9b320b 100644 --- a/mitama/models/nodes.py +++ b/mitama/models/nodes.py @@ -2,7 +2,6 @@ import hashlib import random import secrets -import smtplib from pywebpush import webpush import bcrypt @@ -12,9 +11,8 @@ from Crypto.Random import get_random_bytes from Crypto.Hash import SHA256 -from mitama.db import BaseDatabase, func, ForeignKey, relationship, Table, backref -from mitama.db.types import Column, Group, Integer, LargeBinary -from mitama.db.types import Node as NodeType +from mitama.db import ForeignKey, relationship, Table, backref +from mitama.db.types import Column, LargeBinary from mitama.db.types import String from mitama.db.model import UUID from mitama.noimage import load_noimage_group, load_noimage_user @@ -24,12 +22,15 @@ secret = secrets.token_hex(32) + class AuthorizationError(Exception): INVALID_TOKEN = 0 WRONG_PASSWORD = 1 - USER_NOT_FOUND= 2 + USER_NOT_FOUND = 2 + def __init__(self, code): self.code = code + @property def message(self): return [ @@ -37,21 +38,32 @@ def message(self): "パスワードが間違っています", "ユーザーが見つかりません" ][self.code] + def __str__(self): return self.message + user_group = Table( "mitama_user_group", db.metadata, Column("_id", String(64), default=UUID(), primary_key=True), - Column("group_id", String(64), ForeignKey("mitama_group._id", ondelete="CASCADE")), - Column("user_id", String(64), ForeignKey("mitama_user._id", ondelete="CASCADE")), + Column( + "group_id", + String(64), + ForeignKey("mitama_group._id", ondelete="CASCADE") + ), + Column( + "user_id", + String(64), + ForeignKey("mitama_user._id", ondelete="CASCADE") + ), ) + class UserGroup(db.Model): __table__ = user_group _id = user_group.c._id - group_id= user_group.c.group_id + group_id = user_group.c.group_id user_id = user_group.c.user_id user = relationship("User") group = relationship("Group") @@ -88,7 +100,7 @@ def to_dict(self): @property def icon(self): - if self._icon != None: + if self._icon is not None: icon = self._icon else: icon = self.load_noimage() @@ -111,7 +123,12 @@ def icon(self, value): def icon_to_dataurl(self): f = magic.Magic(mime=True, uncompress=True) mime = f.from_buffer(self.icon) - return "data:" + mime + ";base64," + base64.b64encode(self.icon).decode() + return "".join([ + "data:", + mime, + ";base64,", + base64.b64encode(self.icon).decode() + ]) @classmethod def add_name_proxy(cls, fn): @@ -128,15 +145,16 @@ def add_icon_proxy(cls, fn): @classmethod def retrieve(cls, _id=None, screen_name=None, **kwargs): if _id is not None: - return super().retrieve(_id = _id) + return super().retrieve(_id=_id) elif screen_name is not None: - return super().retrieve(_screen_name = screen_name) + return super().retrieve(_screen_name=screen_name) else: return super().retrieve(**kwargs) def __eq__(self, other): return self._id == other._id + class User(AbstractNode, db.Model): """ユーザーのモデルクラスです @@ -148,7 +166,12 @@ class User(AbstractNode, db.Model): """ __tablename__ = "mitama_user" - _id = Column(String(64), default=UUID("user"), primary_key = True, nullable=False) + _id = Column( + String(64), + default=UUID("user"), + primary_key=True, + nullable=False + ) _project = None _token = Column(String(64)) email = Column(String(64), nullable=False) @@ -168,7 +191,9 @@ def load_noimage(self): return load_noimage_user() def password_check(self, password): - password = base64.b64encode(hashlib.sha256(password.encode() * 10).digest()) + password = base64.b64encode( + hashlib.sha256(password.encode() * 10).digest() + ) password_ = self.password if isinstance(password_, str): password_ = password_.encode() @@ -186,9 +211,11 @@ def password_auth(cls, screen_name, password): user = cls.retrieve(_screen_name=screen_name) if user is None: raise AuthorizationError(AuthorizationError.USER_NOT_FOUND) - except: + except Exception: raise AuthorizationError(AuthorizationError.USER_NOT_FOUND) - password = base64.b64encode(hashlib.sha256(password.encode() * 10).digest()) + password = base64.b64encode( + hashlib.sha256(password.encode() * 10).digest() + ) password_ = user.password if isinstance(password_, str): password_ = password_.encode() @@ -208,13 +235,23 @@ def valid_password(self, password): project = self._project if project.password_validation is None: return password - MIN_PASSWORD_LEN = project.password_validation.get('min_password_len', None) - COMPLICATED_PASSWORD = project.password_validation.get('complicated_password', False) + MIN_PASSWORD_LEN = project.password_validation.get( + 'min_password_len', + None + ) + COMPLICATED_PASSWORD = project.password_validation.get( + 'complicated_password', + False + ) if MIN_PASSWORD_LEN and len(password) < MIN_PASSWORD_LEN: raise ValueError('パスワードは{}文字以上である必要があります'.format(MIN_PASSWORD_LEN)) - if COMPLICATED_PASSWORD and (not any(c.isdigit() for c in password)) or (not any(c.isalpha() for c in password)): + if ( + COMPLICATED_PASSWORD and ( + not any(c.isdigit() for c in password) + ) or (not any(c.isalpha() for c in password)) + ): raise ValueError('パスワードは数字とアルファベットの両方を含む必要があります') return password @@ -227,7 +264,9 @@ def set_password(self, password): """ password = self.valid_password(password) salt = bcrypt.gensalt() - password = base64.b64encode(hashlib.sha256(password.encode() * 10).digest()) + password = base64.b64encode( + hashlib.sha256(password.encode() * 10).digest() + ) self.password = bcrypt.hashpw(password, salt) def mail(self, subject, body, type="html"): @@ -235,7 +274,10 @@ def mail(self, subject, body, type="html"): def get_jwt(self): nonce = "".join([str(random.randint(0, 9)) for i in range(16)]) - result = jwt.encode({"id": self._id, "nonce": nonce}, secret, algorithm="HS256") + result = jwt.encode({ + "id": self._id, + "nonce": nonce + }, secret, algorithm="HS256") return result @classmethod @@ -247,7 +289,7 @@ def check_jwt(cls, token): """ try: result = jwt.decode(token, secret, algorithms="HS256") - except jwt.exceptions.InvalidTokenError as err: + except jwt.exceptions.InvalidTokenError: raise AuthorizationError(AuthorizationError.INVALID_TOKEN) return cls.retrieve(result["id"]) @@ -272,6 +314,7 @@ def push(self, data): for subscription in self.subscriptions: subscription.push(data) + class Group(AbstractNode, db.Model): """グループのモデルクラスです @@ -282,7 +325,12 @@ class Group(AbstractNode, db.Model): """ __tablename__ = "mitama_group" - _id = Column(String(64), default=UUID("group"), primary_key=True, nullable=False) + _id = Column( + String(64), + default=UUID("group"), + primary_key=True, + nullable=False + ) _project = None users = relationship( "User", @@ -298,8 +346,8 @@ def to_dict(self, only_profile=False): profile = super().to_dict() if not only_profile: profile["parent"] = self.parent.to_dict() - profile["groups"] = [n.to_dict(True) for n in self.groups ] - profile["users"] = [n.to_dict(True) for n in self.users ] + profile["groups"] = [n.to_dict(True) for n in self.groups] + profile["users"] = [n.to_dict(True) for n in self.users] return profile def load_noimage(self): @@ -311,7 +359,7 @@ def relation(cls): @_classproperty def relations(cls): - return relation("mitama_group._id", cascade="all, delete") + return relationship("mitama_group._id", cascade="all, delete") @_classproperty def relation_or_null(cls): @@ -319,7 +367,7 @@ def relation_or_null(cls): @classmethod def tree(cls): - return Group.query.filter(Group.parent == None).all() + return Group.query.filter(Group.parent is None).all() def append(self, node): if isinstance(node, User): @@ -337,7 +385,9 @@ def append_all(self, nodes): elif isinstance(node, Group): self.groups.append(node) else: - raise TypeError("Appending object must be Group or User instance") + raise TypeError( + "Appending object must be Group or User instance" + ) self.query.session.commit() def remove(self, node): @@ -352,7 +402,9 @@ def remove(self, node): def remove_all(self, nodes): for node in nodes: if not isinstance(node, Group) and not isinstance(node, User): - raise TypeError("Appending object must be Group or User instance") + raise TypeError( + "Appending object must be Group or User instance" + ) if isinstance(node, Group): self.groups.remove(node) else: @@ -400,6 +452,9 @@ def is_in(self, node): else: raise TypeError("Checking object must be Group or User instance") + def __contains__(self, node): + return self.is_in(node) + def delete(self): """グループを削除します""" from mitama.app.hook import HookRegistry @@ -428,20 +483,22 @@ def mail(self, subject, body, type="html", to_all=False): for group in self.groups: group.mail(subject, body, type, to_all) + def get_random_token(): s = get_random_bytes(32) h = SHA256.new() h.update(s) return h.hexdigest() + class UserInvite(db.Model): __tablename__ = "mitama_user_invite" - token = Column(String(64), default=get_random_token, unique = True) + token = Column(String(64), default=get_random_token, unique=True) email = Column(String(255)) screen_name = Column(String(255)) name = Column(String(255)) _icon = Column(LargeBinary) - roles = Column(String(255), default = "") + roles = Column(String(255), default="") def load_noimage(self): return load_noimage_user() @@ -453,25 +510,48 @@ def icon(self): def icon_to_dataurl(self): f = magic.Magic(mime=True, uncompress=True) mime = f.from_buffer(self.icon) - return "data:" + mime + ";base64," + b64encode(self.icon).decode() + return ''.join([ + "data:", + mime, + ";base64,", + base64.b64encode(self.icon).decode() + ]) def mail(self, subject, body, type="html"): self._project.send_mail(self.email, subject, body, type) + role_user = Table( "mitama_role_user", db.metadata, - Column("role_id", String(64), ForeignKey("mitama_role._id", ondelete="CASCADE")), - Column("user_id", String(64), ForeignKey("mitama_user._id", ondelete="CASCADE")) + Column( + "role_id", + String(64), + ForeignKey("mitama_role._id", ondelete="CASCADE") + ), + Column( + "user_id", + String(64), + ForeignKey("mitama_user._id", ondelete="CASCADE") + ) ) role_group = Table( "mitama_role_group", db.metadata, - Column("role_id", String(64), ForeignKey("mitama_role._id", ondelete="CASCADE")), - Column("group_id", String(64), ForeignKey("mitama_group._id", ondelete="CASCADE")) + Column( + "role_id", + String(64), + ForeignKey("mitama_role._id", ondelete="CASCADE") + ), + Column( + "group_id", + String(64), + ForeignKey("mitama_group._id", ondelete="CASCADE") + ) ) + class Role(db.Model): __tablename__ = "mitama_role" screen_name = Column(String(64), unique=True, nullable=False) @@ -508,16 +588,26 @@ def remove(self, node): "mitama_role_relation", db.metadata, Column("_id", String(64), default=UUID(), primary_key=True), - Column("role_id", String(64), ForeignKey("mitama_inner_role._id", ondelete="CASCADE")), - Column("relation_id", String(64), ForeignKey("mitama_user_group._id", ondelete="CASCADE")) + Column( + "role_id", + String(64), + ForeignKey("mitama_inner_role._id", ondelete="CASCADE") + ), + Column( + "relation_id", + String(64), + ForeignKey("mitama_user_group._id", ondelete="CASCADE") + ) ) + class RoleRelation(db.Model): __table__ = role_relation _id = role_relation.c._id role_id = role_relation.c.role_id relation_id = role_relation.c.relation_id + class InnerRole(db.Model): __tablename__ = "mitama_inner_role" screen_name = Column(String(64), unique=True, nullable=False) @@ -543,10 +633,19 @@ def exists(self, group, user): relation = UserGroup.retrieve(group=group, user=user) return relation in self.relations + class Node(db.Model): __tablename__ = "mitama_node" - user_id = Column(String(64), ForeignKey("mitama_user._id", ondelete="CASCADE"), unique=True) - group_id = Column(String(64), ForeignKey("mitama_group._id", ondelete="CASCADE"), unique=True) + user_id = Column( + String(64), + ForeignKey("mitama_user._id", ondelete="CASCADE"), + unique=True + ) + group_id = Column( + String(64), + ForeignKey("mitama_group._id", ondelete="CASCADE"), + unique=True + ) user = relationship(User) group = relationship(Group) @@ -581,10 +680,35 @@ def retrieve(cls, obj=None, id=None, **kwargs): def object(self): return self.user if self.user is not None else self.group + def is_ancestor(self, node): + if self.group is not None: + return self.group.is_ancestor(node) + else: + return False + + def is_descendant(self, node): + if self.group is not None: + return self.group.is_descendant(node) + else: + return False + + def is_in(self, node): + if self.group is not None: + return self.group.is_in(node) + else: + return False + + def __contains__(self, node): + return self.is_in(node) + + class PushSubscription(db.Model): __tablename__ = "mitama_push_subscription" _project = None - user_id = Column(String(64), ForeignKey("mitama_user._id", ondelete="CASCADE")) + user_id = Column( + String(64), + ForeignKey("mitama_user._id", ondelete="CASCADE") + ) user = relationship("User", backref="subscriptions") subscription = Column(String(1024)) diff --git a/mitama/models/permissions.py b/mitama/models/permissions.py index 7f4b664..cf07ae0 100644 --- a/mitama/models/permissions.py +++ b/mitama/models/permissions.py @@ -1,22 +1,35 @@ -from mitama.db import DatabaseManager, BaseDatabase, func, ForeignKey, relationship, Table -from mitama.db.types import Column, Group, Integer, LargeBinary -from mitama.db.types import Node as NodeType +from mitama.db import DatabaseManager, ForeignKey, relationship, Table +from mitama.db.types import Column from mitama.db.types import String from sqlalchemy import event + def permission(db_, permissions): from .nodes import User, Group, UserGroup, Role, InnerRole role_permission = Table( db_.Model.prefix + "_role_permission", db_.metadata, - Column("role_id", String(64), ForeignKey("mitama_role._id", ondelete="CASCADE"), primary_key=True), - Column("permission_id", String(64), ForeignKey(db_.Model.prefix + "_permission._id", ondelete="CASCADE"), primary_key=True), + Column( + "role_id", + String(64), + ForeignKey("mitama_role._id", ondelete="CASCADE"), + primary_key=True + ), + Column( + "permission_id", + String(64), + ForeignKey( + db_.Model.prefix + "_permission._id", + ondelete="CASCADE" + ), + primary_key=True + ), extend_existing=True ) class Permission(db_.Model): name = Column(String(64)) - screen_name = Column(String(64), unique = True) + screen_name = Column(String(64), unique=True) roles = relationship( "Role", secondary=role_permission, @@ -81,19 +94,30 @@ def after_create(target, conn, **kw): def inner_permission(db_, permissions): - from .nodes import User, Group, UserGroup, Role, InnerRole + from .nodes import User, Group, Node, UserGroup, Role, InnerRole inner_role_permission = Table( db_.Model.prefix + "_inner_role_permission", db_.metadata, - Column("role_id", String(64), ForeignKey("mitama_inner_role._id", ondelete="CASCADE")), - Column("permission_id", String(64), ForeignKey(db_.Model.prefix + "_inner_permission._id", ondelete="CASCADE")), + Column( + "role_id", + String(64), + ForeignKey("mitama_inner_role._id", ondelete="CASCADE") + ), + Column( + "permission_id", + String(64), + ForeignKey( + db_.Model.prefix + "_inner_permission._id", + ondelete="CASCADE" + ) + ), extend_existing=True ) class InnerPermission(db_.Model): name = Column(String(64)) - screen_name = Column(String(64), unique = True) + screen_name = Column(String(64), unique=True) roles = relationship( "InnerRole", secondary=inner_role_permission, @@ -103,14 +127,14 @@ class InnerPermission(db_.Model): @classmethod def accept(cls, screen_name, role): """特定のRoleに許可します """ - permission = cls.retrieve(screen_name = screen_name) + permission = cls.retrieve(screen_name=screen_name) permission.roles.append(role) permission.update() @classmethod def forbit(cls, screen_name, role): """UserまたはGroupの許可を取りやめます """ - permission = cls.retrieve(screen_name = screen_name) + permission = cls.retrieve(screen_name=screen_name) permission.roles.remove(role) permission.update() @@ -118,8 +142,12 @@ def forbit(cls, screen_name, role): def is_accepted(cls, screen_name, group, user): """UserまたはGroupが許可されているか確認します """ + if isinstance(group, Node): + group = group.object + if isinstance(group, User): + return False rel = UserGroup.retrieve(user=user, group=group) - permission = cls.retrieve(screen_name = screen_name) + permission = cls.retrieve(screen_name=screen_name) for role in permission.roles: if rel in role.relations: return True @@ -149,4 +177,3 @@ def after_create(target, conn, **kw): event.listen(InnerPermission.__table__, "after_create", after_create) return InnerPermission - diff --git a/mitama/models/roles.py b/mitama/models/roles.py deleted file mode 100644 index fa1bcd4..0000000 --- a/mitama/models/roles.py +++ /dev/null @@ -1,9 +0,0 @@ -from mitama.db import BaseDatabase, func, ForeignKey, relationship, Table, backref -from mitama.db.types import Column, Integer, LargeBinary -from mitama.db.types import Node as NodeType -from mitama.db.types import String -from mitama.db.model import UUID - -from .core_db import db -from .nodes import User, Group, UserGroup -