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
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
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
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
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