获取服务帐户凭据 Apps 脚本 - TypeError:无法从 null 读取属性

Posted

技术标签:

【中文标题】获取服务帐户凭据 Apps 脚本 - TypeError:无法从 null 读取属性【英文标题】:Getting service account credentials Apps Script - TypeError: Cannot read property from null 【发布时间】:2020-09-14 02:16:03 【问题描述】:

我正在使用数据洞察高级服务为数据洞察开发一个社区连接器,该连接器直接链接到 BigQuery。但是,我无法检索服务帐户凭据。

我已经将服务帐户密钥的整个 json 文件复制并粘贴为SERVICE_ACCOUNT_CREDS var 中的字符串,以及SERVICE_ACCOUNT_KEY var 中 json 中“private_key”中的整个字符串,以及作为服务帐户电子邮件和计费项目 ID 作为字符串,分别在变量 SERVICE_ACCOUNT_EMAILBILLING_PROJECT_ID 中。当我运行 getData() 函数时尝试进行身份验证时失败。

代码(是世界银行高级服务示例,但带有我的凭据):

var cc = DataStudioApp.createCommunityConnector();
var scriptProperties = PropertiesService.getScriptProperties();

function isAdminUser() 
  return false;


function getAuthType() 
  return cc
    .newAuthTypeResponse()
    .setAuthType(cc.AuthType.NONE)
    .build();


function getConfig(request) 
  var config = cc.getConfig();

  config
    .newInfo()
    .setId('info')
    .setText(
      'No configuration is required for this connector. Click connect to create a new data source.'
    );

  return config.build();


function getFields() 
  var fields = cc.getFields();
  var types = cc.FieldType;
  var aggregations = cc.AggregationType;

  fields
    .newDimension()
    .setId('country_name')
    .setName('Country')
    .setType(types.TEXT);

  fields
    .newDimension()
    .setId('country_code')
    .setName('Country Code')
    .setType(types.TEXT);

  fields
    .newDimension()
    .setId('indicator_name')
    .setName('Indicator')
    .setType(types.TEXT);

  fields
    .newDimension()
    .setId('year')
    .setName('Year')
    .setType(types.YEAR);

  fields
    .newMetric()
    .setId('value')
    .setName('Value')
    .setType(types.NUMBER)
    .setIsReaggregatable(true)
    .setAggregation(aggregations.SUM);

  return fields;


function getSchema(request) 
  return 
    schema: getFields().build()
  ;


var SERVICE_ACCOUNT_CREDS = ''+
  '"type": "service_account",'+
  '"project_id": "string for the project id",'+
  '"private_key_id": "string for the private key id",'+
  '"private_key": "-----BEGIN PRIVATE KEY-----the private key-----END PRIVATE KEY-----\n",'+
  '"client_email": "serviceaccount@projectid.iam.gserviceaccount.com",'+
  '"client_id": "the client id",'+
  '"auth_uri": "https://accounts.google.com/o/oauth2/auth",'+
  '"token_uri": "https://oauth2.googleapis.com/token",'+
  '"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",'+
  '"client_x509_cert_url": "url"'+
'';
var SERVICE_ACCOUNT_KEY = 'private key string';
var SERVICE_ACCOUNT_EMAIL = 'serviceaccount@projectid.iam.gserviceaccount.com';
var BILLING_PROJECT_ID = 'projectid';

/**
 * Copy the entire credentials JSON file from creating a service account in GCP.
 */
function getServiceAccountCreds() 
  return JSON.parse(scriptProperties.getProperty(SERVICE_ACCOUNT_CREDS));


function getOauthService() 
  var serviceAccountCreds = getServiceAccountCreds();
  var serviceAccountKey = serviceAccountCreds[SERVICE_ACCOUNT_KEY];
  var serviceAccountEmail = serviceAccountCreds[SERVICE_ACCOUNT_EMAIL];

  return OAuth2.createService('WorldBankHealthPopulation')
    .setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
    .setTokenUrl('https://accounts.google.com/o/oauth2/token')
    .setPrivateKey(serviceAccountKey)
    .setIssuer(serviceAccountEmail)
    .setPropertyStore(scriptProperties)
    .setCache(CacheService.getScriptCache())
    .setScope(['https://www.googleapis.com/auth/bigquery.readonly']);


