我有一个要部署在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
答案 0 :(得分:2)
确实不能将with_subject
方法与GAE或GCE凭据一起使用。但是,有一种解决方法可以在我的GCE服务器上工作,并且我认为这也适用于GAE默认服务帐户。解决方案是使用带有所需subject
和scopes
的服务帐户标识来构建新凭据。可以找到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
,并通过创建具有正确的subject
和scopes
的新凭据来对其进行处理。
您还可以将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端点。发布此答案,以便对可能不想向服务帐户添加任何其他角色的其他人有用。