GAE AttributeError:“凭据”对象没有属性“ with_subject”

时间:2018-11-08 06:55:40

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

我有一个要部署在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

3 个答案:

答案 0 :(得分:2)

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

首先,服务帐户需要权限才能为其自己创建服务帐户令牌。这可以通过转到项目IAM and admin > Service accounts页面来完成(确保信息面板可见,可以从右上角进行切换)。复制服务帐户的电子邮件地址,并通过选中复选框来选择有问题的服务帐户。现在,信息面板应具有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)

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

您还可以将None作为subject函数的delegated_credentials传递,它会创建不带委派的凭据,因此该功能可以带或不带委派使用。

答案 1 :(得分:0)

@ marc.fargas您可以在GitHub上查看googleapis / google-auth-library-python library。您会发现与该方法有关的一些信息:

凭据被认为是不可变的。如果要修改范围     或用于委派的主题,请使用:meth:if( function_exists('acf_add_options_page') ) { acf_add_options_page(array( 'page_title' => 'Theme General Settings', 'menu_title' => 'Theme Settings', 'menu_slug' => 'theme-general-settings', 'capability' => 'edit_posts', 'redirect' => false )); acf_add_options_sub_page(array( 'page_title' => 'Theme Header Settings', 'menu_title' => 'Header', 'parent_slug' => 'theme-general-settings', )); acf_add_options_sub_page(array( 'page_title' => 'Theme Footer Settings', 'menu_title' => 'Footer', 'parent_slug' => 'theme-general-settings', )); } 或     :meth:add_action( 'admin_menu', 'main_home' ); /** * Adds a submenu page under a custom post type parent. */ function main_home() { add_submenu_page( 'theme-general-settings', __( 'Books Shortcode Reference', 'textdomain' ), __( 'Shortcode Reference', 'textdomain' ), 'manage_options', 'books-shortcode-ref', 'books_ref_page_callback' ); } /** * Display callback for the submenu page. */ function books_ref_page_callback() { ?> <div class="wrap"> <h1><?php _e( 'Books Shortcode Reference', 'textdomain' ); ?></h1> <p><?php _e( 'Helpful stuff here', 'textdomain' ); ?></p> </div> <?php } ::         scoped_credentials =凭据.with_scopes(['email'])         proxyed_credentials =凭据.with_subject(主题)

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

在App Engine上时,您将获得app_engine.Credentials的实例,该实例不具有with_subject方法。这说明了观察到的行为以及您看到的错误。

根据documentation关于域范围内的委派,只有服务帐户凭据可以具有域范围内的委派。

答案 2 :(得分:0)

我处于类似情况下,试图通过GAE v2中的Cron作业对链接到Google表格的联合BQ表运行查询。 GAE中的默认服务帐户无法使用必需的Google云端硬盘范围。 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环境。

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