class UserUsergroup(db.Model): """Represent a UserUsergroup record.""" USER_STATUS = { 'ADMIN': 'A', 'MEMBER': 'M', 'PENDING': 'P', } def __str__(self): """Return string representation.""" return "%s:%s" % (self.user.nickname, self.usergroup.name) __tablename__ = 'user_usergroup' id_user = db.Column(db.Integer(15, unsigned=True), db.ForeignKey(User.id), nullable=False, server_default='0', primary_key=True) id_usergroup = db.Column(db.Integer(15, unsigned=True), db.ForeignKey(Usergroup.id), nullable=False, server_default='0', primary_key=True) user_status = db.Column(db.CHAR(1), nullable=False, server_default='') user_status_date = db.Column(db.DateTime, nullable=False, default=datetime.now, onupdate=datetime.now) user = db.relationship(User, backref=db.backref('usergroups')) usergroup = db.relationship(Usergroup, backref=db.backref( 'users', cascade="all, delete-orphan")) def is_admin(self): """Return True if user is a admin.""" return self.user_status == self.USER_STATUS['ADMIN']
class PersistentIdentifier(db.Model): """Store and register persistent identifiers. Assumptions: * Persistent identifiers can be represented as a string of max 255 chars. * An object has many persistent identifiers. * A persistent identifier has one and only one object. """ __tablename__ = 'pidSTORE' __table_args__ = ( db.Index('uidx_type_pid', 'pid_type', 'pid_value', unique=True), db.Index('idx_status', 'status'), db.Index('idx_object', 'object_type', 'object_value'), ) id = db.Column(db.Integer(15, unsigned=True), primary_key=True) """Id of persistent identifier entry.""" pid_type = db.Column(db.String(6), nullable=False) """Persistent Identifier Schema.""" pid_value = db.Column(db.String(length=255), nullable=False) """Persistent Identifier.""" pid_provider = db.Column(db.String(length=255), nullable=False) """Persistent Identifier Provider""" status = db.Column(db.CHAR(length=1), nullable=False) """Status of persistent identifier, e.g. registered, reserved, deleted.""" object_type = db.Column(db.String(3), nullable=True) """Object Type - e.g. rec for record.""" object_value = db.Column(db.String(length=255), nullable=True) """Object ID - e.g. a record id.""" created = db.Column(db.DateTime(), nullable=False, default=datetime.now) """Creation datetime of entry.""" last_modified = db.Column( db.DateTime(), nullable=False, default=datetime.now, onupdate=datetime.now ) """Last modification datetime of entry.""" # # Class methods # @classmethod def create(cls, pid_type, pid_value, pid_provider='', provider=None): """Internally reserve a new persistent identifier. A provider for the given persistent identifier type must exists. By default the system will choose a provider according to the pid type. If desired, the default system provider can be overridden via the provider keyword argument. Return PID object if successful otherwise None. """ # Ensure provider exists if provider is None: provider = PidProvider.create(pid_type, pid_value, pid_provider) if not provider: raise Exception( "No provider found for %s:%s (%s)" % ( pid_type, pid_value, pid_provider) ) try: obj = cls(pid_type=provider.pid_type, pid_value=provider.create_new_pid(pid_value), pid_provider=pid_provider, status=cfg['PIDSTORE_STATUS_NEW']) obj._provider = provider db.session.add(obj) db.session.commit() obj.log("CREATE", "Created") return obj except SQLAlchemyError: db.session.rollback() obj.log("CREATE", "Failed to created. Already exists.") return None @classmethod def get(cls, pid_type, pid_value, pid_provider='', provider=None): """Get persistent identifier. Return None if not found. """ pid_value = to_unicode(pid_value) obj = cls.query.filter_by( pid_type=pid_type, pid_value=pid_value, pid_provider=pid_provider ).first() if obj: obj._provider = provider return obj else: return None # # Instance methods # def has_object(self, object_type, object_value): """Determine if this PID is assigned to a specific object.""" if object_type not in cfg['PIDSTORE_OBJECT_TYPES']: raise Exception("Invalid object type %s." % object_type) object_value = to_unicode(object_value) return self.object_type == object_type and \ self.object_value == object_value def get_provider(self): """Get the provider for this type of persistent identifier.""" if self._provider is None: self._provider = PidProvider.create( self.pid_type, self.pid_value, self.pid_provider ) return self._provider def assign(self, object_type, object_value, overwrite=False): """Assign this persistent identifier to a given object. Note, the persistent identifier must first have been reserved. Also, if an exsiting object is already assigned to the pid, it will raise an exception unless overwrite=True. """ if object_type not in cfg['PIDSTORE_OBJECT_TYPES']: raise Exception("Invalid object type %s." % object_type) object_value = to_unicode(object_value) if not self.id: raise Exception( "You must first create the persistent identifier before you " "can assign objects to it." ) if self.is_deleted(): raise Exception( "You cannot assign objects to a deleted persistent identifier." ) # Check for an existing object assigned to this pid existing_obj_id = self.get_assigned_object(object_type) if existing_obj_id and existing_obj_id != object_value: if not overwrite: raise Exception( "Persistent identifier is already assigned to another " "object" ) else: self.log( "ASSIGN", "Unassigned object %s:%s (overwrite requested)" % ( self.object_type, self.object_value) ) self.object_type = None self.object_value = None elif existing_obj_id and existing_obj_id == object_value: # The object is already assigned to this pid. return True self.object_type = object_type self.object_value = object_value db.session.commit() self.log("ASSIGN", "Assigned object %s:%s" % (self.object_type, self.object_value)) return True def update(self, with_deleted=False, *args, **kwargs): """Update the persistent identifier with the provider..""" if self.is_new() or self.is_reserved(): raise Exception( "Persistent identifier has not yet been registered." ) if not with_deleted and self.is_deleted(): raise Exception("Persistent identifier has been deleted.") provider = self.get_provider() if provider is None: self.log("UPDATE", "No provider found.") raise Exception("No provider found.") if provider.update(self, *args, **kwargs): if with_deleted and self.is_deleted(): self.status = cfg['PIDSTORE_STATUS_REGISTERED'] db.session.commit() return True return False def reserve(self, *args, **kwargs): """Reserve the persistent identifier with the provider. Note, the reserve method may be called multiple times, even if it was already reserved. """ if not (self.is_new() or self.is_reserved()): raise Exception( "Persistent identifier has already been registered." ) provider = self.get_provider() if provider is None: self.log("RESERVE", "No provider found.") raise Exception("No provider found.") if provider.reserve(self, *args, **kwargs): self.status = cfg['PIDSTORE_STATUS_RESERVED'] db.session.commit() return True return False def register(self, *args, **kwargs): """Register the persistent identifier with the provider.""" if self.is_registered() or self.is_deleted(): raise Exception( "Persistent identifier has already been registered." ) provider = self.get_provider() if provider is None: self.log("REGISTER", "No provider found.") raise Exception("No provider found.") if provider.register(self, *args, **kwargs): self.status = cfg['PIDSTORE_STATUS_REGISTERED'] db.session.commit() return True return False def delete(self, *args, **kwargs): """Delete the persistent identifier.""" if self.is_new(): # New persistent identifier which haven't been registered yet. Just # delete it completely but keep log) # Remove links to log entries (but otherwise leave the log entries) PidLog.query.filter_by(id_pid=self.id).update({'id_pid': None}) db.session.delete(self) self.log("DELETE", "Unregistered PID successfully deleted") else: provider = self.get_provider() if not provider.delete(self, *args, **kwargs): return False self.status = cfg['PIDSTORE_STATUS_DELETED'] db.session.commit() return True def sync_status(self, *args, **kwargs): """Synchronize persistent identifier status. Used when the provider uses an external service, which might have been modified outside of our system. """ provider = self.get_provider() result = provider.sync_status(self, *args, **kwargs) db.session.commit() return result def get_assigned_object(self, object_type=None): """Return an assigned object.""" if object_type is not None and self.object_type == object_type: return self.object_value return None def is_registered(self): """Return true if the persistent identifier has been registered.""" return self.status == cfg['PIDSTORE_STATUS_REGISTERED'] def is_deleted(self): """Return true if the persistent identifier has been deleted.""" return self.status == cfg['PIDSTORE_STATUS_DELETED'] def is_new(self): """Return true if the PIDhas not yet been registered or reserved.""" return self.status == cfg['PIDSTORE_STATUS_NEW'] def is_reserved(self): """Return true if the PID has not yet been reserved.""" return self.status == cfg['PIDSTORE_STATUS_RESERVED'] def log(self, action, message): """Store action and message in log.""" if self.pid_type and self.pid_value: message = "[%s:%s] %s" % (self.pid_type, self.pid_value, message) p = PidLog(id_pid=self.id, action=action, message=message) db.session.add(p) db.session.commit()
class Usergroup(db.Model): """Represent a Usergroup record.""" def __str__(self): """Return string representation.""" return "%s <%s>" % (self.name, self.description) __tablename__ = 'usergroup' JOIN_POLICIES = { 'VISIBLEOPEN': 'VO', 'VISIBLEMAIL': 'VM', 'INVISIBLEOPEN': 'IO', 'INVISIBLEMAIL': 'IM', 'VISIBLEEXTERNAL': 'VE', } LOGIN_METHODS = { 'INTERNAL': 'INTERNAL', 'EXTERNAL': 'EXTERNAL', } id = db.Column(db.Integer(15, unsigned=True), nullable=False, primary_key=True, autoincrement=True) name = db.Column(db.String(255), nullable=False, server_default='', unique=True, index=True) description = db.Column(db.Text, nullable=True) join_policy = db.Column(ChoiceType(map(lambda (k, v): (v, k), JOIN_POLICIES.items()), impl=db.CHAR(2)), nullable=False, server_default='') login_method = db.Column(ChoiceType( map(lambda (k, v): (v, k), LOGIN_METHODS.items())), nullable=False, server_default='INTERNAL') # FIXME Unique(login_method(70), name) __table_args__ = (db.Index('login_method_name', 'login_method', 'name', mysql_length={ 'login_method': 60, 'name': 255 }), db.Model.__table_args__) @classmethod def filter_visible(cls): """Return query object with filtered out invisible groups.""" visible = filter(lambda k: k[0] == 0, cls.JOIN_POLICIES.values()) assert len(visible) > 1 # if implementation chage use == instead of in return cls.query.filter(cls.join_policy.in_(visible)) @property def login_method_is_external(self): """Return True if the group is external.""" return self.login_method == Usergroup.LOGIN_METHODS['EXTERNAL'] def join(self, user, status=None): """Join user to group. :param user: User to add into the group. :param status: status of user """ # if I want to join another user from the group if (user.id != current_user.get_id() and # I need to be an admin of the group not self.is_admin(current_user.get_id())): raise AccountSecurityError( 'Not enough right to ' 'add user "{0}" from group "{1}"'.format( user.nickname, self.name)) # join group self.users.append( UserUsergroup( id_user=user.id, user_status=status or self.new_user_status, )) try: db.session.commit() except Exception: db.session.rollback() raise def leave(self, user): """Remove user from group. :param user: User to remove from the group. """ # if I want to remove another user from the group if (user.id != current_user.get_id() and # I need to be an admin of the group not self.is_admin(current_user.get_id())): raise AccountSecurityError( 'Not enough right to ' 'remove user "{0}" from group "{1}"'.format( user.nickname, self.name)) # check that I'm not the last admin before leaving the group. if self.is_admin(user.id) and self.admins.count() == 1: raise IntegrityUsergroupError('User can leave the group ' 'without admins, please delete the ' 'group if you want to leave.') # leave the group UserUsergroup.query.filter_by( id_usergroup=self.id, id_user=user.id, ).delete() try: db.session.commit() except Exception: db.session.rollback() raise def is_admin(self, id_user): """Return True if the user is an admin of the group.""" return db.session.query( self.admins.filter( UserUsergroup.id_user == id_user).exists()).scalar() def get_users_not_in_this_group(self, nickname=None, email=None, limit=None): """Return users that not joined this group.""" # base query query = User.query.outerjoin(User.usergroups).filter( User.id.notin_( db.select([UserUsergroup.id_user], UserUsergroup.id_usergroup == self.id))) # additional optional filters if nickname: query = query.filter(User.nickname.like(nickname)) if email: query = query.filter(User.email.like(email)) if limit: query = query.limit(limit) # return results return query @property def new_user_status(self): """Return user status for new user.""" if not self.join_policy.code.endswith('O'): return UserUsergroup.USER_STATUS['PENDING'] return UserUsergroup.USER_STATUS['MEMBER']
class AccessRequest(db.Model): """Represent an request for access to restricted files in a record.""" __tablename__ = 'accreqREQUEST' STATUS_CODES = { RequestStatus.EMAIL_VALIDATION: _(u'Email validation'), RequestStatus.PENDING: _(u'Pending'), RequestStatus.ACCEPTED: _(u'Accepted'), RequestStatus.REJECTED: _(u'Rejected'), } id = db.Column(db.Integer(unsigned=True), primary_key=True, autoincrement=True) """Access request ID.""" status = db.Column(ChoiceType(STATUS_CODES.items(), impl=db.CHAR(1)), nullable=False, index=True) """Status of request.""" receiver_user_id = db.Column(db.Integer(unsigned=True), db.ForeignKey(User.id), nullable=False, default=None) """Receiver's user id.""" receiver = db.relationship(User, foreign_keys=[receiver_user_id]) """Relationship to user""" sender_user_id = db.Column(db.Integer(unsigned=True), db.ForeignKey(User.id), nullable=True, default=None) """Sender's user id (for authenticated users).""" sender = db.relationship(User, foreign_keys=[sender_user_id]) """Relationship to user for a sender""" sender_full_name = db.Column(db.String(length=255), nullable=False, default='') """Sender's full name.""" sender_email = db.Column(db.String(length=255), nullable=False, default='') """Sender's email address.""" recid = db.Column(db.Integer(unsigned=True), nullable=False, index=True) """Record concerned for the request.""" created = db.Column(db.DateTime, nullable=False, default=datetime.now, index=True) """Creation timestamp.""" modified = db.Column(db.DateTime, nullable=False, default=datetime.now, onupdate=datetime.now) """Last modification timestamp.""" justification = db.Column(db.Text, default='', nullable=False) """Sender's justification for how they fulfill conditions.""" message = db.Column(db.Text, default='', nullable=False) """Receivers message to the sender.""" link_id = db.Column(db.Integer(unsigned=True), db.ForeignKey(SecretLink.id), nullable=True, default=None) """Relation to secret link if request was accepted.""" link = db.relationship(SecretLink, foreign_keys=[link_id]) """Relationship to secret link.""" @classmethod def create(cls, recid=None, receiver=None, sender_full_name=None, sender_email=None, justification=None, sender=None): """Create a new access request. :param recid: Record id (required). :param receiver: User object of receiver (required). :param sender_full_name: Full name of sender (required). :param sender_email: Email address of sender (required). :param justification: Justification message (required). :param sender: User object of sender (optional). """ sender_user_id = None if sender is None else sender.get_id() assert recid assert receiver assert sender_full_name assert sender_email assert justification # Determine status status = RequestStatus.EMAIL_VALIDATION if sender and sender.is_confirmed(): status = RequestStatus.PENDING # Create object obj = cls(status=status, recid=recid, receiver_user_id=receiver.get_id(), sender_user_id=sender_user_id, sender_full_name=sender_full_name, sender_email=sender_email, justification=justification) db.session.add(obj) db.session.commit() # Send signal if obj.status == RequestStatus.EMAIL_VALIDATION: request_created.send(obj) else: request_confirmed.send(obj) return obj @classmethod def query_by_receiver(cls, user): """Get access requests for a specific receiver.""" return cls.query.filter_by(receiver_user_id=user.get_id()) @classmethod def get_by_receiver(cls, request_id, user): """Get access request for a specific receiver.""" return cls.query.filter_by(id=request_id, receiver_user_id=user.get_id()).first() def confirm_email(self): """Confirm that senders email is valid.""" if self.status != RequestStatus.EMAIL_VALIDATION: raise InvalidRequestStateError(RequestStatus.EMAIL_VALIDATION) self.status = RequestStatus.PENDING db.session.commit() request_confirmed.send(self) def accept(self, message=None, expires_at=None): """Accept request.""" if self.status != RequestStatus.PENDING: raise InvalidRequestStateError(RequestStatus.PENDING) self.status = RequestStatus.ACCEPTED db.session.commit() request_accepted.send(self, message=message, expires_at=expires_at) def reject(self, message=None): """Reject request.""" if self.status != RequestStatus.PENDING: raise InvalidRequestStateError(RequestStatus.PENDING) self.status = RequestStatus.REJECTED db.session.commit() request_rejected.send(self, message=message) def create_secret_link(self, title, description=None, expires_at=None): """Create a secret link from request.""" self.link = SecretLink.create( title, self.receiver, extra_data=dict(recid=self.recid), description=description, expires_at=expires_at, ) db.session.commit() return self.link