Google Drive API - Transfer ownership as delegated admin - google-api

I am trying to transfer ownership from a normal user to a delegated admin user which has both the Drive service privilege and the Data Transfer privilege, which is required according to the documentation.
I'm trying to do this on behalf of the delegated admin user.
The reason for this is, that I want to make sure that every file that gets created by any user in our domain, will transfer its ownership to one admin user.
So I have an (offline) access token and refresh token stored in my backend. I use the Push Notification service from the Google Drive API to respond to new files created by users in my domain using this (stripped down) code:
$client = new \Google_Client();
$client->setAuthConfigFile('client_config.json');
// This is the access token json for the delegated admin user.
$client->setAccessToken('{access_token_json_string}');
$drive = new \Google_Service_Drive($client);
// This is the file created by the user. It has two permissions: The user is the 'owner' and the admin user is the 'writer'
$fileId = 'XXX_XXxXXXXxxXXXxxX';
// this is the admin user's permission id, of role 'writer'. This is the one I am trying to change to 'owner'.
$permissionId = '1234567890';
// I've tried setting all values, but that doesn't help
$permission = new \Google_Service_Drive_Permission();
$permission->setId($permissionId);
$permission->setEmailAddress('admin#domain.com');
$permission->setValue('admin#domain.com');
$permission->setType('user');
$permission->setRole('owner');
// Both 'put' and 'update' raise the same error
$drive->permissions->update($fileId, $permission->getId(), $permission, array(
'transferOwnership' => true
));
The response I am getting:
Error calling PUT https://www.googleapis.com/drive/v2/files/XXX_XXxXXXXxxXXXxxX/permissions/1234567890?transferOwnership=true: (403) Insufficient permissions for this file
Note that all other API calls work properly, so the access token is valid. There's also no option available within Google Drive itself to transfer the ownership (as being the admin user). Transferring it from the normal user to the admin user works as expected.

Related

Google API Auth Scopes Incorrect

Bit of an odd one. I have an endpoint to log the user into Google and grant the Google Drive authentication scope for access to a user's drive account. This is said function
if(Auth::check() && $profile->abilities()->contains('manage_docs') && $request->input('redirect_uri') && $request->input('community')) {
$scopes = [
'https://www.googleapis.com/auth/drive',
];
return Socialite::driver('google')->scopes($scopes)->with([
'state' => "sso.redirect.uri=" . $request->input('redirect_uri'). "&type=documents",
"access_type" => "offline",
"prompt" => "consent select_account"
])->redirect();
}
When a user hits this endpoint, they get redirected to Google. Users are reporting they see that they are being asked to grant access to Google Drive from my application, however their credentials do not have the above listed scopes. However, if they remove the Google Account and resign in (using the same method), they get the required scopes..
Tldr, first pass through this method doesn't add the scope, 2nd time through grants correct scopes.
If in doubt, doubt the user's ability to press the tick box to grant the auth scope...

Google Admin API with service account -- bad credentials

