Unable to use sub account with Google Service Account - google-drive-api

I needed to create a script that uploads the resulting screen shots to google drive.
I was hoping I could just auth in as my google user, but that seems... harder? So I abandoned that tact. Next I moved onto service accounts. This works fine (now) for my service account, but when I attempt to specify a user ($auth->sub) I get "Unauthorized client or scope in request.".
function buildService($userEmail) {
$DRIVE_SCOPE = 'https://www.googleapis.com/auth/drive';
$SERVICE_ACCOUNT_EMAIL = 'notsupplied#developer.gserviceaccount.com';
$SERVICE_ACCOUNT_PKCS12_FILE_PATH = 'pathtofile.p12';
$key = file_get_contents($SERVICE_ACCOUNT_PKCS12_FILE_PATH);
$auth = new Google_Auth_AssertionCredentials(
$SERVICE_ACCOUNT_EMAIL,
array($DRIVE_SCOPE),
$key);
$auth->sub = 'myuser#gmail.com';
$client = new Google_Client();
$client->setAssertionCredentials($auth);
return new Google_Service_Drive($client);
}
I'd love to abandon the service account and just auth with my regular google user if thats just as easy. Or solve how (in the api settings maybe?) I can ensure myuser#gmail.com can be used.

Refresh_token is the key here. In a webbrowser use this link to approve your google user:
https://accounts.google.com/AccountChooser?service=lso&continue=https%3A%2F%2Faccounts.google.com%2Fo%2Foauth2%2Fauth%3Fresponse_type%3Dcode%26scope%3Dhttps%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive%26redirect_uri%3Dhttps%3A%2F%2Fwww.example.com%2Foauth2callback%26access_type%3Doffline%26client_id%3D<CLIENT_ID>%26hl%3Den%26from_login%3D1%26as%3D34eac985232ba748&btmpl=authsub&hl=en&approval_prompt=force&access_type=offline
which will return a URL like https://www.example.com/oauth2callback?code=
Then post code=&client_id=&client_secret=&redirect_uri=&grant_type=authorization_code to https://accounts.google.com/o/oauth2/token
This will return a "refresh_token" parameter. Save this. Very important. If you don't get one you have to go to https://security.google.com/settings/security/permissions to revoke permissions from your app.
After you get the refresh token you're good to go:
$client = new Google_Client();
$client->setClientId($client_id);
$client->setClientSecret($client_secret);
$client->setRedirectUri($redirect_uri);
$client->addScope("https://www.googleapis.com/auth/drive");
$client->setAccessType('offline');
$token = $client->refreshToken('<YOUR_REFRESH_TOKEN>');
$service = new Google_Service_Drive($client);

Related

Transferring ownership of the file using Google Drive API

