class OauthUserRoleEntity(db.Model, CRUDMixin): """ Map users to a role and partner """ __tablename__ = 'oauth_user_role' id = db.Column(db.Integer, primary_key=True) partner_id = db.Column( db.Integer, db.ForeignKey('partner.partner_id'), nullable=False) user_id = db.Column( db.Integer, db.ForeignKey('oauth_user.id'), nullable=False) role_id = db.Column( db.Integer, db.ForeignKey('oauth_role.id'), nullable=False) added_at = db.Column(db.DateTime, nullable=False) # @OneToOne partner = db.relationship('PartnerEntity', uselist=False, lazy='joined', foreign_keys=[partner_id]) user = db.relationship('OauthUserEntity', uselist=False, lazy='joined') role = db.relationship('OauthRoleEntity', uselist=False, lazy='joined', foreign_keys=[role_id]) def __repr__(self): return "<UserRole(id: {0.id}, " \ "partner_id: {0.partner_id}, " \ "user_id: {0.user_id}, " \ "user: {0.user.email}, " \ "role_id: {0.role_id}, " \ "role: {0.role.role_code}, " \ "added_at: {0.added_at})>".format(self)
class OauthRoleEntity(db.Model, CRUDMixin): """ Roles so far: root, admin, staff """ __tablename__ = 'oauth_role' id = db.Column(db.Integer, primary_key=True) role_code = db.Column('role_code', db.Text, nullable=False) role_description = db.Column('role_description', db.Text, nullable=False) def __repr__(self): return "<OauthRoleEntity(id: {0.id}, " \ "role_code: {0.role_code}, " \ "role_description: {0.role_description})>".format(self)
class CRUDMixin(object): """ Helper class flask-sqlalchemy entities """ __table_args__ = {'extend_existing': True} id = db.Column(db.Integer, primary_key=True) @classmethod def get_by_id(cls, id): if any( (isinstance(id, str) and id.isdigit(), isinstance( id, (int, float))), ): return cls.query.get(int(id)) return None @classmethod def create(cls, **kwargs): """ Helper for session.add() + session.commit() """ instance = cls(**kwargs) return instance.save() def update(self, commit=True, **kwargs): for attr, value in kwargs.items(): setattr(self, attr, value) return self.save() if commit else self def save(self, commit=True): db.session.add(self) if commit: db.session.commit() return self def delete(self, commit=True): db.session.delete(self) return commit and db.session.commit()
class PartnerEntity(db.Model, CRUDMixin): """ Store partners sendig us the data """ __tablename__ = 'partner' id = db.Column('partner_id', db.Integer, primary_key=True) partner_code = db.Column('partner_code', db.Text, nullable=False) partner_description = db.Column('partner_description', db.Text, nullable=False) partner_added_at = db.Column('partner_added_at', db.DateTime, nullable=False) def __repr__(self): """ Return a friendly object representation """ return "<PartnerEntity(partner_id: {0.id}, "\ "partner_code: {0.partner_code}, " \ "partner_description: {0.partner_description}, " \ "partner_addded_at: {0.partner_added_at})>".format(self)
class LinkageEntity(db.Model, CRUDMixin): """ Maps the UUIDs to "hashed chunks" """ __tablename__ = 'linkage' id = db.Column('linkage_id', db.Integer, primary_key=True) partner_id = db.Column('partner_id', db.Integer, db.ForeignKey('partner.partner_id'), nullable=False) linkage_uuid = db.Column('linkage_uuid', db.Binary, nullable=False) linkage_hash = db.Column('linkage_hash', db.Binary, nullable=False) linkage_addded_at = db.Column('linkage_added_at', db.DateTime, nullable=False) # @OneToOne partner = db.relationship(PartnerEntity, uselist=False, lazy='joined') @staticmethod def short_hash(val): return val[:8] @staticmethod def load_paginated(per_page=25, page_num=1): """ Helper for formating a list of linkages """ def item_from_entity(entity): return { 'id': entity.id, 'partner_code': entity.partner.partner_code, 'added_at': entity.date_time.strftime(utils.FORMAT_US_DATE_TIME) } pagination = LinkageEntity.query.paginate(page_num, per_page, False) items = map(item_from_entity, pagination.items) return items, pagination.pages def friendly_uuid(self): return utils.hexlify(self.linkage_uuid) def friendly_hash(self): return utils.hexlify(self.linkage_hash) @staticmethod def get_chunks_cache(chunks): """ From the list [x, y, z] of chunks return a dictionary which tells if a chunk was `linked` or not: {x: LinkageEntity, y: LinkageEntity, z: None} """ bin_chunks = [ binascii.unhexlify(chunk.encode('utf-8')) for chunk in chunks ] links = LinkageEntity.query.filter( LinkageEntity.linkage_hash.in_(bin_chunks)).all() links_cache = {link.friendly_hash(): link for link in links} result = {} for chunk in chunks: result[chunk] = links_cache.get(chunk, None) return result @staticmethod def get_distinct_uuids_for_chunks(chunks_cache): """ From the list [x, y, z] of chunks return the set(uuid_1, uuid_2) if the database contains the following rows: x => uuid_1 y => uuid_1 z => uuid_2 """ result = set() for link in chunks_cache.values(): if link: result.add(link.friendly_uuid()) return result def __repr__(self): """ Return a friendly object representation """ return "<LinkageEntity(linkage_id: {0.id}, "\ "partner_id: {0.partner_id}, " \ "linkage_uuid: {1}, "\ "linkage_hash: {2}, "\ "linkage_addded_at: {0.linkage_addded_at})>".format( self, binascii.hexlify(self.linkage_uuid), binascii.hexlify(self.linkage_hash) )
class OauthAccessTokenEntity(db.Model, CRUDMixin): """ Access tokens are used for accessing protected data. """ __tablename__ = 'oauth_access_token' id = db.Column(db.Integer, primary_key=True) client_id = db.Column(db.String(40), db.ForeignKey(OauthClientEntity.client_id), nullable=False) client = db.relationship(OauthClientEntity, uselist=False, lazy='joined') # currently only bearer is supported token_type = db.Column(db.String(40)) access_token = db.Column(db.String(255), unique=True) refresh_token = db.Column(db.String(255), unique=True) # Note: this column stores UTC values expires = db.Column(db.DateTime) _scopes = db.Column(db.Text) added_at = db.Column('added_at', db.DateTime, nullable=False) @property def user(self): return self.client.user if self.client else None @property def scopes(self): if self._scopes: return self._scopes.split() return [] def is_expired(self): """ Note: If the `expires` attribute is None we consider the token expired. :rtype bool: true if 'expires' datetime > the current UTC datetime """ if self.expires: return self.expires < datetime.utcnow() return True @property def expires_in(self): """ :rtype int: the number of seconds left until the token expiration """ secs = 0 if not self.is_expired(): diff = self.expires - datetime.utcnow() secs = int(diff.total_seconds()) + 1 return secs def serialize(self): """ It is very important to return at least the following three values: - id: used for lookups - access_token: used in the request "Authorization" header - expires_in: used to determine if we need to generate a new token """ return { 'id': self.id, 'token_type': self.token_type, 'access_token': self.access_token, 'expires_in': self.expires_in, '.expires_on_utc': utils.serialize_date_utc(self.expires), '.expires_on_local': utils.serialize_date_est(self.expires), } def __repr__(self): """ Return a friendly object representation """ return "<OauthAccessTokenEntity(id: {0.id}, "\ "token_type: {0.token_type}, " \ "user_id: {0.user.id}, " \ "expires: {0.expires}, " \ "expires_in: {0.expires_in}, " \ "scopes: {0.scopes}, " \ "added_at: {0.added_at})>".format(self)
class OauthUserEntity(db.Model, UserMixin, CRUDMixin): """ A user, or resource owner, is usually the registered user on your site. """ __tablename__ = 'oauth_user' id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String(255), unique=True, nullable=False) first_name = db.Column(db.String(255), nullable=True) last_name = db.Column(db.String(255), nullable=True) mi_name = db.Column(db.String(255), nullable=True) password_hash = db.Column(db.String(255), nullable=True) added_at = db.Column('added_at', db.DateTime, nullable=False) # Indirect mappings via `oauth_user_role` table partner = db.relationship( PartnerEntity, secondary=OauthUserRoleEntity.__tablename__, primaryjoin="oauth_user.c.id==oauth_user_role.c.user_id", secondaryjoin="oauth_user_role.c.partner_id==partner.c.partner_id", backref=db.backref('oauth_user'), uselist=False, ) role = db.relationship( OauthRoleEntity, secondary=OauthUserRoleEntity.__tablename__, primaryjoin="oauth_user.c.id==oauth_user_role.c.user_id", secondaryjoin="oauth_user_role.c.role_id==oauth_role.c.id", backref=db.backref('oauth_user'), uselist=False, ) def hash_password(self, password): """ @see https://www.akkadia.org/drepper/SHA-crypt.txt """ self.password_hash = pwd_context.encrypt(password, scheme='sha512_crypt', category='admin') def verify_password(self, password): """ Return true if password matches the stored hash """ return pwd_context.verify(password, self.password_hash, scheme='sha512_crypt') # scheme='bcrypt') # scheme='bcrypt_sha256') # scheme='pbkdf2_sha512') # def is_authenticated(self): # """ Returns True if the user is authenticated, i.e. they have provided # valid credentials. # """ # return True # def is_anonymous(self): # """ Flag instances of valid users """ # return False def serialize(self): """ Helper for sending data in http responses """ return { 'id': self.id, 'email': self.email, 'added_at': self.added_at.isoformat(), } def __repr__(self): """ Return a friendly object representation """ display_partner = self.partner.partner_code if self.partner else '' display_role = self.role.role_code if self.role else '' return "<UserEntity(id: {0.id}, "\ "email: {0.email}, " \ "partner_code: {1}, " \ "role_code: {2}, " \ "added_at: {0.added_at})>".format( self, display_partner, display_role)
class OauthClientEntity(db.Model, CRUDMixin): """ A client is the app which want to use the resource of a user. It is suggested that the client is registered by a user on your site, but it is not required. """ __tablename__ = 'oauth_client' id = db.Column(db.Integer, primary_key=True) client_id = db.Column(db.String(40), unique=True) client_secret = db.Column(db.String(55), nullable=False) user_id = db.Column(db.ForeignKey('oauth_user.id')) user = db.relationship(OauthUserEntity, uselist=False, lazy='joined') _redirect_uris = db.Column(db.Text) _default_scopes = db.Column(db.Text) added_at = db.Column('added_at', db.DateTime, nullable=False) @property def client_type(self): """ confidential - an application that is capable of keeping a client password confidential to the world. This client password is assigned to the client app by the authorization server. This password is used to identify the client to the authorization server, to avoid fraud. An example of a confidential client could be a web app, where no one but the administrator can get access to the server, and see the client password. public - an application that is not capable of keeping a client password confidential. For instance, a mobile phone application or a desktop application that has the client password embedded inside it. Such an application could get cracked, and this could reveal the password. The same is true for a JavaScript application running in the users browser. The user could use a JavaScript debugger to look into the application, and see the client password """ return 'confidential' @property def redirect_uris(self): if self._redirect_uris: return self._redirect_uris.split() return [] @property def default_redirect_uri(self): # return self.redirect_uris[0] return '' @property def default_scopes(self): if self._default_scopes: return self._default_scopes.split() return [] def serialize(self): return { 'id': self.id, 'user_id': self.user_id, 'added_at': self.added_at.isoformat(), } def __repr__(self): """ Return a friendly object representation """ return "<OauthClientEntity(id: {0.id}, "\ "client_id: {0.client_id}, " \ "user_id: {0.user_id}, " \ "added_at: {0.added_at})>".format(self)