【问题标题】:GAE AttributeError: 'Credentials' object has no attribute 'with_subject'GAE AttributeError:“凭据”对象没有属性“with_subject”
【发布时间】:2019-04-11 16:02:00
【问题描述】:

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

我在本地做:

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)

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

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

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

当我在本地使用的 GOOGLE_APPLICATION_CREDENTIALS 和 GAE 中的两者都是具有域范围委派的服务帐户时,有什么不同?

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

【问题讨论】:

  • 你能显示完整的回溯吗?
  • 按要求添加回溯;请注意,我的代码基本上是对稍后调用的 gsuite_sync_users() 的调用。 (Celery 现已禁用)

标签: python google-app-engine google-authentication google-api-python-client


【解决方案1】:

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

首先,服务帐户需要权限才能为自己创建服务帐户令牌。这可以通过转到项目IAM and admin &gt; Service accounts 页面来完成(确保信息面板可见,可以从右上角切换)。复制服务帐户电子邮件地址并通过勾选复选框来选择相关服务帐户。现在信息面板应该有ADD MEMBER 按钮。单击它并将服务帐户电子邮件粘贴到 New members 文本框。单击Select role 下拉列表并选择角色Service Accounts -&gt; 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 函数,它会在没有委托的情况下创建凭据,因此该函数可以在有或没有委托的情况下使用。

【讨论】:

  • 我目前没有测试环境。但该解决方案似乎解决了我原来的问题,所以我将其标记为答案。
  • 它对我有用,但是,我注意到当我在部署为 CloudFunction 时运行它时,它会在正常工作之前在日志中生成 20 行错误。如果没有就好了……
【解决方案2】:

@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(subject)

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

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

根据域范围委派的documentation,只有服务帐户凭据才能进行域范围委派。

【讨论】:

  • 链接文件是在使用 ADC(应用程序默认凭据)时不需要的密钥文件时。使用密钥文件,它会像您使用与 GAE 默认帐户不同的另一个服务帐户一样工作。
  • .with_subject 是我正在尝试使用的。问题是通过 ADC 传递到 GAE 环境的凭据由于某种原因不具备这种能力。问题出在 GAE 凭证/ADC 中,因为在使用 $GOOGLE_APPLICATION_CREDENTIALS 时,完全相同的代码在 GAE 之外工作。我真的希望不必使用密钥文件,因为这需要我以某种方式将其推送到 GAE,无论是在代码中(BAD)还是使用 KMS(太多而太少?)
【解决方案3】:

我遇到了类似的情况,试图通过 GAE v2 中的 Cron 作业对链接到 Google Sheet 的联合 BQ 表运行查询。 GAE 中的默认服务帐户无法使用所需的 Google Drive 范围。 vkopio's answer 很棒,我最终也使用了它,因为它看起来更干净,但这是另一个不需要将 Service Account Token Creator 角色分配给服务帐户的解决方案。我在使用 Rest API 梳理documentation for Cloud Functions(使用类似于 GAE 的底层计算架构)时将它们放在一起。

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

我可以在我的请求头中使用这个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 端点。发布此答案,以便对可能不想向服务帐户添加任何其他角色的其他人有所帮助。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-12-01
    • 2021-04-19
    • 2021-11-22
    • 1970-01-01
    • 1970-01-01
    • 2018-08-28
    • 1970-01-01
    • 2020-12-11
    相关资源
    最近更新 更多