Example #1
0
class Resource(db.Model, TrackModificationsMixIn):
    __tablename__ = 'resource'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.String(255), nullable=False)
    role_id = db.Column(db.Integer, db.ForeignKey('role.id'))
    permissions = db.relationship('Permission', backref=db.backref('resource'), cascade='all, delete-orphan')

    def __init__(self, name, permissions):
        """Initializes a new Resource object, which is a type of Resource that a Role may have access to.

        Args:
            name (str): Name of the Resource object.
            permissions (list[str]): List of permissions ("create", "read", "update", "delete", "execute")
                for the Resource
        """
        self.name = name
        self.set_permissions(permissions)

    def set_permissions(self, new_permissions):
        """Adds the given list of permissions to the Resource object.

        Args:
            new_permissions (list|set[str]): A list of permission names with which the Resource will be associated.
                These permissions must be in the set ["create", "read", "update", "delete", "execute"].
        """
        self.permissions = []
        new_permission_names = set(new_permissions)
        self.permissions.extend([Permission(permission) for permission in new_permission_names])

    def as_json(self, with_roles=False):
        """Returns the dictionary representation of the Resource object.

        Args:
            with_roles (bool, optional): Boolean to determine whether or not to include Role objects associated with the
                Resource in the JSON representation. Defaults to False.
        """
        out = {'id': self.id, 'name': self.name, 'permissions': [permission.name for permission in self.permissions]}
        if with_roles:
            out["role"] = self.role.name
        return out
Example #2
0
class User(db.Model, TrackModificationsMixIn):
    __tablename__ = 'user'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    roles = db.relationship('Role',
                            secondary=user_roles_association,
                            backref=db.backref('users', lazy='dynamic'))
    username = db.Column(db.String(80), unique=True, nullable=False)
    _password = db.Column('password', db.String(255), nullable=False)
    active = db.Column(db.Boolean, default=True)
    last_login_at = db.Column(db.DateTime)
    current_login_at = db.Column(db.DateTime)
    last_login_ip = db.Column(db.String(45))
    current_login_ip = db.Column(db.String(45))
    login_count = db.Column(db.Integer, default=0)

    def __init__(self, name, password, roles=None):
        """Initializes a new User object

        Args:
            name (str): The username for the User.
            password (str): The password for the User.
            roles (list[int]): List of Role ids for the User. Defaults to None.
        """
        self.username = name
        self._password = pbkdf2_sha512.hash(password)
        self.roles = []
        if roles:
            self.set_roles(roles)

    @hybrid_property
    def password(self):
        """Returns the password for the user.
        """
        return self._password

    @password.setter
    def password(self, new_password):
        """Sets the password for a user, and encrypts it.

        Args:
            new_password (str): The new password for the User.
        """
        self._password = pbkdf2_sha512.hash(new_password)

    def verify_password(self, password_attempt):
        """Verifies that the input password matches with the stored password.

        Args:
            password_attempt(str): The input password.

        Returns:
            True if the passwords match, False if not.
        """
        return pbkdf2_sha512.verify(password_attempt, self._password)

    def set_roles(self, new_roles):
        """Sets the roles for a User.

        Args:
            new_roles (list[int]|set(int)): A list of Role IDs for the User.
        """

        new_role_ids = set(new_roles)
        new_roles = Role.query.filter(
            Role.id.in_(new_role_ids)).all() if new_role_ids else []

        self.roles[:] = new_roles

        roles_not_added = new_role_ids - {role.id for role in new_roles}
        if roles_not_added:
            logger.warning(
                'Cannot add roles {0} to user {1}. Roles do not exist'.format(
                    roles_not_added, self.id))

    def login(self, ip_address):
        """Tracks login information for the User upon logging in.

        Args:
            ip_address (str): The IP address from which the User logged in.
        """
        self.last_login_at = self.current_login_at
        self.current_login_at = datetime.utcnow()
        self.last_login_ip = self.current_login_ip
        self.current_login_ip = ip_address
        self.login_count += 1

    def logout(self):
        """Tracks login/logout information for the User upon logging out.
        """
        if self.login_count > 0:
            self.login_count -= 1
        else:
            logger.warning(
                'User {} logged out, but login count was already at 0'.format(
                    self.id))
        db.session.commit()

    def has_role(self, role):
        """Checks if a User has a Role associated with it.

        Args:
            role (int): The ID of the Role.

        Returns:
            True if the User has the Role, False otherwise.
        """
        return role in [role.id for role in self.roles]

    def as_json(self, with_user_history=False):
        """Returns the dictionary representation of a User object.

        Args:
            with_user_history (bool, optional): Boolean to determine whether or not to include user history in the JSON
                representation of the User. Defaults to False.

        Returns:
            The dictionary representation of a User object.
        """
        out = {
            "id": self.id,
            "username": self.username,
            "roles": [role.as_json() for role in self.roles],
            "active": self.active
        }
        if with_user_history:
            out.update({
                "last_login_at":
                utc_as_rfc_datetime(self.last_login_at),
                "current_login_at":
                utc_as_rfc_datetime(self.current_login_at),
                "last_login_ip":
                self.last_login_ip,
                "current_login_ip":
                self.current_login_ip,
                "login_count":
                self.login_count
            })
        return out
