2

I have a Powershell script that calls a Google App Script function.

When I run the Powershell script I can see the following error on the Error Reporting on my GCP project:

Exception: You do not have permission to call SpreadsheetApp.getActiveSpreadsheet. Required permissions: (https://www.googleapis.com/auth/spreadsheets.currentonly || https://www.googleapis.com/auth/spreadsheets)
at toSpreadsheet (Código:3)
at fromPS (Código:14)

I have understood that I have to authorize the scope, so I've been trying to do that by editing the manifest file.

The Authorization Scopes Documentation says,

"During the authorization flow, Apps Script presents human-readable descriptions of the required scopes to the user. For example, if your script needs read-only access to your spreadsheets, the manifest may have the scope https://www.googleapis.com/auth/spreadsheets.readonly. During the authorization flow, a script with this scope asks the user to allow this application to "View your Google Spreadsheets"."

In my case I edited the manifest file appscript.json to add the scope https://www.googleapis.com/auth/spreadsheets, then I saved it, published the Google App Script project as API Executable, and finally I run the Powershell code again, but I still get the same error as above. During all this flow, I was not asked to allow anything. I cannot understand what is missing authorizing the script have the required permission.

I also added the spreadsheets scope to OAuth consent screen, but it seems to do not make any difference. I am suspecting I should use a Service Account to accomplish that since I see no way to go through a OAuth Client Verification since my script on Google is called from Powershell script. I dont want to believe on that because getting to know how config OAuth2 took me a lot of time :(

A few considerations:

  1. The function that the run method calls by Powershell just works fine when I run it directly from Google Script Editor.

  2. The script project is deployd as an API executable

  3. Google Apps Script API is enabled in the GCP project

  4. It is associated to a Standard GCP project

  5. The OAuth credential is Web Application type

  6. The script for writing and reading values from Powershell to Google Sheets works fine

Google script:

function toSpreadsheet(text2write)
  { 
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("HIL_APP");
  var LastRow = sheet.getLastRow();

  for (var i = 1; i < LastRow; i++)
  {
    sheet.getRange(i+1, 8, 1).setValue(text2write)
  }
  return "myreturn"
}

function fromPS(params)
{
  Logger.log(params) 
  var rtn = toSpreadsheet(params)
  return rtn
}

manifest file:

{
  "oauthScopes": [
    "https://www.googleapis.com/auth/spreadsheets"
  ],  
  "timeZone": "America/Argentina/Buenos_Aires",
  "dependencies": {
  },
  "webapp": {
    "access": "ANYONE",
    "executeAs": "USER_DEPLOYING"
  },
  "exceptionLogging": "STACKDRIVER",
  "executionApi": {
    "access": "MYSELF"
  },
  "runtimeVersion": "V8"
}

Powershell code:

function doit{
    $json = ".\client_id.json"
    $jdata = get-content $json | convertfrom-json
    <#
    $jdata | ForEach-Object {
        $_.PSObject.Properties.Value
    }
    #>
    $ClientID = $jdata.web.client_id.ToString()
    $ClientSecret = $jdata.web.client_secret.ToString()
    $refreshToken = "1//04VvG_FTyDGhiCgYIARAAGAQSNwF-L9IrZ-o1kaZQQccvzL5m4TUTNz6b9Q4KCb16t4cH11gGCshWZWvgaCoMlg73FgpLAGOYTEk" 
    $grantType = "refresh_token" 
    $requestUri = "https://accounts.google.com/o/oauth2/token" 
    $GAuthBody = "refresh_token=$refreshToken&client_id=$ClientID&client_secret=$ClientSecret&grant_type=$grantType" 
    $GAuthResponse = Invoke-RestMethod -Method Post -Uri $requestUri -ContentType "application/x-www-form-urlencoded" -Body $GAuthBody


    $accessToken = $GAuthResponse.access_token

    $headers = @{"Authorization" = "Bearer $accessToken"          

                  "Content-type" = "application/json"}

    $spreadsheetId = "1htbeGlqZ4hojQBWl9fxE4nW_KZI9uVwi0ApzNOIbwnY"

    $currentDate = (Get-Date).ToString('MM/dd/yyyy')
    $currentTime = (Get-Date).ToString('HH:mm:sstt')

$json = @”
{
    "range": "HIL_APP!A1:G1",
    "majorDimension": "ROWS",
    "values":
                [[
                    "HIL_NAME",
                    "$env:ComputerName",
                    "$currentDate",
                    "$currentTime",
                    "$env:UserName",
                    "input from user",
                    "attempt"
                ],]
}
“@

    $write = Invoke-WebRequest -Uri "https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/HIL_APP!A1:G1:append?valueInputOption=USER_ENTERED" -Method Post -ContentType "application/json" -Body $json  -Headers @{"Authorization"="Bearer $accessToken"}
    $read = Invoke-WebRequest -Uri "https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/HIL_APP!A1:G1" -Headers @{"Authorization"="Bearer $accessToken"}
    $read
    Write-Output "read: " ($read.Content | ConvertFrom-Json)

    $scriptId = "1eF7ZaHH-pw2-AjnRVhOgnDxBUpfr0wALk1dVFg7B220bg_KuwVudbALh"          

$json = @"
{
  "function": "fromPS",
  "parameters": ["myparam"],
  "devMode": true
}
"@

$resp = Invoke-WebRequest -Uri "https://script.googleapis.com/v1/scripts/${scriptId}:run" -Method Post -ContentType "application/json" -Body $json -Headers @{"Authorization"="Bearer $accessToken"}
$resp 
Write-Output "script response: " ($resp.Content | ConvertFrom-Json)
}

$error.Clear()

clear

doit
TheMaster
  • 45,448
  • 6
  • 62
  • 85
Cleber Marques
  • 426
  • 5
  • 20
  • Ensure [all four requirements](https://developers.google.com/apps-script/api/how-tos/execute#requirements) are satisfied – TheMaster Apr 05 '20 at 21:43
  • In order to correctly understand about your situation, can you provide the Google Apps Script for replicating your issue? – Tanaike Apr 05 '20 at 21:46
  • @TheMaster, I run from Script Editor it works fine. On the link you shared I dont fully undestand the last requirement "Ensure that the script *and the calling application* share a common Cloud Platform (GCP) project." The calling application on that case is Powershell, right? Once they use the same ClientID then this requirement is satisfied. Am I right? – Cleber Marques Apr 05 '20 at 22:07
  • @Tanaike, I edited my question to share the google script, manifest and powershell code. I also listed 6 considerations – Cleber Marques Apr 05 '20 at 22:09
  • Thank you for replying and updating it. At first, can I ask you whether you could confirm that your settings of Apps Script API worked using a simple sample script in [your previous question](https://stackoverflow.com/q/60942605/7108653)? Because if you cannot still use Apps Script API, I think that at first, that issue is required to be resolved. – Tanaike Apr 05 '20 at 22:12
  • 1
    @Tanaike, I have confirmed the setting for App Script API works fine by modifying my google script to return a single string and the powershell was able to read this string as response from google script. – Cleber Marques Apr 05 '20 at 22:26
  • Thank you for replying. I could understand that your settings of Apps Script API is correct. About your this question, in my environment, I could confirm that your script worked using Apps Script API. So unfortunately, I cannot replicate your situation. So although I'm not sure whether this is the direct solution, I thought that your refresh token might be required to be updated using new scope of `https://www.googleapis.com/auth/spreadsheets`. How about this? – Tanaike Apr 05 '20 at 22:29
  • For example, as a simple method for using the access token for running the function in your GAS project, how about putting `const getOauth = () => console.log(ScriptApp.getOAuthToken());` in your GAS project and run it with the script editor? By this, the access token for running the GAS project can be directly retrieved. And please use the retrieved access token at `$accessToken = "###"` in your script of powershell. I think that as a test run, this method can be also used. – Tanaike Apr 05 '20 at 22:41
  • @Tanaike, Interesting stuff happened 1) I added the scope from OAuth 2.0 Playground; 2) selected "Exchange authorization code for tokens"; 3) checked "Auto-refresh the token before it expires"; 4) Copied Refresh token; 5) added the line const getOauth = () => console.log(ScriptApp.getOAuthToken()); to GAS; 6) Ran GAS from Editor; 7) I got `null` in the Logs Viewer. It should return the new Refresh token, right? 8) I updated the Powershell with new Refresh token from step 4 and ran it, and I got 401 (Unauthorized) and 403 (Forbidden to access) errors. GAS doesnt recognize the refleshed token – Cleber Marques Apr 06 '20 at 01:08
  • 1
    Thank you for replying. If the settings is completed, when `ScriptApp.getOAuthToken()` is run, an access token is returned. From your replying, although I'm not sure about your actual situation, I could understand that the access token is not returned from it. In this case, when the simple script which returns `ok` is run, your script of powershell works? – Tanaike Apr 06 '20 at 02:19
  • @Clebar Partly true.You should also ensure that the oauth credentials are from the same project. Also, what do you mean by *The script for writing and reading values from Powershell to Google Sheets works fine*? No problem now? – TheMaster Apr 06 '20 at 05:57
  • @Tanaike, thank you so much for replying! And my apologies. I tried once again and it worked! I am not sure why the code `const getOauth = () => console.log(ScriptApp.getOAuthToken());` did not work before, but I replaced it only for `console.log(ScriptApp.getOAuthToken());` and I could get a refreshed token from this, then I updated the Powershell script with this token as you suggested before. After all that now everything works fine. May I ask you what if this token expire? Do you have any suggestion how to automatically get an up to date token? Thank you so much for your so esteemed help. – Cleber Marques Apr 06 '20 at 06:46
  • @TheMaster, thank you so much for replying. I meant it works only for writing and reading values for Google Sheets, but the issue was for triggering a GAS function using the method run of Google App Script API. I wanted to say in other words that the authentication was apparently ok. – Cleber Marques Apr 06 '20 at 06:47
  • Thank you for replying. The method for using `ScriptApp.getOAuthToken()` is used for just testing whether the function of the GAS project can be run by Apps Script API. So, when you want to refresh the access token, please retrieve the refresh token with the scopes. In your initial script of powershell, you are using the refresh token. Although I'm not sure how did you get it, when you retrieve the refresh token and retrieve the access token using the refresh token, you can use your script by refreshing the access token. If this was not the direction you want, I apologize. – Tanaike Apr 06 '20 at 07:45
  • @Tanaike, Thanks. Dont want you to post the solution for the main post? then I mark it as response. I resolved it by adding the scope and then doing what you suggested adding the line ScriptApp.getOAuthToken() on the GAS to check if it returns the same token declared in Powershell script. In my case it was not equal, then I updated the PS code. I just wanted to mark you answer as the solution just in case of other user have the same issue. – Cleber Marques Apr 06 '20 at 18:20
  • Thank you for replying. I'm glad your issue was resolved. And I agree with your proposal. I also think that it is required to do a bit complicated settings for using the script.run method of Apps Script API. When a test flow is proposed, that might be useful for users. So I posted it as an answer. Could you please confirm it? – Tanaike Apr 06 '20 at 22:20
  • Yes, thank you again! – Cleber Marques Apr 06 '20 at 22:41