I'm creating a spreadsheet file using google drive api, then pass ownership to another email, $automationEmail in this case. This is how I do it:
$permission = $this->insertPermission($service, $file->id, $automationEmail, 'user', 'owner');
And this is the insertPermission function
function insertPermission($service, $fileId, $value, $type, $role)
{
$newPermission = new Google_Service_Drive_Permission();
$newPermission->setEmailAddress($value);
$newPermission->setType($type);
$newPermission->setRole($role);
if ($role == 'owner') {
$permission = $service->permissions->create($fileId, $newPermission, array('fields' => 'id', 'transferOwnership' => 'true'));
} else {
$permission = $service->permissions->create($fileId, $newPermission);
}
if ($permission) {
return $permission;
}
return NULL;
}
Until now, this used to work perfectly fine. But recently it stopped working and gives me an error: Consent is required to transfer ownership of a file to another user.
For other permissions,like writer or reader, everything works fine. Did anything change in the API that I could not find? I searched for the solution, but seems like noone has had this issue before, or at least I could not find anything. Why did it stop working?
Issue:
Currently, when transfering file ownership between users who are not part of the same organization, the transfer requires the new owner to accept the invitation.
When the current owner (user #1) sets user #2 as the new owner, an invitation is sent to user #2, and user #1 remains the owner until user #2 accepts it:
You remain the file owner until the pending owner accepts your invitation.
Since this requires the new owner to accept the invitation, the transfer cannot be done programmatically via API, so this method fails for users who don't belong to the same organization.
From Issue Tracker https://issuetracker.google.com/issues/227973724#comment22 :
Following up here, this is the expected behavior as currently Drive does not support the changing of the ownership for items which are owned by gmail.com accounts.
Issue Tracker feature request:
API: Consent is required to transfer ownership of a file to another user
Reference:
Make someone else the owner of your file
Transferring ownership directly so someone did not work, so I used the following approach: In our google workspace we enabled Domain-wide delegation for the Google service account:
https://developers.google.com/admin-sdk/directory/v1/guides/delegation#delegate_domain-wide_authority_to_your_service_account
Then I created a dedicated user for Google drive files, logged in as this user to get the access token and saved it. After that I was able to create the new files on Google Drive as this dedicated user, so the files now belong to this email by default. This is how I did it:
$credentials = storage_path('credentials.json');
$token = [
'access_token' => config('app.flex_email_access_token')
];
$client = new Google_Client();
$client->setApplicationName('Sheets');
$client->setScopes([Google_Service_Sheets::DRIVE, Google_Service_Sheets::SPREADSHEETS]);
$client->setSubject(config('app.flex_email'));
$client->setAccessType('offline');
$client->setAuthConfig($credentials);
$client->setAccessToken($token);
$serviceDrive = new Google_Service_Drive($client);
$body = new Google_Service_Drive_DriveFile(array(
"name" => [$request->name],
'parents' => [$listing->pls_folder_id]
));
$spreadsheet = $serviceDrive->files->copy('yourspreadsheetid', $body);
In this example I copy the file, but the same approach works when creating them. Since the files now belong to the user I need by default, the ownership transfer issue has been avoided.

Google Drive API suddenly does not authenticate - '{ "error" : "invalid_grant" }'

hoping for some support on our Google Drive API. We have a script that creates Google Drive folders when new clients are added. This script worked great for over 6 months, and suddenly broke. The error we get is:
Error refreshing the OAuth2 token, message: '{ "error" : "invalid_grant" }'
I'd like to stress that nothing in our code changed. The tokens simply stopped authenticating for all users.
We are going mad trying to figure out why this would "randomly" happen one day. Any help is greatly appreciated.
try {
require '/var/www/html/s2/googledrive/vendor/autoload.php';
define('APPLICATION_NAME', 'Drive API Quickstart');
define('CREDENTIALS_PATH', '~/.credentials/dirrec.json');
define('CLIENT_SECRET_PATH', '/var/www/html/client_secret.json');
define('SCOPES', 'https://www.googleapis.com/auth/drive');
$client = new Google_Client();
$client->setApplicationName(APPLICATION_NAME);
$client->setScopes(SCOPES);
$client->setAuthConfigFile(CLIENT_SECRET_PATH);
$client->setAccessType('offline');
// Load previously authorized credentials from a file.
require_once('include/upload_file.php');
$file = new UploadFile();
//get the file location
$file->temp_file_location = 'custom/modules/Accounts/dirrec.json';
$accessToken = $file->get_file_contents();
$client->setAccessToken($accessToken);
$service = new Google_Service_Drive($client);
// Refresh the token if it's expired.
if ($client->isAccessTokenExpired()) {
$client->refreshToken($client->getRefreshToken());
file_put_contents('custom/modules/Accounts/dirrec.json', $client->getAccessToken());
}

Custom google app script doesn't work after copying spreadsheet with google java client

I have google spreadsheet template with custom add-on which I'm trying to copy
def copyTemplateSpreadsheet(Drive driveService) {
File templateCopy = new File()
templateCopy.setName("excel-template")
def copiedFile = driveService.files().copy(templateSpreadsheetId, templateCopy).execute()
setCorrectPermission(driveService, copiedFile.getId())
copiedFile
}
private void setCorrectPermission(Drive driveService, def fileId) {
Permission newPermission = new Permission();
newPermission.setType("anyone");
newPermission.setRole("writer");
driveService.permissions().create(fileId, newPermission).execute();
}
The problem is that copied spreadsheet has broken add-on (isn't displayed in add-ons menu). There is correct add-on code in script editor but when I try to run any function I get error message
"We're sorry, a server error occurred. Please wait a bit and try again"
Keep in mind that the very same code work well in my template spreadsheet. Even if I delete all the code and leave empty onOpen function the error still appears.
Copying add-ons works well when I do it using regular google drive website (drive.google.com) and also worked when I tried to use google's API Explorer (https://developers.google.com/drive/v3/reference/files/copy#try-it). The problem seems to only when using sdk (at least java one - I haven't tried any other)
Also keep in mind I'm using google service account created as described in this article https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount
and creating Drive instance with following code
Drive getDriveService() throws GeneralSecurityException, IOException, URISyntaxException {
HttpTransport httpTransport = new NetHttpTransport();
JacksonFactory jsonFactory = new JacksonFactory();
GoogleCredential credential = new GoogleCredential.Builder()
.setTransport(httpTransport)
.setJsonFactory(jsonFactory)
.setServiceAccountId(G_SERVICE_EMAIL)
.setServiceAccountScopes(Arrays.asList(DriveScopes.DRIVE))
.setServiceAccountPrivateKeyFromP12File(PKC_12_FILE)
.build();
Drive service = new Drive.Builder(httpTransport, jsonFactory, null)
.setHttpRequestInitializer(credential)
.build();
return service;
}
not sure if it matters though, especially since everything else seems to work just fine
Any fix ideas? I'm open to any workarounds as long as they work.
Also I would be fine with creating new file and just adding add-on code but it seems like I can't do it with API
I've found an acceptable solution which I described there
How can I create spreadsheet with included gs script by API?
Hope it helps someone ;)
Following the comment in How can I create spreadsheet with included gs script by API?, this is the app Scripts solution for making an authenticated POST:
function sendToHR(url,data){
var forDriveScope = DriveApp.getStorageUsed(); //needed to get Drive Scope requested
var dataToSend = [getName(),getID()];
for(key in data){
dataToSend.push(data[key])
}
var paylod = {
"data" : dataToSend
};
paylod = JSON.stringify(paylod);
var param = {
"method":"POST",
"headers" : {"Accept":"application/json","Authorization": "Bearer " + ScriptApp.getOAuthToken()},
"payload": paylod
};
return UrlFetchApp.fetch(url,param).getContentText();
}
And I have an example in python, that might be a bit more useful to you, in order for the python script to execute as a certien user I downloaded a JSON file with the keys from the projects console -> create credential -> get Key and download the file
def get_service():
global http_auth
global delegated_credentials
scopes = ['https://www.googleapis.com/auth/userinfo.email']
keyfile = os.path.join(CURR_DIR, JSON_FILENAME)
credentials = ServiceAccountCredentials.from_json_keyfile_name(
keyfile, scopes=scopes)
delegated_credentials = credentials.create_delegated(ADMIN_EMAIL)
http_auth = delegated_credentials.authorize(Http())
return build('SERVICE', 'v1', http=http_auth,
discoveryServiceUrl='DISCOVERY API SERVICE')
ADMIN_EMAIL is the actual admin email address and CURR_DIR and JSON_FILENAME are related to the downloaded file in your case I'm guessing you dont need admin rights just download the JSON file from the console of your current project and use your email address. Mine works when using the discovery API but a regular POST should be a bit faster to make

How do I getGivenName() of getActiveUser()

Each user on the domain initiates a simple script we run for leave entitlements but we want the welcome message to be "Hi First Name," however the script doesn't seem to be able to fetch getGivenName() from getActiveUser() for a standard user.
Is there a way?
As noted in comments, and in Documentation, the UserManager Service is only accessible by Domain Administrators.
Here's an alternative. Domain Users may have themselves in their own contacts, so how about a best-effort attempt at finding themselves there?
/**
* Get current user's name, by accessing their contacts.
*
* #returns {String} First name (GivenName) if available,
* else FullName, or login ID (userName)
* if record not found in contacts.
*/
function getOwnName(){
var email = Session.getEffectiveUser().getEmail();
var self = ContactsApp.getContact(email);
// If user has themselves in their contacts, return their name
if (self) {
// Prefer given name, if that's available
var name = self.getGivenName();
// But we will settle for the full name
if (!name) name = self.getFullName();
return name;
}
// If they don't have themselves in Contacts, return the bald userName.
else {
var userName = Session.getEffectiveUser().getUsername();
return userName;
}
}
In Apps Script, I was able to get this information using the About REST API: https://developers.google.com/drive/v2/reference/about/get
var aboutData = DriveApp.About.get();
var userEmail = aboutData["user"]["emailAddress"];
var userDisplayName = aboutData["user"]["displayName"];
You can get a user name but first you have to create a domain user using the provisioning api. You can enable the API by logging in to your admin account, and select Domain settings and the User settings tab to select the checkbox enabling the Provisioning API. Read more about it here
You can then use
user = user.getgivenName()
Since the UserManager Service is only available to a Domain Administrator, you could publish a service as the administrator, that serves user's Given Names, and invoke that from the user-run script using the UrlFetchApp.
The UserName Service
Refer to the Content Service Documentation for the background information this is based upon.
The service accepts a parameter, userName, which it uses to perform a lookup as the administrator.
Paste the following code into a script, then deploy the script as a web service. This must be done by a Domain Administrator, as the service access the UserManager Service, but the script must be made accessible by all users in the domain. (Since I'm not an admin in my domain, I cannot access the UserManager, so I've included a domain-user-invokable line for testing, calling the getOwnName() function I described in my first answer.)
Remember to invoke doGet() from the debugger to go through the authorization before accessing the published service.
/**
* When invoked as a Web Service running as Domain Administrator,
* returns the GivenName of the requested user.
*
* #param {String} userName= Should be set to Session.getEffectiveUser().getUsername().
*/
function doGet(request) {
//return ContentService.createTextOutput(getOwnName()); // for testing by non-admin user
var userName = request.parameters.userName;
var givenName = UserManager.getUser(userName).getGivenName();
return ContentService.createTextOutput(givenName);
}
Invoke service using UrlFetch
Refer to Using External APIs for an explanation of how to make use of the service written in the previous section. I'll show how to access the service from another script, but remember that you can also do this from web pages within your domain.
We will use UrlFetchApp.fetch() to get our service to return the user's first name as a String.
The service was written to accept one parameter, userName, and we append this to the url, in the form userName=<string>.
With the URL built, we fetch(), then retrieve the name from the response. While this example returns just the name, you may choose to change the service to return the complete "Hello User" string.
function testService() {
var domain = "my-google-domain.com";
var scriptId = "Script ID of service";
var url = "https://script.google.com/a/macros/"+domain+"/s/"+scriptId+"/exec?"
+ "userName="+Session.getEffectiveUser().getUsername();
var response = UrlFetchApp.fetch(url);
var myName = response.getContentText();
debugger; // pause in debugger
}
Another potential way of getting the display name on a gmail account is to find a Draft email in the GmailApp, and get the From header, which may have the full name. Some drafts might be setup with no display name in gmail, in which case the From header will only be the email address, but typically the From header is in the format:
Firstname Lastname <email#domain.com>
This code should get you the string above from the first gmail Draft: (note this will probably throw an exception if there are no drafts, so check that first.)
GmailApp.getDrafts()[0].getMessage().getHeader("From")
Ref: https://developers.google.com/apps-script/reference/gmail/gmail-message#getHeader(String)
Ref: https://www.ietf.org/rfc/rfc2822.txt

Unable to list files with GoogleDrive API

I'm trying to get a list of documents from google drive via its SDK. I use the service account to accomplish the authentication, and set the scope to https://www.googleapis.com/auth/drive which has the full permission.
However, the response contains an empty item list though there do have test files in my google drive.
AuthorizationServerDescription desc = GoogleAuthenticationServer.Description;
X509Certificate2 key = new X509Certificate2(m_file, "notasecret", X509KeyStorageFlags.Exportable);
string scope = Google.Apis.Drive.v2.DriveService.Scopes.Drive.ToString().ToLower();
AssertionFlowClient client = new AssertionFlowClient(desc, key) { ServiceAccountId = m_email, Scope = "https://www.googleapis.com/auth/" + scope };
OAuth2Authenticator<AssertionFlowClient> auth = new OAuth2Authenticator<AssertionFlowClient>(client, AssertionFlowClient.GetState);
DriveService service = new DriveService(auth);
FilesResource.ListRequest request = service.Files.List();
request.Q = "";
Stream dataStream = request.FetchAsStream();
StreamReader reader = new StreamReader(dataStream);
string responseFromServer = reader.ReadToEnd();
Check Nivco's answer to Google Drive SDK: Empty array of files returned from Drive for more details about your issue.
You're basically listing the files in the service account's Drive and not yours. What you need to do is setup the email for the user of the domain you are trying to impersonate, check the Google Drive SDK documentation for instructions and sample code showing how to do it:
https://developers.google.com/drive/delegation