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') operations = db.relationship('Operation', backref=db.backref('resource'), cascade='all, delete-orphan') def __init__(self, name, permissions, needed_ids=None): """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.needed_ids = needed_ids 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, 'operations': [(operation.operation_id, operation.permissions_list) for operation in self.operations], '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], optional): 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. Returns: (str): The password """ 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: (bool): 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( f"Cannot add roles {roles_not_added} to user {self.id}. Roles do not exist" ) 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( f"User {self.id} logged out, but login count was already at 0") 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: (bool): 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: (dict): 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 ScheduledTask(db.Model, TrackModificationsMixIn): """A SqlAlchemy table representing a a task scheduled for periodic execution Attributes: id (int): The primary key name (str): The name of the task description (str): A description of the task status (str): The status of the task. either "running" or "stopped" workflows (list[ScheduledWorkflow]): The workflows attached to this task trigger_type (str): The type of trigger to use for the scheduler. Either "date", "interval", "cron", or "unspecified" trigger_args (str): The arguments for the scheduler trigger Args: name (str): The name of the task description (str, optional): A description of the task. Defaults to empty string workflows (list[str], optional): The uuids of the workflows attached to this task. Defaults to empty list, task_trigger (dict): A dict containing two fields: "type", which contains the type of trigger to use for the scheduler ("date", "interval", "cron", or "unspecified"), and "args", which contains the arguments for the scheduler trigger """ __tablename__ = 'scheduled_task' id = db.Column(db.Integer, primary_key=True, autoincrement=True) name = db.Column(db.String(255), nullable=False) description = db.Column(db.String(255), nullable=False) status = db.Column(db.Enum('running', 'stopped', name='task_statuses')) workflows = db.relationship('ScheduledWorkflow', cascade="all, delete-orphan", backref='post', lazy='dynamic', passive_deletes=True) trigger_type = db.Column(db.Enum('date', 'interval', 'cron', 'unspecified', name='trigger_types')) trigger_args = db.Column(db.String(255)) def __init__(self, name, description='', status='running', workflows=None, task_trigger=None): self.name = name self.description = description if workflows is not None: for workflow in set(workflows): self.workflows.append(ScheduledWorkflow(workflow_id=workflow)) if task_trigger is not None: construct_trigger(task_trigger) # Throws an error if the args are invalid self.trigger_type = task_trigger['type'] self.trigger_args = json.dumps(task_trigger['args']) else: self.trigger_type = 'unspecified' self.trigger_args = '{}' self.status = status if status in ('running', 'stopped') else 'running' if self.status == 'running' and self.trigger_type != 'unspecified': self._start_workflows() def update(self, json_in): """Updates this task from a JSON representation of it Args: json_in (dict): The JSON representation of the updated task """ trigger = None if 'task_trigger' in json_in and json_in['task_trigger']: trigger = construct_trigger(json_in['task_trigger']) # Throws an error if the args are invalid self._update_scheduler(trigger) self.trigger_type = json_in['task_trigger']['type'] self.trigger_args = json.dumps(json_in['task_trigger']['args']) if 'name' in json_in: self.name = json_in['name'] if 'description' in json_in: self.description = json_in['description'] if 'workflows' in json_in and json_in['workflows']: self._modify_workflows(json_in, trigger=trigger) if 'status' in json_in and json_in['status'] != self.status: self._update_status(json_in) def start(self): """Start executing this task """ if self.status != 'running': self.status = 'running' if self.trigger_type != 'unspecified': self._start_workflows() def stop(self): """Stop executing this scheduled task """ if self.status != 'stopped': self.status = 'stopped' self._stop_workflows() def _update_status(self, json_in): self.status = json_in['status'] if self.status == 'running': self._start_workflows() elif self.status == 'stopped': self._stop_workflows() def _start_workflows(self, trigger=None): from flask import current_app trigger = trigger if trigger is not None else construct_trigger(self._reconstruct_scheduler_args()) current_app.running_context.scheduler.schedule_workflows(self.id, current_app.running_context.executor.execute_workflow, self._get_workflow_ids_as_list(), trigger) def _stop_workflows(self): from flask import current_app current_app.running_context.scheduler.unschedule_workflows(self.id, self._get_workflow_ids_as_list()) def _modify_workflows(self, json_in, trigger): from flask import current_app new, removed = self.__get_different_workflows(json_in) for workflow in self.workflows: self.workflows.remove(workflow) for workflow in json_in['workflows']: self.workflows.append(ScheduledWorkflow(workflow_id=workflow)) if self.trigger_type != 'unspecified' and self.status == 'running': trigger = trigger if trigger is not None else construct_trigger(self._reconstruct_scheduler_args()) if new: current_app.running_context.scheduler.schedule_workflows(self.id, current_app.running_context.executor.execute_workflow, new, trigger) if removed: current_app.running_context.scheduler.unschedule_workflows(self.id, removed) def _update_scheduler(self, trigger): from flask import current_app current_app.running_context.scheduler.update_workflows(self.id, trigger) def _reconstruct_scheduler_args(self): return {'type': self.trigger_type, 'args': json.loads(self.trigger_args)} def _get_workflow_ids_as_list(self): return [workflow.workflow_id for workflow in self.workflows] def __get_different_workflows(self, json_in): original_workflows = set(self._get_workflow_ids_as_list()) incoming_workflows = set(json_in['workflows']) new = incoming_workflows - original_workflows removed = original_workflows - incoming_workflows return new, removed def as_json(self): """Gets a JSON representation of this ScheduledTask Returns: (dict): The JSON representation of this ScheduledTask """ return {'id': self.id, 'name': self.name, 'description': self.description, 'status': self.status, 'workflows': self._get_workflow_ids_as_list(), 'task_trigger': self._reconstruct_scheduler_args()}
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