# -*- coding: UTF-8 -*-
"""Classes to manage authentication with Google Cloud.
.. note::
To authenticate, you must have completed one of the setup options described in
:doc:`/one-time-setup/authentication`. The recommendation is to use a
:ref:`service account <service account>` and :ref:`set environment variables <set env vars>`.
In that case, you will not need to call this module directly.
.. autosummary::
Auth
----
"""
import logging
import os
import attrs
import google.auth
import google.auth.credentials
import google.oauth2.credentials
import google_auth_oauthlib.helpers
from requests_oauthlib import OAuth2Session
LOGGER = logging.getLogger(__name__)
[docs]
@attrs.define
class Auth:
"""Credentials for authenticating with a Google Cloud project.
This class provides methods to load credentials from either a service account key file or an
OAuth2 session. To authenticate, you must have completed one of the setup options described
in :doc:`/one-time-setup/authentication`.
In typical use cases, the following arguments are set as environment variables instead of
being passed to `Auth` explicitly.
Args:
GOOGLE_CLOUD_PROJECT (str, optional):
The project ID of the Google Cloud project to connect to.
GOOGLE_APPLICATION_CREDENTIALS (str, optional):
The path to a keyfile containing service account credentials. Either this or the
`OAUTH_CLIENT_*` settings are required for successful authentication.
OAUTH_CLIENT_ID (str, optional):
The client ID for an OAuth2 connection. Either this and `OAUTH_CLIENT_SECRET`, or
the `GOOGLE_APPLICATION_CREDENTIALS` setting, are required for successful
authentication.
OAUTH_CLIENT_SECRET (str, optional):
The client secret for an OAuth2 connection. Either this and `OAUTH_CLIENT_ID`, or
the `GOOGLE_APPLICATION_CREDENTIALS` setting, are required for successful
authentication.
Example:
The basic call is:
.. code-block:: python
myauth = pittgoogle.Auth()
This will load authentication settings from your :ref:`environment variables <set env vars>`.
You can override this behavior with keyword arguments. This does not automatically load the
credentials. To do that, request them explicitly:
.. code-block:: python
myauth.credentials
It will first look for a service account key file, then fallback to OAuth2.
----
"""
# Strings _below_ the field will make these also show up as individual properties in rendered docs.
GOOGLE_CLOUD_PROJECT: str | None = attrs.field(
factory=lambda: os.getenv("GOOGLE_CLOUD_PROJECT", None)
)
"""The project ID of the Google Cloud project to connect to."""
GOOGLE_APPLICATION_CREDENTIALS: str | None = attrs.field(
factory=lambda: os.getenv("GOOGLE_APPLICATION_CREDENTIALS", None)
)
"""The path to a keyfile containing service account credentials."""
OAUTH_CLIENT_ID: str | None = attrs.field(factory=lambda: os.getenv("OAUTH_CLIENT_ID", None))
"""The client ID for an OAuth2 connection."""
OAUTH_CLIENT_SECRET: str | None = attrs.field(
factory=lambda: os.getenv("OAUTH_CLIENT_SECRET", None)
)
"""The client secret for an OAuth2 connection."""
# The rest don't need string descriptions because they are explicitly defined as properties below.
_credentials = attrs.field(default=None, init=False)
_oauth2 = attrs.field(default=None, init=False)
@property
def credentials(
self,
) -> google.auth.credentials.Credentials | google.oauth2.credentials.Credentials:
"""Credentials, loaded from a service account key file or an OAuth2 session."""
if self._credentials is None:
self._credentials = self._get_credentials()
return self._credentials
def _get_credentials(
self,
) -> google.auth.credentials.Credentials | google.oauth2.credentials.Credentials:
"""Load user credentials from a service account key file or an OAuth2 session.
Try the service account first, fall back to OAuth2.
"""
# service account credentials
try:
credentials, project = google.auth.load_credentials_from_file(
self.GOOGLE_APPLICATION_CREDENTIALS
)
# OAuth2
except (TypeError, google.auth.exceptions.DefaultCredentialsError) as ekeyfile:
LOGGER.warning(
(
"Service account credentials not found for "
f"\nGOOGLE_CLOUD_PROJECT {self.GOOGLE_CLOUD_PROJECT} "
f"\nGOOGLE_APPLICATION_CREDENTIALS {self.GOOGLE_APPLICATION_CREDENTIALS}"
"\nFalling back to OAuth2. "
"If this is unexpected, check the kwargs you passed or "
"try setting environment variables."
)
)
try:
credentials = google_auth_oauthlib.helpers.credentials_from_session(self.oauth2)
except Exception as eoauth:
raise PermissionError("Cannot obtain credentials.") from Exception(
[ekeyfile, eoauth]
)
else:
if project != self.GOOGLE_CLOUD_PROJECT:
# prevent confusion about which project we'll connect to
raise ValueError(
(
f"GOOGLE_CLOUD_PROJECT ({self.GOOGLE_CLOUD_PROJECT}) "
"must match the credentials in "
"GOOGLE_APPLICATION_CREDENTIALS at "
f"{self.GOOGLE_APPLICATION_CREDENTIALS} (project: {project})."
)
)
LOGGER.info(f"Authenticated to Google Cloud project {self.GOOGLE_CLOUD_PROJECT}")
return credentials
@property
def oauth2(self) -> OAuth2Session:
"""`requests_oauthlib.OAuth2Session` connected to the Google Cloud project."""
if self._oauth2 is None:
self._oauth2 = self._authenticate_with_oauth2()
return self._oauth2
def _authenticate_with_oauth2(self) -> OAuth2Session:
"""Guide user through authentication and create `OAuth2Session` for credentials.
The user will need to visit a URL, authenticate themselves, and authorize
`PittGoogleConsumer` to make API calls on their behalf.
The user must have a Google account that is authorized make API calls
through the project defined by `GOOGLE_CLOUD_PROJECT`.
In addition, the user must be registered with Pitt-Google (this is a Google
requirement on apps that are still in dev).
"""
# create an OAuth2Session
client_id = self.OAUTH_CLIENT_ID
client_secret = self.OAUTH_CLIENT_SECRET
authorization_base_url = "https://accounts.google.com/o/oauth2/auth"
redirect_uri = "https://ardent-cycling-243415.appspot.com/" # TODO: better page
scopes = [
"https://www.googleapis.com/auth/logging.write",
"https://www.googleapis.com/auth/pubsub",
]
oauth2 = OAuth2Session(client_id, redirect_uri=redirect_uri, scope=scopes)
# instruct the user to authorize
authorization_url, _ = oauth2.authorization_url(
authorization_base_url,
access_type="offline",
# access_type="online",
# prompt="select_account",
)
print(
(
"Please visit this URL to authenticate yourself and authorize "
"PittGoogleConsumer to make API calls on your behalf:"
f"\n\n{authorization_url}\n"
)
)
authorization_response = input(
"After authorization, you should be directed to the Pitt-Google Alert "
"Broker home page. Enter the full URL of that page (it should start with "
"https://ardent-cycling-243415.appspot.com/):\n"
)
# complete the authentication
_ = oauth2.fetch_token(
"https://accounts.google.com/o/oauth2/token",
authorization_response=authorization_response,
client_secret=client_secret,
)
LOGGER.info(f"Authenticated to Google Cloud project {self.GOOGLE_CLOUD_PROJECT}")
return oauth2