Example #3
0
class Message(db.Model):
    __tablename__ = 'message'

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    subject = db.Column(db.String())
    body = db.Column(db.String(), nullable=False)
    users = db.relationship('User',
                            secondary=user_messages_association,
                            backref=db.backref('messages', lazy='dynamic'))
    workflow_execution_id = db.Column(UUIDType(binary=False), nullable=False)
    requires_reauth = db.Column(db.Boolean, default=False)
    requires_response = db.Column(db.Boolean, default=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    history = db.relationship('MessageHistory', backref='message', lazy=True)

    def __init__(self,
                 subject,
                 body,
                 workflow_execution_id,
                 users,
                 requires_reauth=False,
                 requires_response=False):
        self.subject = subject
        self.body = body
        self.workflow_execution_id = workflow_execution_id
        self.users = users
        self.requires_reauth = requires_reauth
        self.requires_response = requires_response

    def record_user_action(self, user, action):
        if user in self.users:
            if ((action == MessageAction.unread
                 and not self.user_has_read(user)) or
                (action == MessageAction.respond and
                 (not self.requires_response or self.is_responded()[0]))):
                return
            elif action == MessageAction.delete:
                self.users.remove(user)
            self.history.append(MessageHistory(user, action))

    def user_has_read(self, user):
        user_history = [
            history_entry for history_entry in self.history
            if history_entry.user_id == user.id
        ]
        for history_entry in user_history[::-1]:
            if history_entry.action in (MessageAction.read,
                                        MessageAction.unread):
                if history_entry.action == MessageAction.unread:
                    return False
                if history_entry.action == MessageAction.read:
                    return True
        else:
            return False

    def user_last_read_at(self, user):
        user_history = [
            history_entry for history_entry in self.history
            if history_entry.user_id == user.id
        ]
        for history_entry in user_history[::-1]:
            if history_entry.action == MessageAction.read:
                return history_entry.timestamp
        else:
            return None

    def get_read_by(self):
        return {
            entry.username
            for entry in self.history if entry.action == MessageAction.read
        }

    def is_responded(self):
        if not self.requires_response:
            return False, None, None
        for history_entry in self.history[::-1]:
            if history_entry.action == MessageAction.respond:
                return True, history_entry.timestamp, history_entry.username
        else:
            return False, None, None

    def as_json(self, with_read_by=True, user=None, summary=False):
        responded, responded_at, responded_by = self.is_responded()
        ret = {
            'id': self.id,
            'subject': self.subject,
            'created_at': utc_as_rfc_datetime(self.created_at),
            'awaiting_response': self.requires_response and not responded
        }

        if user:
            ret['is_read'] = self.user_has_read(user)
            last_read_at = self.user_last_read_at(user)
            if last_read_at:
                ret['last_read_at'] = utc_as_rfc_datetime(last_read_at)
        if not summary:
            ret.update({
                'body': json.loads(self.body),
                'workflow_execution_id': str(self.workflow_execution_id),
                'requires_reauthorization': self.requires_reauth,
                'requires_response': self.requires_response
            })
            if responded:
                ret['responded_at'] = utc_as_rfc_datetime(responded_at)
                ret['responded_by'] = responded_by
            if with_read_by:
                ret['read_by'] = list(self.get_read_by())
        return ret
Example #4
0
class Role(db.Model, TrackModificationsMixIn):
    __tablename__ = 'role'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.String(80), unique=True, nullable=False)
    description = db.Column(db.String(255))
    resources = db.relationship('Resource',
                                backref=db.backref('role'),
                                cascade='all, delete-orphan')

    def __init__(self, name, description='', resources=None):
        """Initializes a Role object. Each user has one or more Roles associated with it, which determines the user's
            permissions.

        Args:
            name (str): The name of the Role.
            description (str, optional): A description of the role.
            resources (list(dict[name:resource, permissions:list[permission])): A list of dictionaries containing the
                name of the resource, and a list of permission names associated with the resource. Defaults to None.
        """
        self.name = name
        self.description = description
        self.resources = []
        if resources:
            self.set_resources(resources)

    def set_resources(self, new_resources):
        """Adds the given list of resources to the Role object.

        Args:
            new_resources (list(dict[name:resource, permissions:list[permission])): A list of dictionaries containing
                the name of the resource, and a list of permission names associated with the resource.
        """
        new_resource_names = set(
            [resource['name'] for resource in new_resources])
        current_resource_names = set(
            [resource.name
             for resource in self.resources] if self.resources else [])
        resource_names_to_add = new_resource_names - current_resource_names
        resource_names_to_delete = current_resource_names - new_resource_names
        resource_names_intersect = current_resource_names.intersection(
            new_resource_names)

        self.resources[:] = [
            resource for resource in self.resources
            if resource.name not in resource_names_to_delete
        ]

        for resource_perms in new_resources:
            if resource_perms['name'] in resource_names_to_add:
                self.resources.append(
                    Resource(resource_perms['name'],
                             resource_perms['permissions']))
            elif resource_perms['name'] in resource_names_intersect:
                resource = Resource.query.filter_by(
                    role_id=self.id, name=resource_perms['name']).first()
                if resource:
                    resource.set_permissions(resource_perms['permissions'])
        db.session.commit()

    def as_json(self, with_users=False):
        """Returns the dictionary representation of the Role object.

        Args:
            with_users (bool, optional): Boolean to determine whether or not to include User objects associated with the
                Role in the JSON representation. Defaults to False.

        Returns:
            (dict): The dictionary representation of the Role object.
        """
        out = {
            "id": self.id,
            "name": self.name,
            "description": self.description,
            "resources": [resource.as_json() for resource in self.resources]
        }
        if with_users:
            out['users'] = [user.username for user in self.users]
        return out
