Transferring ownership of the file using Google Drive API - 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.

Related

how to access and modify google workspace domain user's google drive files as a domain administrator using google apps scripts

Objective:
as a google workspace domain admin for a school that uses google workspace education, I want to create a google apps script that given a google workspace user's email address (the current owner), the scritp should be able to get a list of all the user's folders and files in their google drive and then it should also be able to transfer the ownership of those folders and files to domain user and add the current owner as a viewer so they can only see the folders/files but can't modify them in any way.
things I tried:
DriveApp can access files/folders and change the ownership of the file/folder but only if you are the owner, and I want to do this as the domain admin, regardless which user owns the google drive and respective files/folders.
Drive API, seems to do the same as DriveApp as far you're the owner, I couldn't figure out how to give Drive API admin permissions so I can see every domain user google drive file list, if that's even possible.
GAM advance: I found this as management tool, I set it and it migh do what I need but it's bit complex for me, plus I was really hoping to be able to build the tool myself.
What worked halfway:
I found this: https://github.com/googleworkspace/apps-script-oauth2#using-service-accounts which refers to using a service account. It took a while but I manage to get a list of items that exist on a user's google drive with the script below. but I can't figure out how to access those files/folders so I can change the ownership or set viewers on them. I think I read that the service account will only give me read-only access so I'm doubting this is even possible.
Here's what I got so far:
function main(){
// Private key and client email of the service account.
var key = getJsonKey()
var clientEmail = 'service_account_email_setup_in_google_dev_console';
// Email address of the user to impersonate.
var userEmail = 'a_regular_domain_user#my_google_workspace_domain.com';
try{
var drive = getDriveService_(key,userEmail,clientEmail);
if (drive.hasAccess()) {
// this code gets me a json response with items that list id's and urls and other
//file metadata of the
// files that belongs to the domain user, this is as far as i got.
var url = 'https://www.googleapis.com/drive/v2/files';
var response = UrlFetchApp.fetch(url, {
headers: {
Authorization: 'Bearer ' + drive.getAccessToken()
}
});
var result = JSON.parse(response.getContentText());
//the following code returns a fileid in the user's google
//drive not shared with the admin
var fileid = JSON.stringify(result.items[0].id)
Logger.log(fileid);
//but the following code returns an error indicating that the
//file is not found (in reality it's not accessible by the
//admin account)
var file = Drive.Files.get(fileid);
//access a list of items and as I traverse it I'd like to
//change the ownership and
//add the the current user as a file viewer
//??
} else {
Logger.log(drive.getLastError());
}
}catch (e){
Logger.log(e)
}
}
// Load the JSON key file with private key for service account
function getJsonKey(){
var keyFile = DriveApp.getFileById("json_fileid_in_drive_obtained_from_googledevcons");
var key = JSON.parse(keyFile.getBlob().getDataAsString()).private_key;
return key
}
function reset() {
getDriveService__().reset();
}
//get the google drive from the domain user's email address
function getDriveService_(key,userEmail,clientEmail) {
return OAuth2.createService('GoogleDrive:' + userEmail)
.setTokenUrl('https://oauth2.googleapis.com/token')
.setPrivateKey(key)
.setIssuer(clientEmail)
.setSubject(userEmail)
.setPropertyStore(PropertiesService.getUserProperties())
.setCache(CacheService.getUserCache())
.setScope('https://www.googleapis.com/auth/drive');
}
Any help is appreciated :)
You are going in the right direction, the only part you are missing currently is setting up Domain Wide Delegation this will allow you to impersonate the users in your domain so you can make the changes on behalf of them by granting the service account permissions through the above mentioned DWD.
Since you have already created the Oauth2Service you will just need to send the user to impersonate through the OauthParams:
const oauthParams = {
serviceName: 'Nameofyourservice',
serviceAccount,
scopes: ['https://www.googleapis.com/auth/appsmarketplace.license', 'https://www.googleapis.com/auth/userinfo.email', 'https://mail.google.com', 'https://www.googleapis.com/auth/iam'],
userToImpersonate: 'usertoimpersonate#test.com',
};
The scopes were from the Marketplace API as an example.

Google Web Apps - Get user email but run scripts as owner