var FIELDS_WHITELIST = [
  'country_name',
  'country_code',
  'indicator_name',
  'year',
  'value'
];
var BASE_SQL =
  'SELECT FIELDS FROM `bigquery-public-data.world_bank_health_population.health_nutrition_population`';

function makeSQL(request) 
  // Create an object of [fieldName]: boolean to use as a constant time lookup.
  var simpleSet = FIELDS_WHITELIST.reduce(function(obj, field) 
    obj[field] = true;
    return obj;
  , );
  var requestFieldNames = request.fields.map(function(field) 
    return field.name;
  );
  var fieldNames = FIELDS_WHITELIST.filter(function(fieldName) 
    return simpleSet[fieldName];
  );
  var fieldsSQL = fieldNames.join(', ');
  return BASE_SQL.replace('FIELDS', fieldsSQL);


function getData(request) 
  var accessToken = getOauthService().getAccessToken();
  var serviceAccountCreds = getServiceAccountCreds();
  var billingProjectId = serviceAccountCreds[BILLING_PROJECT_ID];
  var sql = makeSQL(request);

  return cc
    .newBigQueryConfig()
    .setAccessToken(accessToken)
    .setBillingProjectId(billingProjectId)
    .setUseStandardSql(true)
    .setQuery(sql)
    .build();

每当我运行getOauthService()getData() 时,我都会得到'Cannot read property'private key string' from null

非常感谢任何适合 5 岁儿童的帮助或教程。

【问题讨论】:

1.显示您的脚本隐藏敏感细节,但保持其格式。 2.链接您所指的文档 正如@TheMaster 提到的,如果您提供文档 URL 以获取有关您的问题的更多上下文,这将很有帮助。您可以评论或编辑您的帖子以添加文档吗? DataStudio 高级服务:developers.google.com/datastudio/connector/advanced-services World Bank Example github.com/googledatastudio/community-connectors/tree/master/… @JoseVasquez TheMaster 【参考方案1】:

回答

在您的getServiceAccountCreds 函数中,您使用对象scriptProperties.getProperty(SERVICE_ACCOUNT_CREDS) 检索字段,该对象是SERVICE_ACCOUNT_CREDS。根据Google Apps Script: Properties 示例,预期的唯一参数类型是String。因此,您应该避免解析 JSON 并使用在代码开头创建的 Properties 对象。

我建议您使用以下代码。

代码

替换你的代码

var SERVICE_ACCOUNT_CREDS = ''+
  '"type": "service_account",'+
  '"project_id": "string for the project id",'+
  '"private_key_id": "string for the private key id",'+
  '"private_key": "-----BEGIN PRIVATE KEY-----the private key-----END PRIVATE KEY-----\n",'+
  '"client_email": "serviceaccount@projectid.iam.gserviceaccount.com",'+
  '"client_id": "the client id",'+
  '"auth_uri": "https://accounts.google.com/o/oauth2/auth",'+
  '"token_uri": "https://oauth2.googleapis.com/token",'+
  '"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",'+
  '"client_x509_cert_url": "url"'+
'';
var SERVICE_ACCOUNT_KEY = 'private key string';
var SERVICE_ACCOUNT_EMAIL = 'serviceaccount@projectid.iam.gserviceaccount.com';
var BILLING_PROJECT_ID = 'projectid';

/**
 * Copy the entire credentials JSON file from creating a service account in GCP.
 */
function getServiceAccountCreds() 
  return JSON.parse(scriptProperties.getProperty(SERVICE_ACCOUNT_CREDS));

对于这个代码