Example #5
0
class Message(db.Model):
    """Flask-SqlAlchemy Table which holds messages for users.

    It has a many to many relationship with both the users and roles tables.

    Attributes:
        id (int): The primary key
        subject (str): The subject of the message
        body (str): The body of the message as a JSON string
        users (list[User]): The users to which this message was sent and who haven't deleted the message
        roles (list[Role]): The roles to which this message was sent
        workflow_execution_id (UUID): The execution id of the workflow which sent the message
        requires_reauth (bool): Does the message require reauthentication to address it?
        requires_response (bool): Does the message require a response?
        created_at (datetime): Timestamp of message creation
        history (list[MessageHistory]): The timeline of actions taken on this message

    Args:
        subject (str): The subject of the message
        body (str): The body of the message as a JSON string
        users (list[User]): The users to which this message was sent and who haven't deleted the message
        roles (list[Role]): The roles to which this message was sent
        requires_reauth (bool): Does the message require reauthentication to address it?
        requires_response (bool): Does the message require a response?
    """
    __tablename__ = 'message'

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    subject = db.Column(db.String())
    body = db.Column(db.String(), nullable=False)
    users = db.relationship('User', secondary=user_messages_association, backref=db.backref('messages', lazy='dynamic'))
    roles = db.relationship('Role', secondary=role_messages_association, backref=db.backref('messages', lazy='dynamic'))
    workflow_execution_id = db.Column(UUIDType(binary=False), nullable=False)
    requires_reauth = db.Column(db.Boolean, default=False)
    requires_response = db.Column(db.Boolean, default=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    history = db.relationship('MessageHistory', backref='message', lazy=True)

    def __init__(self, subject, body, workflow_execution_id, users=None, roles=None, requires_reauth=False,
                 requires_response=False):
        self.subject = subject
        self.body = body
        self.workflow_execution_id = workflow_execution_id
        if not (users or roles):
            message = 'Message must have users and/or roles, but has neither.'
            logger.error(message)
            raise ValueError(message)
        self.users = users if users else []
        self.roles = roles if roles else []
        self.requires_reauth = requires_reauth
        self.requires_response = requires_response

    def record_user_action(self, user, action):
        """Records an action taken by a user on this message

        Args:
            user (User): The user taking the action
            action (MessageAction): The action taken
        """
        if user in self.users:
            if ((action == MessageAction.unread and not self.user_has_read(user))
                    or (action == MessageAction.respond and (not self.requires_response or self.is_responded()[0]))):
                return
            elif action == MessageAction.delete:
                self.users.remove(user)
            self.history.append(MessageHistory(user, action))

    def user_has_read(self, user):
        """Determines if a user has read the message

        Args:
            user (User): The user of the query

        Returns:
            (bool): Has the user read the message?
        """
        user_history = [history_entry for history_entry in self.history if history_entry.user_id == user.id]
        for history_entry in user_history[::-1]:
            if history_entry.action in (MessageAction.read, MessageAction.unread):
                if history_entry.action == MessageAction.unread:
                    return False
                if history_entry.action == MessageAction.read:
                    return True
        else:
            return False

    def user_last_read_at(self, user):
        """Gets the last time the user has read the message

        Args:
            user (User): The user of the query

        Returns:
            (datetime|None): The timestamp of the last time the user has read the message or None if the message has not
                been read by this user
        """
        user_history = [history_entry for history_entry in self.history if history_entry.user_id == user.id]
        for history_entry in user_history[::-1]:
            if history_entry.action == MessageAction.read:
                return history_entry.timestamp
        else:
            return None

    def get_read_by(self):
        """Gets all the usernames of the users who have read this message

        Returns:
            set(str): The usernames of the users who have read this message
        """
        return {entry.username for entry in self.history if entry.action == MessageAction.read}

    def is_responded(self):
        """Has this message been responded to?

        Returns:
            tuple(bool, datetime|None, str|None): A tuple of if the message has been responded to, if it has then the
                datetime of when it was responded to and username of the user who responded to it.
        """
        if not self.requires_response:
            return False, None, None
        for history_entry in self.history[::-1]:
            if history_entry.action == MessageAction.respond:
                return True, history_entry.timestamp, history_entry.username
        else:
            return False, None, None

    def is_authorized(self, user_id=None, role_ids=None):
        """Is a user authorized to respond to this message?

        Args:
            user_id (int): The ID of the user
            role_ids (list[int]): The ids of the roles the user has

        Returns:
            (bool)
        """
        if user_id:
            for user in self.users:
                if user_id == user.id:
                    return True

        if role_ids:
            if isinstance(role_ids, int):
                role_ids = [role_ids]
            for role_id in role_ids:
                for role in self.roles:
                    if role_id == role.id:
                        return True

        return False

    def as_json(self, with_read_by=True, user=None, summary=False):
        """Gets a JSON representation of the message

        Args:
            with_read_by (bool, optional): Should the JSON include who has read the message? Defaults to True.
            user (User, optional): If provided, information specific to the user is included.
            summary (bool, optional): If True, only give a brief summary of the messsage. Defaults to False.

        Returns:

        """
        responded, responded_at, responded_by = self.is_responded()
        ret = {'id': self.id,
               'subject': self.subject,
               'created_at': utc_as_rfc_datetime(self.created_at),
               'awaiting_response': self.requires_response and not responded}

        if user:
            ret['is_read'] = self.user_has_read(user)
            last_read_at = self.user_last_read_at(user)
            if last_read_at:
                ret['last_read_at'] = utc_as_rfc_datetime(last_read_at)
        if not summary:
            ret.update({'body': json.loads(self.body),
                        'workflow_execution_id': str(self.workflow_execution_id),
                        'requires_reauthorization': self.requires_reauth,
                        'requires_response': self.requires_response})
            if responded:
                ret['responded_at'] = utc_as_rfc_datetime(responded_at)
                ret['responded_by'] = responded_by
            if with_read_by:
                ret['read_by'] = list(self.get_read_by())
        return ret