I've recently gravitated to google web apps and I have a bit of a dilemma. I'm trying to build an app that is open to very specific users and the data they are viewing filters based on their access group.
In a google sheet I'm listing the user emails and their respective access groups. Column A - email, Column B - access group
The issue
When the user accesses the web app I'm using this to grab their email:
var email = Session.getActiveUser().getEmail();
And then I run this code to get their access group:
function validate(email){
var sheet = SpreadsheetApp.openById(ID).getSheetByName(ssUserList);
try{
var group = getRowsData(sheet).find(e => e.userEmail === email).securityGroup;
return group;
} catch(e){
return "Not Authorized";
}
}
Because the user doesn't have access to my google sheet, they get an error when the function runs. And I can't deploy the web app to run as me because I need the user's email. I understand this very well.
What I've read:
Tons of other posts and articles about access tokens and credentials and urlFetchApps ... I don't understand any of it and to be honest I don't know which one makes more sense for my situation.
What I've tried:
I can't use the 1st usable option I've found which is to access web app 1 (which runs as user), then call web app 2 using the user email as a parameter because if they share that link from web app 2 then anyone could see the data and I'm working with really sensitive data.
I realize I could just put these parameters in a separate sheet and give them view only access and the scripts will run fine, but I'm extra and I want to do it right.
In reality I'm going to have a few other functions that will need to run as me. If you were in my shoes, where would you start? Or can someone explain it in layman's terms? Should I be looking into something like this? Any help is appreciated!
Summary
One of the possibilities, as suggested here, is to create a separate web application to handle access to SpreadSheets.
The client (the main web app) would make a request through UrlFetchApp to the middleware (web app in charge of consulting the SpreadSheet), the middleware would make the needed queries and would return them to the client. Finally, depending on the response obtained, one content or another would be rendered.
Minimal Example
Configuring the Project
First, we create two GAS projects, one we call Main and the other Middleware. The main point is that the Middleware would run as USER_DEPLOYING and the client as USER_ACCESSING. This allows access to the sheet without requiring additional permissions.
The appscripts.json file would look like this on the client. :
"oauthScopes": [
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/userinfo.email"
],
"webapp": {
"executeAs": "USER_ACCESSING",
"access": "ANYONE"
}
And like this on the middleware:
"oauthScopes": [
"https://www.googleapis.com/auth/spreadsheets"
],
"webapp": {
"executeAs": "USER_DEPLOYING",
"access": "ANYONE_ANONYMOUS"
}
If you have any questions about editing or viewing appscript.json, check the Manifest and Scopes documentation.
Attention: "access": "ANYONE" and "access": "ANYONE_ANONYMOUS" are only being used for testing purposes. This is dangerous, and should be reviewed for the specific needs of your project.
Code Sample
As for the client, we only need to ask for the email of the user who is accessing through Session.getActiveUser().getEmail() and then send it to the middleware to obtain the response. Depending on the response obtained, we will render one content or another (I assume there are two roles present: USER and ADMIN)
Client
const doGet = () => {
var data = {email: Session.getActiveUser().getEmail()}
var options = {
'method': 'POST',
'contentType': 'application/json',
'payload': JSON.stringify(data)
}
var fetch = UrlFetchApp.fetch(URL_MIDDLEWARE, options)
var userAccess = JSON.parse(fetch).accessLevel
return HtmlService.createHtmlOutput(
userAccess === "ADMIN"
? `<h1>${data.email} - ADMIN USER</h1>`
: userAccess === "USER"
? `<h1>${data.email} - COMMON USER</h1>`
: "<h1>Unauthorized</h1>" )
}
For the middleware we need to obtain that email and compare it with our sheet to check the access level of the user. Then we return the result.
Middleware
const doPost = (request) => {
// destructuring the request
const { parameter, postData: { contents, type } = {} } = request;
const userEmail = JSON.parse(contents).email;
let userAccess = SpreadsheetApp.openById(SPREADSHEET_ID).getRange('A1:B2').getValues()
// This can be replaced by a filter function
let userAccessLevel;
for (let user of userAccess) { if (userEmail == user[0]) userAccessLevel = user[1] }
return ContentService.createTextOutput(Utilities.jsonStringify({
user: userEmail,
accessLevel: userAccessLevel
}))
};
Finally, you access the Main Web App to check that everything is working.
Remember that this is a test implementation, and should not be used in production. If you need more information on these topics, you can visit the following links:
Load data asynchronously, Best Practices
Request Parameters doGet() doPost()
ContentService Class

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());
}

Unable to use sub account with Google Service Account

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);

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