class Repo(db.Model): name = Column(String(64), primary_key=True, unique=True) owner = relationship(Node) owner_id = Column(String(64), ForeignKey("mitama_node._id")) @property def entity(self): entity = git.Repo(self.project_dir / 'repos/{}.git'.format(self.name), ) return entity def merge(self, source, target): dirname = hashlib.sha256() dirname.update(self.name.encode()) dirname.update(source.encode()) dirname.update(target.encode()) dirname = dirname.hexdigest() repo = git.Repo.clone_from(self.project_dir / 'repos/{}.git'.format(self.name), self.project_dir / 'tmp/{}'.format(dirname), branch=source) repo.index.merge_tree('origin/' + target).commit( "Merged into {}".format(source)) repo.remotes.origin.push() shutil.rmtree(self.project_dir / 'tmp/{}'.format(dirname))
class UserInvite(db.Model): __tablename__ = "mitama_user_invite" 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="") def load_noimage(self): return load_noimage_user() @property def icon(self): return self._icon or self.load_noimage() def icon_to_dataurl(self): f = magic.Magic(mime=True, uncompress=True) mime = f.from_buffer(self.icon) 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)
class Merge(db.Model): repo_id = Column(String(64), ForeignKey("izanami_repo._id")) repo = relationship(Repo) base = Column(String(255), nullable=False) compare = Column(String(255), nullable=False) body = Column(Text) title = Column(String(255)) user_id = Column(String(64), ForeignKey("mitama_user._id")) user = relationship(User) @property def meta(self): md = markdown.Metadata(extensions=["meta"]) data = md.convert(self.body) return data.Meta def merge(self): self.repo.merge(self.base, self.compare) self.event["merge"]() @property def diff(self): entity = self.repo.entity diff_str = entity.git.diff(self.base, self.compare, ignore_blank_lines=True, ignore_space_at_eol=True) diff = PatchSet(diff_str) return diff
class Profile(db.Model): name = Column(String(255)) ruby = Column(String(255)) epoch = Column(Integer) image = Column(LargeBinary) email = Column(String(255)) contactOption = Column(Integer) contactIdent = Column(String(255))
class Role(db.Model): __tablename__ = "mitama_role" name = Column(String(64)) users = relationship("User", secondary=role_user, backref="roles", cascade="all, delete") groups = relationship("Group", secondary=role_group, backref="roles", cascade="all, delete") def append(self, node): if isinstance(node, Group): self.groups.append(node) else: self.users.append(node) self.update() def remove(self, node): if isinstance(node, Group): self.groups.remove(node) else: self.users.remove(node) self.update()
class Permission(db_.Model): name = Column(String(64)) screen_name = Column(String(64), unique=True) roles = relationship("Role", secondary=role_permission, cascade="all, delete") @classmethod def accept(cls, screen_name, role): """特定のRoleに許可します """ 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.roles.remove(role) permission.update() @classmethod def is_accepted(cls, screen_name, node): """UserまたはGroupが許可されているか確認します """ perm = cls.retrieve(screen_name=screen_name) for role in perm.roles: if isinstance(node, User): if node in role.users: return True for group in role.groups: if group.is_in(node): return True else: if node in role.groups: return True return False @classmethod def is_forbidden(cls, screen_name, node): """UserまたはGroupが許可されていないか確認します """ return not cls.is_accepted(screen_name, node)
class PushSubscription(db.Model): __tablename__ = "mitama_push_subscription" _project = None user_id = Column(String(64), ForeignKey("mitama_user._id", ondelete="CASCADE")) user = relationship("User", backref="subscriptions") subscription = Column(String(1024)) def push(self, data): try: webpush(subscription_info=json.loads(self.subscription), data=json.dumps(data), vapid_private_key=self._project.vapid['private_key'], vapid_claims={ "sub": "mailto:{}".format(self._project.vapid['mailto']) }) except Exception as err: print(err)
class InnerPermission(db_.Model): name = Column(String(64)) screen_name = Column(String(64), unique=True) roles = relationship("InnerRole", secondary=inner_role_permission, cascade="all, delete") @classmethod def accept(cls, screen_name, role): """特定のRoleに許可します """ 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.roles.remove(role) permission.update() @classmethod 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) for role in permission.roles: if rel in role.relations: return True return False @classmethod def is_forbidden(cls, screen_name, group, node): """UserまたはGroupが許可されていないか確認します """ return not cls.is_accepted(screen_name, node)
class Bd(db.Model): group_id = Column(String(64), ForeignKey("mitama_group._id")) group = relationship(Group) @classmethod def is_bd(cls, user): for group in user.groups: try: cls.retrieve(group=group) return True except Exception: continue return False
class InnerRole(db.Model): __tablename__ = "mitama_inner_role" name = Column(String(64)) relations = relationship("UserGroup", secondary=role_relation, backref="roles", cascade="all, delete") def append(self, group, user): relation = UserGroup.retrieve(group=group, user=user) self.relations.append(relation) self.update() def remove(self, group, user): relation = UserGroup.retrieve(group=group, user=user) self.relations.remove(relation) self.update() def exists(self, group, user): relation = UserGroup.retrieve(group=group, user=user) return relation in self.relations
class Career(db.Model): profile = relationship(Profile, backref='careers') profile_id = Column(String(64), ForeignKey("alumni_index_profile._id")) grade = Column(Integer) type = Column(Integer)
class Group(AbstractNode, db.Model): """グループのモデルクラスです :param _id: 固有のID :param screen_name: ドメイン名 :param name: 名前 :param icon: アイコン """ __tablename__ = "mitama_group" _id = Column(String(64), default=UUID("group"), primary_key=True, nullable=False) _project = None users = relationship( "User", secondary=user_group, ) parent_id = Column(String(64), ForeignKey("mitama_group._id")) groups = relationship( "Group", backref=backref("parent", remote_side=[_id]), ) 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] return profile def load_noimage(self): return load_noimage_group() @_classproperty def relation(cls): return Column(String, ForeignKey("mitama_group._id"), nullable=False) @_classproperty def relations(cls): return relationship("mitama_group._id", cascade="all, delete") @_classproperty def relation_or_null(cls): return Column(String, ForeignKey("mitama_group._id"), nullable=True) @classmethod def tree(cls): return Group.query.filter(Group.parent == None).all() def append(self, node): if isinstance(node, User): self.users.append(node) elif isinstance(node, Group): self.groups.append(node) else: raise TypeError("Appending object must be Group or User instance") self.query.session.commit() def append_all(self, nodes): for node in nodes: if isinstance(node, User): self.users.append(node) elif isinstance(node, Group): self.groups.append(node) else: raise TypeError( "Appending object must be Group or User instance") self.query.session.commit() def remove(self, node): if not isinstance(node, Group) and not isinstance(node, User): raise TypeError("Removing object must be Group or User instance") if isinstance(node, Group): self.groups.remove(node) else: self.users.remove(node) self.query.session.commit() 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") if isinstance(node, Group): self.groups.remove(node) else: self.users.remove(node) self.query.session.commit() def is_ancestor(self, node): if not isinstance(node, Group) and not isinstance(node, User): raise TypeError("Checking object must be Group or User instance") if self.parent is None: return False layer = [self.parent] while len(layer) > 0: if isinstance(node, Group) and node in layer: return True else: for node_ in layer: if isinstance(node, User) and node_.is_in(node): return True layer_ = list() for node_ in layer: if node_.parent is not None: layer_.extend([node_.parent]) layer = layer_ return False def is_descendant(self, node): if not isinstance(node, Group) and not isinstance(node, User): raise TypeError("Checking object must be Group or User instance") layer = self.groups while len(layer) > 0: if node in layer: return True layer_ = list() for node_ in layer: layer_.extend(node_.groups) layer = layer_ return False def is_in(self, node): if isinstance(node, User): return node in self.users elif isinstance(node, Group): return node in self.groups 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 hook_registry = HookRegistry() hook_registry.delete_group(self) super().delete() def update(self): """グループの情報を更新します""" super().update() from mitama.app.hook import HookRegistry hook_registry = HookRegistry() hook_registry.update_group(self) def create(self): """グループを作成します""" super().create() from mitama.app.hook import HookRegistry hook_registry = HookRegistry() hook_registry.create_group(self) def mail(self, subject, body, type="html", to_all=False): for user in self.users: user.mail(subject, body, type) if to_all: for group in self.groups: group.mail(subject, body, type, to_all)
class ExtraColumnValue(db.Model): column_id = Column(String(64), ForeignKey("alumni_index_extra_column._id")) profile_id = Column(String(64), ForeignKey("alumni_index_profile._id")) column = relationship("ExtraColumn") profile = relationship("Profile", backref="extra") value = Column(String(255))
class ExtraColumn(db.Model): name = Column(String(255))
class TravelHistory(db.Model): profile = relationship(Profile, backref='travel_histories') profile_id = Column(String(64), ForeignKey("alumni_index_profile._id")) year = Column(Integer) place = Column(String(64)) description = Column(Text)
class User(AbstractNode, db.Model): """ユーザーのモデルクラスです :param _id: 固有のID :param screen_name: ログイン名 :param name: 名前 :param password: パスワード :param icon: アイコン """ __tablename__ = "mitama_user" _id = Column(String(64), default=UUID("user"), primary_key=True, nullable=False) _project = None _token = Column(String(64)) email = Column(String(64), nullable=False) password = Column(String(255)) groups = relationship( "Group", secondary=user_group, ) def to_dict(self, only_profile=False): profile = super().to_dict() if not only_profile: profile["groups"] = [p.to_dict(True) for p in self.groups()] return profile def load_noimage(self): return load_noimage_user() def password_check(self, password): password = base64.b64encode( hashlib.sha256(password.encode() * 10).digest()) password_ = self.password if isinstance(password_, str): password_ = password_.encode() return bcrypt.checkpw(password, password_) @classmethod def password_auth(cls, screen_name, password): """ログイン名とパスワードで認証します :param screen_name: ログイン名 :param password: パスワード :return: Userインスタンス """ try: user = cls.retrieve(_screen_name=screen_name) if user is None: raise AuthorizationError(AuthorizationError.USER_NOT_FOUND) except Exception: raise AuthorizationError(AuthorizationError.USER_NOT_FOUND) password = base64.b64encode( hashlib.sha256(password.encode() * 10).digest()) password_ = user.password if isinstance(password_, str): password_ = password_.encode() if bcrypt.checkpw(password, password_): return user else: raise AuthorizationError(AuthorizationError.WRONG_PASSWORD) def valid_password(self, password): """パスワードが安全か検証します :param password: パスワードのプレーンテキスト :return: 検証済みパスワード """ if self._project is None: return 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) 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))): raise ValueError('パスワードは数字とアルファベットの両方を含む必要があります') return password def set_password(self, password): """パスワードをハッシュ化します :param password: パスワードのプレーンテキスト :return: パスワードハッシュ """ password = self.valid_password(password) salt = bcrypt.gensalt() password = base64.b64encode( hashlib.sha256(password.encode() * 10).digest()) self.password = bcrypt.hashpw(password, salt) def mail(self, subject, body, type="html"): self._project.send_mail(self.email, subject, body, type) try: self.event["mail"](subject, body, type) except Exception: pass 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") return result @classmethod def check_jwt(cls, token): """JWTからUserインスタンスを取得します :param token: JWT :return: Userインスタンス """ try: result = jwt.decode(token, secret, algorithms="HS256") except jwt.exceptions.InvalidTokenError: raise AuthorizationError(AuthorizationError.INVALID_TOKEN) return cls.retrieve(result["id"]) def is_ancestor(self, node): if not isinstance(node, Group) and not isinstance(node, User): raise TypeError("Checking object must be Group or User instance") layer = self.groups while len(layer) > 0: if isinstance(node, Group) and node in layer: return True else: for node_ in layer: if isinstance(node, User) and node_.is_in(node): return True layer_ = list() for node_ in layer: layer_.extend(node_.groups) layer = layer_ return False def push(self, data): for subscription in self.subscriptions: subscription.push(data) try: self.event["push"](data) except Exception: pass
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 = relationship(User) group = relationship(Group) @classmethod def retrieve(cls, obj=None, id=None, **kwargs): if obj is not None: if isinstance(obj, str): if obj.split("-")[0] == "user": obj = User.retrieve(obj) else: obj = Group.retrieve(obj) try: if isinstance(obj, User): return cls.query.filter(cls.user == obj).one() else: return cls.query.filter(cls.group == obj).one() except Exception: if isinstance(obj, User): node = cls() node.user = obj node.create() return node else: node = cls() node.group = obj node.create() return node else: return super().retrieve(id=id, **kwargs) @property 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 AbstractNode(object): _icon = Column(LargeBinary().with_variant(mysql.MEDIUMBLOB, "mysql")) _name = Column("name", String(255)) _screen_name = Column("screen_name", String(255)) _name_proxy = list() _screen_name_proxy = list() _icon_proxy = list() @property def name(self): name = self._name for fn in self._name_proxy: name = fn(name) return name @property def screen_name(self): screen_name = self._screen_name for fn in self._screen_name_proxy: screen_name = fn(screen_name) return screen_name def to_dict(self): return { "_id": self._id, "name": self.name, "screen_name": self.screen_name, } @property def icon(self): if self._icon is not None: icon = self._icon else: icon = self.load_noimage() for fn in self._icon_proxy: icon = fn(icon) return icon @name.setter def name(self, value): self._name = value @screen_name.setter def screen_name(self, value): self._screen_name = value @icon.setter def icon(self, value): self._icon = value def icon_to_dataurl(self): f = magic.Magic(mime=True, uncompress=True) mime = f.from_buffer(self.icon) return "".join( ["data:", mime, ";base64,", base64.b64encode(self.icon).decode()]) @classmethod def add_name_proxy(cls, fn): cls._name_proxy.append(fn) @classmethod def add_screen_name_proxy(cls, fn): cls._screen_name_proxy.append(fn) @classmethod def add_icon_proxy(cls, fn): cls._icon_proxy.append(fn) @classmethod def retrieve(cls, _id=None, screen_name=None, **kwargs): if _id is not None: return super().retrieve(_id=_id) elif screen_name is not None: return super().retrieve(_screen_name=screen_name) else: return super().retrieve(**kwargs) def __eq__(self, other): return self._id == other._id
def inner_permission(db_, permissions): 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")), extend_existing=True) class InnerPermission(db_.Model): name = Column(String(64)) screen_name = Column(String(64), unique=True) roles = relationship("InnerRole", secondary=inner_role_permission, cascade="all, delete") @classmethod def accept(cls, screen_name, role): """特定のRoleに許可します """ 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.roles.remove(role) permission.update() @classmethod 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) for role in permission.roles: if rel in role.relations: return True return False @classmethod def is_forbidden(cls, screen_name, group, node): """UserまたはGroupが許可されていないか確認します """ return not cls.is_accepted(screen_name, node) def after_create(target, conn, **kw): try: DatabaseManager.start_session() for perm_ in permissions: perm = InnerPermission() perm.name = perm_["name"] perm.screen_name = perm_["screen_name"] db_.session.add(perm) db_.session.commit() except Exception as err: print(err) DatabaseManager.rollback_session() finally: DatabaseManager.close_session() event.listen(InnerPermission.__table__, "after_create", after_create) return InnerPermission
def __init__(self, code): self.code = code @property def message(self): return ["トークンが不正です", "パスワードが間違っています", "ユーザーが見つかりません"][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")), ) class UserGroup(db.Model): __table__ = user_group _id = user_group.c._id group_id = user_group.c.group_id user_id = user_group.c.user_id user = relationship("User") group = relationship("Group")