I'm trying to write console app code to update Google Directory with values pulled from a SQL database. I can't seem to make the API connect successfully when using a service account. Any ideas? I've whittled the code down to just the essentials here.
static void Main(string[] args)
{
try
{
// Create a ServiceAccountCredential credential
var xCred = new ServiceAccountCredential(new ServiceAccountCredential.Initializer("saxxxxxxxx#directorysync-xxxxxx.iam.gserviceaccount.com")
{
Scopes = new[] {
DirectoryService.Scope.AdminDirectoryUser,
DirectoryService.Scope.AdminDirectoryUserReadonly
}
}.FromPrivateKey("-----BEGIN PRIVATE KEY-----\nMI...p9XnI4DZFO/QQJc=\n-----END PRIVATE KEY-----\n"));
// Create the service
DirectoryService service = new DirectoryService(
new BaseClientService.Initializer()
{
HttpClientInitializer = xCred,
}
);
var listReq = service.Users.List();
listReq.Domain = "mycompany.com";
listReq.MaxResults = 100;
listReq.OrderBy = UsersResource.ListRequest.OrderByEnum.Email;
Users results = listReq.Execute();
// process the users list here...
}
catch (Exception e)
{ Console.WriteLine(e.Message); }
}
The error happens at the .Execute() line:
Google.Apis.Requests.RequestError
Not Authorized to access this resource/api [403]
Errors [
Message[Not Authorized to access this resource/api] Location[ - ] Reason[forbidden] Domain[global]
]
I've tried code seen elsewhere (How to login to Google API with Service Account in C# - Invalid Credentials) to bring in the whole contents of the .JSON file that contains the credentials for the service account; that made no difference. I'm not the google domain admin, but the admin built the credential and promises that it does, indeed, have rights to the user resources. I'm utterly lost at what's not right.
Either:
You're missing the email address of an admin user to impersonate.
An Admin of the domain needs to assign user management privileges to the service account.
Background
There's two HTTP requests involved in making a Google API request with a service account:
Using the service account's credentials to request an access token from the Google OAuth 2.0 server. A HTTP POST is sent with a JWT signed with the private key of the service account. If successful, an access token is returned which is valid for one hour.
Making the service API request (Admin SDK Directory API in this case) using the OAuth access token obtained from the last step.
The error message you provided is not listed in the JWT error codes page so step 1 is working, and the error is coming from step 2 - the request to Directory API.
You should be able to confirm this using an HTTPS request interceptor like mitmproxy.
You'd get a 403 error for the Directory API users.list method for a few reasons:
You've authenticated as a service account, but that service account has no admin privileges which are sufficient for the request.
You've authenticated as a user in the domain (either using a service account with impersonation with the sub parameter in the JWT request, or using three-legged interactive OAuth), but that user has no admin privileges which are sufficient for the request.
You've authenticated as a service account or user with sufficient privileges, but the domain parameter is not owned by the customer for which the service account or user is associated with.
In your sample code, there is no email address of a domain user (admin) specified (the email address for ServiceAccountCredential.Initializer is actually the OAuth client name of the service account, not a real email address), so it is either case 1 or 3.
To address case 1
A service account has no association with a domain or access privileges by default.
An admin can open Admin console, go to "Account > Admin roles > "User Management Admin" > Admins > Assign service accounts", then:
Add the service account name (looks like an email address, ending #gserviceaccount.com).
Click "Assign role".
Now, the service account does not need to impersonate another admin in the domain, it will directly have admin privileges.
A couple of side-effects of this:
The service account cannot be "suspended" as such, only removed from the admin role.
Audit logging of the actions will show the service account name in the "Reporting > Audit > Admin" section of Admin console.
To address case 2
You may have meant to impersonate an admin user in the domain, which you could do by adding:
, User = "admin#example.com"
- after the Scopes array passed to ServiceAccountCredential.Initializer.
This adds the email address of a user to the JWT request to retrieve an access token for that user (by adding the sub field to the claim-set of the JWT assertion).
To address case 3
Replace the "mycompany.com" on the line:
listReq.Domain = "mycompany.com";
- with a domain that is associated with the customer, or instead, remove that line and add:
listReq.Customer = "my_customer";
(literally my_customer - see users.list query-parameters)
- Which will list users on all domains associated with the customer (Google Workspace and Cloud Identity customers can have many secondary domains).

Create new Parse.Session programmatically for a user, without their password

I'm working on a existing Parse server, and I am currently adding an OAuth system, so that external apps can connect in the name of Parse.User.
I created different classes for codes and tokens, and now my external apps can send requests with an accessToken, corresponding to their application and user (who granted access).
I'm looking for a way to inform the Parse server that the "logged in user" in requests is the end user that authorized the OAuth application. For this, I have created an express middleware handling request before the Parse server middleware, extracting the access token from the request, getting the correct User and Application, and then I wanted to create a Parse.Session programmatically, get the token, and set it in the request as x-parse-session-token. This way, the next handler, Parse, would treat the request as authenticated and performed by the end user.
My problem here is that I cannot find a way to create a session programmatically, I'm aware of the Parse.User.logIn, but that works only with a password.
I've tried the following:
const oAuthSession = await new Parse.Session().save({
user: user // user got from Parse.Query(Parse.User) with masterKey
}, { useMasterKey: true })
But get a Cannot modify readonly attribute user error.
Any hidden method to programmatically create a Parse.Session without a password ?
As pointed out by #DaviMacêdo in the community forum: https://community.parseplatform.org/t/create-new-parse-session-programmatically-for-a-user-without-their-password/1751
We can inject the user directly in the request field, and it will be picked up by Parse: https://github.com/parse-community/parse-server/blob/f6a41729a7a3adc6bd5310cefb3458835b4abb58/src/middlewares.js#L199
const user = await new Parse.Query(Parse.User).get(‘idOfUser’);
req.userFromJWT = user;

Limit Scope to single file in Google Auth JWT client for Google service account

I'm am using a service account to generate an access token to access certain sheets in my google drive. That access token will be sent to the client-side user and using that users will access my sheets. But different users have access to different sheets and I want them to prevent access to other sheets with the same access token. Currently, the service account has access to all the file used by my app.
For generating access token I use googleapis's google.auth.JWT module. I use Firebase cloud functions as my backend with Node version 10
Here is my code,
try {
const jwtClient = new google.auth.JWT(
key.client_email,
null,
key.private_key,
['https://www.googleapis.com/auth/drive',], // drive scope
);
const accessToken = await jwtClient.authorize();
} catch (error) {
console.log(error)
res.status(500).json(error)
}
Thanks in advance
If the permissions of the service account gives the token full access to every file in the folder these tokens would allow to read other documents too. I think you may workaround this granting a specific user permissions to a set of documents by changing the permissions of the file itself, instead of providing a generic access token. This page of the Drive documentation describes how to change permissions at a file level and provides some Node.js samples.

How does Laravel/Lumen verifies the authentication of a user through JWT?

I'm new to Laravel and JWT-auth. I've implemented the process of generating JWT tokens and getting the associated user in my back-end, but I'm still can not understand how the server verifies the authentication of a user from just a token stored on the client side.
If I log in on machine A and change my password on machine B, can I still log in from machine A with the previous token?
You have a 'users' table and generally another table like 'sessions'. When a user makes a request to login through your API, a new line into the 'sessions' table is inserted (the token is saved) and the API return the token to the user. For all requests that need authentification, the user will have to give this token (through HTTP header for example).
When you want to authentificate a user on a request you have to verify if the token exists and remains valid and then retrieve the user. Example in Lumen :
$this->app['auth']->viaRequest('api', function ($request) {
$session = Session::where(['token' => $request->header('token')])->get(); //user gives his token in the header request
if($session){
return $session->user(); //you have setup the hasOne/hasMany relationship between sessions and users
//the user is authenticated
}
return null;
// the user is not authenticated
});
"If I log in on machine A and change my password on machine B, can I still log in from machine A with the previous token?"
If the token is still valid and you allow multiple active sessions then yes. The token isn't set depending of the password.

Resources