class UserExternalId(BaseMixin, db.Model): __tablename__ = 'userexternalid' user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) user = db.relationship(User, primaryjoin=user_id == User.id, backref=db.backref('externalids', cascade="all, delete-orphan")) service = db.Column(db.String(20), nullable=False) userid = db.Column(db.String(250), nullable=False) # Unique id (or OpenID) username = db.Column(db.Unicode(80), nullable=True) oauth_token = db.Column(db.String(250), nullable=True) oauth_token_secret = db.Column(db.String(250), nullable=True) oauth_token_type = db.Column(db.String(250), nullable=True) __table_args__ = (db.UniqueConstraint("service", "userid"), {})
class AuthCode(BaseMixin, db.Model): """Short-lived authorization tokens.""" __tablename__ = 'authcode' user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) user = db.relationship(User, primaryjoin=user_id == User.id) client_id = db.Column(db.Integer, db.ForeignKey('client.id'), nullable=False) client = db.relationship(Client, primaryjoin=client_id == Client.id, backref=db.backref("authcodes", cascade="all, delete-orphan")) code = db.Column(db.String(44), default=newsecret, nullable=False) _scope = db.Column('scope', db.Unicode(250), nullable=False) redirect_uri = db.Column(db.Unicode(250), nullable=False) used = db.Column(db.Boolean, default=False, nullable=False) @property def scope(self): return self._scope.split(u' ') @scope.setter def scope(self, value): self._scope = u' '.join(value) scope = db.synonym('_scope', descriptor=scope) def add_scope(self, additional): if isinstance(additional, basestring): additional = [additional] self.scope = list(set(self.scope).union(set(additional)))
class Team(BaseMixin, db.Model): __tablename__ = 'team' #: Unique and non-changing id userid = db.Column(db.String(22), unique=True, nullable=False, default=newid) #: Displayed name title = db.Column(db.Unicode(250), nullable=False) #: Organization org_id = db.Column(db.Integer, db.ForeignKey('organization.id'), nullable=False) org = db.relationship(Organization, primaryjoin=org_id == Organization.id, backref=db.backref('teams', order_by=title, cascade='all, delete-orphan')) users = db.relationship( User, secondary='team_membership', backref='teams') # No cascades here! Cascades will delete users def __repr__(self): return '<Team %s of %s>' % (self.title, self.org.title) @property def pickername(self): return self.title def permissions(self, user, inherited=None): perms = super(Team, self).permissions(user, inherited) if user and user in self.org.owners.users: perms.add('edit') perms.add('delete') return perms
class UserEmail(BaseMixin, db.Model): __tablename__ = 'useremail' user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) user = db.relationship(User, primaryjoin=user_id == User.id, backref=db.backref('emails', cascade="all, delete-orphan")) _email = db.Column('email', db.Unicode(80), unique=True, nullable=False) md5sum = db.Column(db.String(32), unique=True, nullable=False) primary = db.Column(db.Boolean, nullable=False, default=False) def __init__(self, email, **kwargs): super(UserEmail, self).__init__(**kwargs) self._email = email self.md5sum = md5(self._email).hexdigest() @property def email(self): return self._email #: Make email immutable. There is no setter for email. email = db.synonym('_email', descriptor=email) def __repr__(self): return u'<UserEmail %s of user %s>' % (self.email, repr(self.user)) def __unicode__(self): return unicode(self.email) def __str__(self): return str(self.__unicode__())
class PasswordResetRequest(BaseMixin, db.Model): __tablename__ = 'passwordresetrequest' user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) user = db.relationship(User, primaryjoin=user_id == User.id) reset_code = db.Column(db.String(44), nullable=False, default=newsecret) def __init__(self, **kwargs): super(PasswordResetRequest, self).__init__(**kwargs) self.reset_code = newsecret()
class SMSMessage(BaseMixin, db.Model): __tablename__ = 'smsmessage' # Phone number that the message was sent to phone_number = db.Column(db.String(15), nullable=False) transaction_id = db.Column(db.Unicode(40), unique=True, nullable=True) # The message itself message = db.Column(db.UnicodeText, nullable=False) # Flags status = db.Column(db.Integer, default=0, nullable=False) status_at = db.Column(db.DateTime, nullable=True) fail_reason = db.Column(db.Unicode(25), nullable=True)
class UserEmailClaim(BaseMixin, db.Model): __tablename__ = 'useremailclaim' user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) user = db.relationship(User, primaryjoin=user_id == User.id, backref=db.backref('emailclaims', cascade="all, delete-orphan")) _email = db.Column('email', db.Unicode(80), nullable=True) verification_code = db.Column(db.String(44), nullable=False, default=newsecret) md5sum = db.Column(db.String(32), nullable=False) def __init__(self, email, **kwargs): super(UserEmailClaim, self).__init__(**kwargs) self.verification_code = newsecret() self._email = email self.md5sum = md5(self._email).hexdigest() @property def email(self): return self._email #: Make email immutable. There is no setter for email. email = db.synonym('_email', descriptor=email) def __repr__(self): return u'<UserEmailClaim %s of user %s>' % (self.email, repr( self.user)) def __unicode__(self): return unicode(self.email) def __str__(self): return str(self.__unicode__()) def permissions(self, user, inherited=None): perms = super(UserEmailClaim, self).permissions(user, inherited) if user and user == self.user: perms.add('verify') return perms
class Client(BaseMixin, db.Model): """OAuth client applications""" __tablename__ = 'client' #: User who owns this client user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) user = db.relationship(User, primaryjoin=user_id == User.id, backref=db.backref('clients', cascade="all, delete-orphan")) #: Organization that owns this client. Only one of this or user must be set org_id = db.Column(db.Integer, db.ForeignKey('organization.id'), nullable=True) org = db.relationship(Organization, primaryjoin=org_id == Organization.id, backref=db.backref('clients', cascade="all, delete-orphan")) #: Human-readable title title = db.Column(db.Unicode(250), nullable=False) #: Long description description = db.Column(db.UnicodeText, nullable=False, default=u'') #: Website website = db.Column(db.Unicode(250), nullable=False) #: Redirect URI redirect_uri = db.Column(db.Unicode(250), nullable=True, default=u'') #: Back-end notification URI notification_uri = db.Column(db.Unicode(250), nullable=True, default=u'') #: Front-end notification URI iframe_uri = db.Column(db.Unicode(250), nullable=True, default=u'') #: Resource discovery URI resource_uri = db.Column(db.Unicode(250), nullable=True, default=u'') #: Active flag active = db.Column(db.Boolean, nullable=False, default=True) #: Allow anyone to login to this app? allow_any_login = db.Column(db.Boolean, nullable=False, default=True) #: Team access flag team_access = db.Column(db.Boolean, nullable=False, default=False) #: OAuth client key/id key = db.Column(db.String(22), nullable=False, unique=True, default=newid) #: OAuth client secret secret = db.Column(db.String(44), nullable=False, default=newsecret) #: Trusted flag: trusted clients are authorized to access user data #: without user consent, but the user must still login and identify themself. #: When a single provider provides multiple services, each can be declared #: as a trusted client to provide single sign-in across the services trusted = db.Column(db.Boolean, nullable=False, default=False) def secret_is(self, candidate): """ Check if the provided client secret is valid. """ return self.secret == candidate @property def owner(self): """ Return human-readable owner name. """ if self.user: return self.user.pickername elif self.org: return self.org.pickername else: raise AttributeError("This client has no owner") def owner_is(self, user): return self.user == user or (self.org and self.org in user.organizations_owned()) def orgs_with_team_access(self): """ Return a list of organizations that this client has access to the teams of. """ return [ cta.org for cta in self.org_team_access if cta.access_level == CLIENT_TEAM_ACCESS.ALL ] def permissions(self, user, inherited=None): perms = super(Client, self).permissions(user, inherited) perms.add('view') if user and self.owner_is(user): perms.add('edit') perms.add('delete') perms.add('assign-permissions') perms.add('new-resource') return perms
class AuthToken(BaseMixin, db.Model): """Access tokens for access to data.""" __tablename__ = 'authtoken' user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) # Null for client-only tokens user = db.relationship(User, primaryjoin=user_id == User.id) client_id = db.Column(db.Integer, db.ForeignKey('client.id'), nullable=False) client = db.relationship(Client, primaryjoin=client_id == Client.id, backref=db.backref("authtokens", cascade="all, delete-orphan")) token = db.Column(db.String(22), default=newid, nullable=False, unique=True) token_type = db.Column(db.String(250), default='bearer', nullable=False) # 'bearer', 'mac' or a URL secret = db.Column(db.String(44), nullable=True) _algorithm = db.Column('algorithm', db.String(20), nullable=True) _scope = db.Column('scope', db.Unicode(250), nullable=False) validity = db.Column(db.Integer, nullable=False, default=0) # Validity period in seconds refresh_token = db.Column(db.String(22), nullable=True, unique=True) # Only one authtoken per user and client. Add to scope as needed __table_args__ = (db.UniqueConstraint("user_id", "client_id"), {}) def __init__(self, **kwargs): super(AuthToken, self).__init__(**kwargs) self.token = newid() if self.user: self.refresh_token = newid() self.secret = newsecret() def refresh(self): """ Create a new token while retaining the refresh token. """ if self.refresh_token is not None: self.token = newid() self.secret = newsecret() @property def scope(self): return self._scope.split(u' ') @scope.setter def scope(self, value): self._scope = u' '.join(value) scope = db.synonym('_scope', descriptor=scope) def add_scope(self, additional): if isinstance(additional, basestring): additional = [additional] self.scope = list(set(self.scope).union(set(additional))) @property def algorithm(self): return self._algorithm @algorithm.setter def algorithm(self, value): if value is None: self._algorithm = None self.secret = None elif value in ['hmac-sha-1', 'hmac-sha-256']: self._algorithm = value else: raise ValueError("Unrecognized algorithm '%s'" % value) algorithm = db.synonym('_algorithm', descriptor=algorithm)
class Organization(BaseMixin, db.Model): __tablename__ = 'organization' # owners_id cannot be null, but must be declared with nullable=True since there is # a circular dependency. The post_update flag on the relationship tackles the circular # dependency within SQLAlchemy. owners_id = db.Column(db.Integer, db.ForeignKey('team.id', use_alter=True, name='fk_organization_owners_id'), nullable=True) owners = db.relationship('Team', primaryjoin='Organization.owners_id == Team.id', uselist=False, cascade='all', post_update=True) userid = db.Column(db.String(22), unique=True, nullable=False, default=newid) _name = db.Column('name', db.Unicode(80), unique=True, nullable=True) title = db.Column(db.Unicode(80), default=u'', nullable=False) description = db.Column(db.UnicodeText, default=u'', nullable=False) def __init__(self, *args, **kwargs): super(Organization, self).__init__(*args, **kwargs) if self.owners is None: self.owners = Team(title=u"Owners", org=self) @hybrid_property def name(self): return self._name @name.setter def name(self, value): if self.valid_name(value): self._name = value def valid_name(self, value): existing = Organization.query.filter_by(name=value).first() if existing and existing.id != self.id: return False existing = User.query.filter_by(username=value).first() if existing: return False return True def __repr__(self): return '<Organization %s "%s">' % (self.name or self.userid, self.title) @property def pickername(self): if self.name: return '%s (~%s)' % (self.title, self.name) else: return self.title def clients_with_team_access(self): """ Return a list of clients with access to the organization's teams. """ from lastuserapp.models.client import CLIENT_TEAM_ACCESS return [ cta.client for cta in self.client_team_access if cta.access_level == CLIENT_TEAM_ACCESS.ALL ] def permissions(self, user, inherited=None): perms = super(Organization, self).permissions(user, inherited) if user and user in self.owners.users: perms.add('view') perms.add('edit') perms.add('delete') perms.add('view-teams') perms.add('new-team') else: if 'view' in perms: perms.remove('view') if 'edit' in perms: perms.remove('edit') if 'delete' in perms: perms.remove('delete') return perms
class User(BaseMixin, db.Model): __tablename__ = 'user' userid = db.Column(db.String(22), unique=True, nullable=False, default=newid) fullname = db.Column(db.Unicode(80), default=u'', nullable=False) _username = db.Column('username', db.Unicode(80), unique=True, nullable=True) pw_hash = db.Column(db.String(80), nullable=True) timezone = db.Column(db.Unicode(40), nullable=True) description = db.Column(db.UnicodeText, default=u'', nullable=False) def __init__(self, password=None, **kwargs): self.userid = newid() self.password = password super(User, self).__init__(**kwargs) def _set_password(self, password): if password is None: self.pw_hash = None else: self.pw_hash = bcrypt.hashpw(password, bcrypt.gensalt()) password = property(fset=_set_password) @hybrid_property def username(self): return self._username @username.setter def username(self, value): if value is None: self._username = None elif self.valid_username(value): self._username = value def valid_username(self, value): existing = User.query.filter_by(username=value).first() if existing and existing.id != self.id: return False existing = Organization.query.filter_by(name=value).first() if existing: return False return True def password_is(self, password): if self.pw_hash is None: return False if self.pw_hash.startswith('sha1$'): return check_password_hash(self.pw_hash, password) else: return bcrypt.hashpw(password, self.pw_hash) == self.pw_hash def __repr__(self): return '<User %s "%s">' % (self.username or self.userid, self.fullname) def profileid(self): if self.username: return self.username else: return self.userid def displayname(self): return self.fullname or self.username or self.userid @property def pickername(self): if self.username: return '%s (~%s)' % (self.fullname, self.username) else: return self.fullname def add_email(self, email, primary=False): if primary: for emailob in self.emails: if emailob.primary: emailob.primary = False useremail = UserEmail(user=self, email=email, primary=primary) db.session.add(useremail) return useremail def del_email(self, email): setprimary = False useremail = UserEmail.query.filter_by(user=self, email=email).first() if useremail: if useremail.primary: setprimary = True db.session.delete(useremail) if setprimary: for emailob in UserEmail.query.filter_by(user_id=self.id).all(): if emailob is not useremail: emailob.primary = True break @cached_property def email(self): """ Returns primary email address for user. """ # Look for a primary address useremail = UserEmail.query.filter_by(user_id=self.id, primary=True).first() if useremail: return useremail # No primary? Maybe there's one that's not set as primary? useremail = UserEmail.query.filter_by(user_id=self.id).first() if useremail: # XXX: Mark at primary. This may or may not be saved depending on # whether the request ended in a database commit. useremail.primary = True return useremail # This user has no email address. Return a blank string instead of None # to support the common use case, where the caller will use unicode(user.email) # to get the email address as a string. return u'' def organizations(self): """ Return the organizations this user is a member of. """ return list(set([team.org for team in self.teams])) def organizations_owned(self): """ Return the organizations this user is an owner of. """ return list( set([team.org for team in self.teams if team.org.owners == team])) def organizations_owned_ids(self): """ Return the database ids of the organizations this user is an owner of. This is used for database queries. """ return list( set([ team.org.id for team in self.teams if team.org.owners == team ])) def is_profile_complete(self): """ Return True if profile is complete (fullname, username and email are present), False otherwise. """ return bool(self.fullname and self.username and self.email) @property def profile_url(self): return url_for('profile')