class Client(db.Model, OAuth2ClientMixin): id = db.Column(db.Integer, primary_key=True) client_id = db.Column(db.String(48), index=True, unique=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE')) user = db.relationship( 'User', backref=db.backref( "clients", cascade="all, delete-orphan", ), ) __tablename__ = "oauth2_client" @property def redirect_uris(self): return self.client_metadata.get('redirect_uris', []) @redirect_uris.setter def redirect_uris(self, value): if isinstance(value, str): value = value.split(',') metadata = self.client_metadata metadata['redirect_uris'] = value self.set_client_metadata(metadata) @property def auth_method(self): return self.client_metadata.get('token_endpoint_auth_method') @auth_method.setter def auth_method(self, value): metadata = self.client_metadata metadata['token_endpoint_auth_method'] = value self.set_client_metadata(metadata) @classmethod def find_by_id(cls, client_id) -> Client: return cls.query.get(client_id) @classmethod def all(cls) -> List[Client]: return cls.query.all()
class User(db.Model, UserMixin): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(256), unique=True, nullable=False) password_hash = db.Column(db.LargeBinary, nullable=True) picture = db.Column(db.String(), nullable=True) permissions = db.relationship("Permission", back_populates="user", cascade="all, delete-orphan") authorizations = db.relationship("ExternalServiceAccessAuthorization", cascade="all, delete-orphan") def __init__(self, username=None) -> None: super().__init__() self.username = username def get_user_id(self): return self.id def get_authorization(self, resource: Resource): auths = ExternalServiceAccessAuthorization.find_by_user_and_resource( self, resource) # check for sub-resource authorizations for subresource in ["api"]: if hasattr(resource, subresource): auths.extend( ExternalServiceAccessAuthorization. find_by_user_and_resource(self, getattr(resource, subresource))) return auths @property def current_identity(self): from .services import current_registry, current_user if not current_user.is_anonymous: return self.oauth_identity if current_registry: for p, i in self.oauth_identity.items(): if i.provider == current_registry.server_credentials: return {p: i} return None @property def password(self): raise AttributeError("password is not a readable attribute") @password.setter def password(self, password): self.password_hash = generate_password_hash(password) @password.deleter def password(self): self.password_hash = None @property def has_password(self): return bool(self.password_hash) def has_permission(self, resource: Resource) -> bool: return self.get_permission(resource) is not None def get_permission(self, resource: Resource) -> Permission: return next((p for p in self.permissions if p.resource == resource), None) def verify_password(self, password): return check_password_hash(self.password_hash, password) def save(self): db.session.add(self) db.session.commit() def to_dict(self): return { "id": self.id, "username": self.username, "identities": {n: i.user_info for n, i in self.oauth_identity.items()} } @classmethod def find_by_username(cls, username): return cls.query.filter(cls.username == username).first() @classmethod def all(cls): return cls.query.all()
class OAuthIdentity(models.ExternalServiceAccessAuthorization, ModelMixin): id = db.Column(db.Integer, db.ForeignKey('external_service_access_authorization.id'), primary_key=True) provider_user_id = db.Column(db.String(256), nullable=False) provider_id = db.Column(db.Integer, db.ForeignKey("oauth2_identity_provider.id"), nullable=False) created_at = db.Column(DateTime, default=datetime.utcnow, nullable=False) token = db.Column(JSON, nullable=True) _user_info = None provider = db.relationship("OAuth2IdentityProvider", uselist=False, back_populates="identities") user = db.relationship( models.User, # This `backref` thing sets up an `oauth` property on the User model, # which is a dictionary of OAuth models associated with that user, # where the dictionary key is the OAuth provider name. backref=db.backref( "oauth_identity", collection_class=attribute_mapped_collection("provider.name"), cascade="all, delete-orphan", ), ) __table_args__ = (db.UniqueConstraint("provider_id", "provider_user_id"), ) __tablename__ = "oauth2_identity" __mapper_args__ = {'polymorphic_identity': 'oauth2_identity'} def __init__(self, provider, user_info, provider_user_id, token): super().__init__(self.user) self.provider = provider self.provider_user_id = provider_user_id self._user_info = user_info self.token = token self.resources.append(provider.api_resource) def as_http_header(self): return f"{self.provider.token_type} {self.token['access_token']}" @property def username(self): return f"{self.provider.name}_{self.provider_user_id}" @property def user_info(self): if not self._user_info: self._user_info = self.provider.get_user_info( self.provider_user_id, self.token) return self._user_info @user_info.setter def user_info(self, value): self._user_info = value def set_token(self, token): self.token = token def __repr__(self): parts = [] parts.append(self.__class__.__name__) if self.id: parts.append("id={}".format(self.id)) if self.provider: parts.append('provider="{}"'.format(self.provider)) return "<{}>".format(" ".join(parts)) @staticmethod def find_by_user_id(user_id, provider_name) -> OAuthIdentity: try: return OAuthIdentity.query\ .filter(OAuthIdentity.provider.has(name=provider_name))\ .filter_by(user_id=user_id).one() except NoResultFound: raise OAuthIdentityNotFoundException(f"{user_id}_{provider_name}") @staticmethod def find_by_provider_user_id(provider_user_id, provider_name) -> OAuthIdentity: try: return OAuthIdentity.query\ .filter(OAuthIdentity.provider.has(name=provider_name))\ .filter_by(provider_user_id=provider_user_id).one() except NoResultFound: raise OAuthIdentityNotFoundException( f"{provider_name}_{provider_user_id}") @classmethod def all(cls) -> List[OAuthIdentity]: return cls.query.all()
class OAuthIdentity(models.ExternalServiceAccessAuthorization, ModelMixin): id = db.Column(db.Integer, db.ForeignKey('external_service_access_authorization.id'), primary_key=True) provider_user_id = db.Column(db.String(256), nullable=False) provider_id = db.Column(db.Integer, db.ForeignKey("oauth2_identity_provider.id"), nullable=False) created_at = db.Column(DateTime, default=datetime.utcnow, nullable=False) _token = db.Column("token", JSON, nullable=True) _user_info = None provider = db.relationship("OAuth2IdentityProvider", uselist=False, back_populates="identities") user = db.relationship( models.User, # This `backref` thing sets up an `oauth` property on the User model, # which is a dictionary of OAuth models associated with that user, # where the dictionary key is the OAuth provider name. backref=db.backref( "oauth_identity", collection_class=attribute_mapped_collection("provider.name"), cascade="all, delete-orphan", ), ) __table_args__ = (db.UniqueConstraint("provider_id", "provider_user_id"), ) __tablename__ = "oauth2_identity" __mapper_args__ = {'polymorphic_identity': 'oauth2_identity'} def __init__(self, provider, user_info, provider_user_id, token): super().__init__(self.user) self.provider = provider self.provider_user_id = provider_user_id self._user_info = user_info self.token = token self.resources.append(provider.api_resource) def as_http_header(self): return f"{self.provider.token_type} {self.fetch_token()['access_token']}" @property def username(self): return f"{self.provider.name}_{self.provider_user_id}" @property def token(self) -> OAuth2Token: return OAuth2Token(self._token) @token.setter def token(self, token: dict): self._token = token def fetch_token(self): # enable dynamic refresh only if the identity # has been already stored in the database if inspect(self).persistent: # fetch up to date identity data self.refresh() # reference to the token associated with the identity instance token = self.token # the token should be refreshed # if it is expired or close to expire (i.e., n secs before expiration) if token.to_be_refreshed(): if 'refresh_token' not in token: logger.debug( "The token should be refreshed but no refresh token is associated with the token" ) else: logger.debug("Trying to refresh the token...") oauth2session = OAuth2Session(self.provider.client_id, self.provider.client_secret, token=self.token) new_token = oauth2session.refresh_token( self.provider.access_token_url, refresh_token=token['refresh_token']) self.token = new_token self.save() logger.debug("User token updated") logger.debug("Using token %r", self.token) return self.token @property def user_info(self): if not self._user_info: logger.debug( "[Identity %r], Trying to read profile of user %r from provider %r...", self.id, self.user_id, self.provider.name) self._user_info = self.provider.get_user_info( self.provider_user_id, self.fetch_token()) return self._user_info @user_info.setter def user_info(self, value): self._user_info = value def __repr__(self): parts = [] parts.append(self.__class__.__name__) if self.id: parts.append("id={}".format(self.id)) if self.provider: parts.append('provider="{}"'.format(self.provider)) return "<{}>".format(" ".join(parts)) @staticmethod def find_by_user_id(user_id, provider_name) -> OAuthIdentity: try: return OAuthIdentity.query\ .filter(OAuthIdentity.provider.has(name=provider_name))\ .filter_by(user_id=user_id).one() except NoResultFound: raise OAuthIdentityNotFoundException(f"{user_id}_{provider_name}") @staticmethod def find_by_provider_user_id(provider_user_id, provider_name) -> OAuthIdentity: try: return OAuthIdentity.query\ .filter(OAuthIdentity.provider.has(name=provider_name))\ .filter_by(provider_user_id=provider_user_id).one() except NoResultFound: raise OAuthIdentityNotFoundException( f"{provider_name}_{provider_user_id}") @classmethod def all(cls) -> List[OAuthIdentity]: return cls.query.all()
class Client(db.Model, OAuth2ClientMixin): id = db.Column(db.Integer, primary_key=True) client_id = db.Column(db.String(48), index=True, unique=True) user_id = db.Column( db.Integer, db.ForeignKey('user.id', ondelete='CASCADE') ) user = db.relationship( 'User', backref=db.backref( "clients", cascade="all, delete-orphan", ), ) __tablename__ = "oauth2_client" def is_confidential(self): return self.has_client_secret() def set_client_metadata(self, value): if not isinstance(value, dict): return data = copy.deepcopy(value) data['scope'] = values_as_string(value['scope'], out_separator=" ") for p in ('redirect_uris', 'grant_types', 'response_types', 'contacts'): data[p] = values_as_list(value.get(p, [])) return super().set_client_metadata(data) @property def redirect_uris(self): return super().redirect_uris @redirect_uris.setter def redirect_uris(self, value): metadata = self.client_metadata metadata['redirect_uris'] = value self.set_client_metadata(metadata) @property def scopes(self): return self.scope.split(" ") if self.scope else [] @scopes.setter def scopes(self, scopes): metadata = self.client_metadata metadata['scope'] = scopes self.set_client_metadata(metadata) @property def auth_method(self): return self.client_metadata.get('token_endpoint_auth_method') @auth_method.setter def auth_method(self, value): metadata = self.client_metadata metadata['token_endpoint_auth_method'] = value self.set_client_metadata(metadata) @classmethod def find_by_id(cls, client_id) -> Client: return cls.query.get(client_id) @classmethod def all(cls) -> List[Client]: return cls.query.all()