GAE AttributeError: 'Credentials' object has no attribute 'with_subject'

GAE AttributeError: 'Credentials' object has no attribute 'with_subject'

我有一个 python 应用程序,我想在 App Engine(第 2 代 Python 3.7)上部署,我在该应用程序上使用启用了全域委派的服务帐户来访问用户数据。

我在本地做:

import google.auth
from apiclient.discovery import build

creds, project = google.auth.default(
    scopes=['https://www.googleapis.com/auth/admin.directory.user', ],
)
creds = creds.with_subject(GSUITE_ADMIN_USER)

service = build('admin', 'directory_v1', credentials=creds)

这很好用,据我所知,这是 current 在使用应用程序默认凭据时执行此操作的方法(我在本地定义了 GOOGLE_APPLICATION_CREDENTIALS)。

问题出在 GAE 上,部署时,对 with_subject 的调用会引发: AttributeError: 'Credentials' object has no attribute 'with_subject'

我已经在 GAE 服务帐户上启用了全域委派。

我在本地使用的 GOOGLE_APPLICATION_CREDENTIALS 和在 GAE 中使用的 GOOGLE_APPLICATION_CREDENTIALS 两者都是具有全域委派的服务帐户有何不同?

GAE 上的 .with_subject() 在哪里?

收到的creds对象是compute_engine.credentials.Credentials类型。

完整追溯:

Traceback (most recent call last):
  File "/env/lib/python3.7/site-packages/gunicorn/arbiter.py", line 583, in spawn_worker
    worker.init_process()
  File "/env/lib/python3.7/site-packages/gunicorn/workers/gthread.py", line 104, in init_process
    super(ThreadWorker, self).init_process()
  File "/env/lib/python3.7/site-packages/gunicorn/workers/base.py", line 129, in init_process
    self.load_wsgi()
  File "/env/lib/python3.7/site-packages/gunicorn/workers/base.py", line 138, in load_wsgi
    self.wsgi = self.app.wsgi()
  File "/env/lib/python3.7/site-packages/gunicorn/app/base.py", line 67, in wsgi
    self.callable = self.load()
  File "/env/lib/python3.7/site-packages/gunicorn/app/wsgiapp.py", line 52, in load
    return self.load_wsgiapp()
  File "/env/lib/python3.7/site-packages/gunicorn/app/wsgiapp.py", line 41, in load_wsgiapp
    return util.import_app(self.app_uri)
  File "/env/lib/python3.7/site-packages/gunicorn/util.py", line 350, in import_app
    __import__(module)
  File "/srv/main.py", line 1, in <module>
    from config.wsgi import application
  File "/srv/config/wsgi.py", line 38, in <module>
    call_command('gsuite_sync_users')
  File "/env/lib/python3.7/site-packages/django/core/management/__init__.py", line 148, in call_command
    return command.execute(*args, **defaults)
  File "/env/lib/python3.7/site-packages/django/core/management/base.py", line 353, in execute
    output = self.handle(*args, **options)
  File "/srv/metanube_i4/users/management/commands/gsuite_sync_users.py", line 14, in handle
    gsuite_sync_users()
  File "/env/lib/python3.7/site-packages/celery/local.py", line 191, in __call__
    return self._get_current_object()(*a, **kw)
  File "/env/lib/python3.7/site-packages/celery/app/task.py", line 375, in __call__
    return self.run(*args, **kwargs)
  File "/srv/metanube_i4/users/tasks.py", line 22, in gsuite_sync_users
    creds = creds.with_subject(settings.GSUITE_ADMIN_USER)
AttributeError: 'Credentials' object has no attribute 'with_subject'"  

包(部分列表):

google-api-core==1.5.0
google-api-python-client==1.7.4
google-auth==1.5.1
google-auth-httplib2==0.0.3
google-cloud-bigquery==1.6.0
google-cloud-core==0.28.1
google-cloud-logging==1.8.0
google-cloud-storage==1.13.0
google-resumable-media==0.3.1
googleapis-common-protos==1.5.3
httplib2==0.11.3
oauthlib==2.1.0

@marc.fargas 你可以看看 GitHub 上的 googleapis/google-auth-library-python library。您会找到一些与相关方法相关的信息:

凭据被认为是不可变的。如果你想修改范围 或用于委托的主题,使用 :meth:with_scopes 或 :meth:with_subject:: scoped_credentials = credentials.with_scopes(['email']) delegated_credentials = credentials.with_subject(主题)

当您使用 "GOOGLE_APPLICATION_CREDENTIALS" 定义您的应用程序默认凭据时,您将获得一个 google.auth.service_account.Credentials 的实例,它具有 with_subject 方法。

在 App Engine 上,您得到的是 app_engine.Credentials 的实例,它没有 with_subject 方法。这解释了观察到的行为和您看到的错误。

根据关于全域委派的 documentation,只有服务帐户凭据可以进行全域委派。