var SERVICE_ACCOUNT_CREDS_PROPS = 
    type: "service_account",
    project_id: "string for the project id",
    private_key_id: "string for the private key id",
    private_key: "-----BEGIN PRIVATE KEY-----the private key-----END PRIVATE KEY-----\n",
    client_email: "serviceaccount@projectid.iam.gserviceaccount.com",
    client_id: "the client id",
    auth_uri: "https://accounts.google.com/o/oauth2/auth",
    token_uri: "https://oauth2.googleapis.com/token",
    auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs",
    client_x509_cert_url: "url"
  ;
scriptProperties.setProperties(SERVICE_ACCOUNT_CREDS_PROPS);

var SERVICE_ACCOUNT_KEY = 'private_key';
var SERVICE_ACCOUNT_EMAIL = 'client_email';
var BILLING_PROJECT_ID = 'project_id';

/**
 * Copy the entire credentials JSON file from creating a service account in GCP.
 */
function getServiceAccountCreds() 
  return scriptProperties.getProperties();

参考

Google Apps Script: PropertiesService

Google Apps Script: Properties

【讨论】:

工作就像一个魅力。谢谢!【参考方案2】:

实际上,尽管连接器代码在 @Jose Vasquez 建议后在应用程序脚本中运行,但在 Google Data Studio 上使用它时返回错误 Insufficient permissions to the underlying data set.。在进一步评估本教程后,我错过了一个非常重要的点。凭证对应的json不应粘贴在代码本身中,而应在项目属性中引用:

然后在脚本属性选项卡上,作为键值对,键为SERVICE_ACCOUNT_CREDS,值为json服务帐户键的内容:

那么,代码替换就变成了:

var SERVICE_ACCOUNT_CREDS = ''+
  '"type": "service_account",'+
  '"project_id": "string for the project id",'+
  '"private_key_id": "string for the private key id",'+
  '"private_key": "-----BEGIN PRIVATE KEY-----the private key-----END PRIVATE KEY-----\n",'+
  '"client_email": "serviceaccount@projectid.iam.gserviceaccount.com",'+
  '"client_id": "the client id",'+
  '"auth_uri": "https://accounts.google.com/o/oauth2/auth",'+
  '"token_uri": "https://oauth2.googleapis.com/token",'+
  '"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",'+
  '"client_x509_cert_url": "url"'+
'';
var SERVICE_ACCOUNT_KEY = 'private key string';
var SERVICE_ACCOUNT_EMAIL = 'serviceaccount@projectid.iam.gserviceaccount.com';
var BILLING_PROJECT_ID = 'string for the project id';

/**
 * Copy the entire credentials JSON file from creating a service account in GCP.
 */
function getServiceAccountCreds() 
  return JSON.parse(scriptProperties.getProperty(SERVICE_ACCOUNT_CREDS));

简单地说:

var SERVICE_ACCOUNT_CREDS = 'SERVICE_ACCOUNT_CREDS';
var SERVICE_ACCOUNT_KEY = 'private_key';
var SERVICE_ACCOUNT_EMAIL = 'client_email';
var BILLING_PROJECT_ID = 'project_id';

/**
 * Copy the entire credentials JSON file from creating a service account in GCP.
 */
function getServiceAccountCreds() 
  return JSON.parse(scriptProperties.getProperty(SERVICE_ACCOUNT_CREDS));


由于服务帐户凭据已在脚本属性中设置

【讨论】:

以上是关于获取服务帐户凭据 Apps 脚本 - TypeError:无法从 null 读取属性的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Google Apps 脚本中使用服务帐户对 Google 表格进行身份验证

在不提示的情况下在 Powershell 中获取当前用户的凭据对象

服务帐户可以调用 Google Apps Script Execution API 吗?

使用 firebase-admin。服务帐户凭据有啥用途?

为自动电子邮件脚本存储帐户凭据(尤其是密码)的最佳方式是啥?

与服务帐户电子邮件共享 Google 表格