1 Answers1

4

In order to run the function of Google Apps Script (GAS) using Apps Script API, it is required to do a bit complicated settings. In this case, I would like to propose for testing to run the GAS function as follows. This flow might be too careful.

Flow:

  1. Link the Cloud Platform Project to Google Apps Script Project. Ref
  2. Install for running the GAS function with the scripts.run method in Apps Script API. Ref
  3. Put the script you want to run to the script editor of Google Apps Script.
    • Here, please run the function by the script editor and confirm whether the script works. By this, the issue of the script can be avoided.
  4. Put the following script for testing to run. This is used for 1st test of Apps Script API.

    function test() {
      return "ok";
    }
    
  5. Put a following sample script for retrieving the access token. This is used for testing it. Please run this at the script editor, and copy the returned access token.

    function getToken() {
      Logger.log(ScriptApp.getOAuthToken());
    }
    
  6. Test to run the GAS function of test() using the retrieved access token. In this case, the script of powershell is used by replacing $accessToken = $GAuthResponse.access_token.

    • When an error occurs, please confirm the settings of Apps Script API. In this case, it can be said that the GAS script is correct.
    • When no error occurs, please test to run the function you want to run. In this case, the required scopes have already been included in the access token. By this, the issue of scopes can be avoided.
  7. When above test is done and your script for using Apps Script API works, please retrieve the refresh token using the scopes. The scopes can be seen at the script editor. By this, the valid access token can be retrieved by the refresh token. And your script can be used at the local PC.

References:

Tanaike
  • 181,128
  • 11
  • 97
  • 165
  • I posted a similar answer to [this question](https://stackoverflow.com/questions/51616556/q-google-photos-library-api-i-dont-know-how-it-works-someone/51957412#51957412) although my project assumes it runs locally, not in Google Apps Script. I'd be interested in your feedback. – Rich Moss May 17 '21 at 18:04
  • @Rich Moss Thank you for your comment. If this was useful, I'm glad. – Tanaike May 18 '21 at 00:12