确实不能将 with_subject 方法与 GAE 或 GCE 凭据一起使用。但是,有一个解决方法可以让我在我的 GCE 服务器上工作,我认为这也适用于 GAE 默认服务帐户。解决方案是使用具有所需 subjectscopes 的服务帐户身份来构建新凭据。可以找到详细的指南 here,但我也会在下面解释该过程。

首先,服务帐户需要为自己创建服务帐户令牌的权限。这可以通过转到项目 IAM and admin > Service accounts 页面来完成(确保信息面板可见,它可以从右上角切换)。通过勾选复选框复制服务帐户电子邮件地址和 select 有问题的服务帐户。现在信息面板应该有 ADD MEMBER 按钮。单击它并将服务帐户电子邮件粘贴到 New members 文本框。单击 Select role 下拉菜单并选择角色 Service Accounts -> Service Account Token Creator。您可以使用以下 gcloud 命令检查角色是否已分配:

gcloud iam service-accounts get-iam-policy [SERVICE_ACCOUNT_EMAIL]

现在进入实际的 Python 代码。此示例是对上面链接的文档的轻微修改。

from googleapiclient.discovery import build
from google.auth import default, iam
from google.auth.transport import requests
from google.oauth2 import service_account

TOKEN_URI = 'https://accounts.google.com/o/oauth2/token'
SCOPES = ['https://www.googleapis.com/auth/admin.directory.user']
GSUITE_ADMIN_USER = 'admin@example.com'

def delegated_credentials(credentials, subject, scopes):
    try:
        # If we are using service account credentials from json file
        # this will work
        updated_credentials = credentials.with_subject(subject).with_scopes(scopes)
    except AttributeError:
        # This exception is raised if we are using GCE default credentials

        request = requests.Request()

        # Refresh the default credentials. This ensures that the information
        # about this account, notably the email, is populated.
        credentials.refresh(request)

        # Create an IAM signer using the default credentials.
        signer = iam.Signer(
            request,
            credentials,
            credentials.service_account_email
        )

        # Create OAuth 2.0 Service Account credentials using the IAM-based
        # signer and the bootstrap_credential's service account email.
        updated_credentials = service_account.Credentials(
            signer,
            credentials.service_account_email,
            TOKEN_URI,
            scopes=scopes,
            subject=subject
        )
    except Exception:
        raise

    return updated_credentials


creds, project = default()
creds = delegated_credentials(creds, GSUITE_ADMIN_USER, SCOPES) 

service = build('admin', 'directory_v1', credentials=creds)

如果您使用服务帐户文件的路径设置了 GOOGLE_APPLICATION_CREDENTIALS 环境变量,try 块将不会失败。如果应用程序是 运行 在 Google Cloud 上,则会有一个 AttributeError 并通过创建具有正确 subjectscopes 的新凭据来处理。

您还可以将 None 作为 subject 传递给 delegated_credentials 函数,它会在没有委托的情况下创建凭据,因此可以在有或没有委托的情况下使用此函数。

我在类似的情况下试图通过 GAE v2 中的 Cron 作业 运行 链接到 Google Sheet 的联合 BQ table 上的查询。 GAE 中的默认服务帐户无法使用所需的 Google 驱动器范围。 vkopio's answer is great and I ended up using it as well because it looks cleaner, but here's another solution which doesn't require the Service Account Token Creator role to be assigned to the service account. I put it together while combing through the documentation for Cloud Functions(使用类似于 GAE 的底层计算架构)使用 Rest API.

import requests

METADATA_URL = 'http://metadata.google.internal/computeMetadata/v1'
METADATA_HEADERS = {'Metadata-Flavor': 'Google'}
SERVICE_ACCOUNT = 'default'
SCOPES=['https://www.googleapis.com/auth/admin.directory.user']


def get_access_token(scopes):
    """
    Retrieves an access_token in App Engine for the default service account

    :param scopes: List of Google scopes as strings
    :return: access token as string
    """
    scopes_str = ','.join(scopes)
    url = f'{METADATA_URL}/instance/service-accounts/{SERVICE_ACCOUNT}/token?scopes={scopes_str}'
    # Request an access token from the metadata server.
    r = requests.get(url, headers=METADATA_HEADERS)
    r.raise_for_status()
    # Extract the access token from the response.
    access_token = r.json()['access_token']
    return access_token

我能够在我的 header 中使用这个 access_token 来满足我的请求

headers = {'Authorization': f'Bearer {access_token}'}

r = requests.post(url, json=job_body, headers=headers)

其中 url 指向我想使用 job_body 中的适当配置调用的特定 Rest 端点。请注意,这在 App Engine 环境之外不起作用。

oauth2client 中有一种使用 AccessTokenCredentials 创建凭据的方法,但现在 Google 已弃用该方法,因此此方法需要直接使用 Rest 端点。发布此答案,以便对可能不想向服务帐户添加任何其他角色的其他人有所帮助。