Gmail API returns 403 error code and "Delegation denied for <user email>" - oauth-2.0

Gmail API fails for one domain when retrieving messages with this error:
com.google.api.client.googleapis.json.GoogleJsonResponseException: 403 OK
{
"code" : 403,
"errors" : [ {
"domain" : "global",
"message" : "Delegation denied for <user email>",
"reason" : "forbidden"
} ],
"message" : "Delegation denied for <user email>"
}
I am using OAuth 2.0 and Google Apps Domain-Wide delegation of authority to access the user data. The domain has granted data access rights to the application.

Seems like best thing to do is to just always have userId="me" in your requests. That tells the API to just use the authenticated user's mailbox--no need to rely on email addresses.

I had the same issue before, the solution is super tricky, you need to impersonate the person you need to access gmail content first, then use userId='me' to run the query. It works for me.
here is some sample code:
users = # coming from directory service
for user in users:
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE, scopes=SCOPES)
####IMPORTANT######
credentials_delegated = credentials.with_subject(user['primaryEmail'])
gmail_service = build('gmail', 'v1', credentials=credentials_delegated)
results = gmail_service.users().labels().list(userId='me').execute()
labels = results.get('labels', [])
for label in labels:
print(label['name'])

Our users had migrated into a domain and their account had aliases attached to it. We needed to default the SendAs address to one of the imported aliases and want a way to automate it. The Gmail API looked like the solution, but our privileged user with roles to make changes to the accounts was not working - we kept seeing the "Delegation denied for " 403 error.
Here is a PHP example of how we were able to list their SendAs settings.
<?PHP
//
// Description:
// List the user's SendAs addresses.
//
// Documentation:
// https://developers.google.com/gmail/api/v1/reference/users/settings/sendAs
// https://developers.google.com/gmail/api/v1/reference/users/settings/sendAs/list
//
// Local Path:
// /path/to/api/vendor/google/apiclient-services/src/Google/Service/Gmail.php
// /path/to/api/vendor/google/apiclient-services/src/Google/Service/Gmail/Resource/UsersSettingsSendAs.php
//
// Version:
// Google_Client::LIBVER == 2.1.1
//
require_once $API_PATH . '/path/to/google-api-php-client/vendor/autoload.php';
date_default_timezone_set('America/Los_Angeles');
// this is the service account json file used to make api calls within our domain
$serviceAccount = '/path/to/service-account-with-domain-wide-delagation.json';
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $serviceAccount );
$userKey = 'someuser#my.domain';
// In the Admin Directory API, we may do things like create accounts with
// an account having roles to make changes. With the Gmail API, we cannot
// use those accounts to make changes. Instead, we impersonate
// the user to manage their account.
$impersonateUser = $userKey;
// these are the scope(s) used.
define('SCOPES', implode(' ', array( Google_Service_Gmail::GMAIL_SETTINGS_BASIC ) ) );
$client = new Google_Client();
$client->useApplicationDefaultCredentials(); // loads whats in that json service account file.
$client->setScopes(SCOPES); // adds the scopes
$client->setSubject($impersonateUser); // account authorized to perform operation
$gmailObj = new Google_Service_Gmail($client);
$res = $gmailObj->users_settings_sendAs->listUsersSettingsSendAs($userKey);
print_r($res);
?>

I wanted to access the emails of fresh email id/account but what happened was, the recently created folder with '.credentials' containing a JSON was associated with the previous email id/account which I tried earlier. The access token and other parameters present in JSON are not associated with new email id/account. So, in order make it run you just have to delete the '.credentails' folder and run the program again. Now, the program opens the browser and asks you to give permissions.
To delete the folder containing files in python
import shutil
shutil.rmtree("path of the folder to be deleted")
you may add this at the end of the program

Recently I started exploring Gmail API and I am following the same approach as Guo mentioned. However, it is going to take of time and too many calls when we the number of users or more. After domain wide delegation my expectation was admin id will be able to access the delegated inboxes, but seems like we need to create service for each user.

Related

I can't get Google Adwords Customers Campagin inform

I want to build a web application. Clients can use the web application to read their google Adwrods accounts information( campagins or budgets ).
First, I use oath2 get client's refresh_token and access_token.
Using the refresh_token, I can get all adwords id under the client by (https://github.com/googleads/google-ads-ruby)
client = Google::Ads::GoogleAds::GoogleAdsClient.new do |config|
config.client_id = "client_id"
config.client_secret = "client_secret"
config.refresh_token = "refresh_token"
config.login_customer_id = "XXX-XXX-XXXX"
config.developer_token = "XXXXXXXXXXXXXXXX"
end
accessible_customers = client.service.customer.list_accessible_customers().resource_names
When I want to get client Adword account information,
resource_name = client.path.customer("XXXXXXXX")
customer = client.service.customer.get_customer(resource_name: resource_name)
I get "GRPC::Unauthenticated: 16:Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential", but the config file can't let me set access_token.
So, where can i set client's access_token, or which step i missed?
The error is telling you that client has not been set up correctly. It could be a bunch of issues from Google account to wrong information. I would check and make sure all the info you are passing in Google::Ads::GoogleAds::GoogleAdsClient.new is correct.
Also, you only need to pass 'login_customer_id' for manager accounts only, it doesn't sound like you are a manager account.
From https://github.com/googleads/google-ads-ruby/blob/master/google_ads_config.rb
# Required for manager accounts only: Specify the login customer ID used to
# authenticate API calls. This will be the customer ID of the authenticated
# manager account. If you need to use different values for this field, then
# make sure fetch a new copy of the service after each time you change the
# value.
# c.login_customer_id = 'INSERT_LOGIN_CUSTOMER_ID_HERE'

boto3 list all accounts in an organization

I have a requirement that I want to list all the accounts and then write all the credentials in my ~/.aws/credentials file. Fir this I am using boto3 in the following way
import boto3
client = boto3.client('organizations')
response = client.list_accounts(
NextToken='string',
MaxResults=123
)
print(response)
This fails with the following error
botocore.exceptions.ClientError: An error occurred (ExpiredTokenException) when calling the ListAccounts operation: The security token included in the request is expired
The question is , which token is it looking at? And if I want information about all accounts what credentials should I be using in the credentials file or the config file?
You can use boto3 paginators and pages.
Get an organizations object by using an aws configuration profile in the master account:
session = boto3.session.Session(profile_name=master_acct)
client = session.client('sts')
org = session.client('organizations')
Then use the org object to get a paginator.
paginator = org.get_paginator('list_accounts')
page_iterator = paginator.paginate()
Then iterate through every page of accounts.
for page in page_iterator:
for acct in page['Accounts']:
print(acct) # print the account
I'm not sure what you mean about "getting credentials". You can't get someone else's credentials. What you can do is list users, and if you want then list their access keys. That would require you to assume a role in each of the member accounts.
From within the above section, you are already inside a for-loop of each member account. You could do something like this:
id = acct['Id']
role_info = {
'RoleArn': f'arn:aws:iam::{id}:role/OrganizationAccountAccessRole',
'RoleSessionName': id
}
credentials = client.assume_role(**role_info)
member_session = boto3.session.Session(
aws_access_key_id=credentials['Credentials']['AccessKeyId'],
aws_secret_access_key=credentials['Credentials']['SecretAccessKey'],
aws_session_token=credentials['Credentials']['SessionToken'],
region_name='us-east-1'
)
However please note, that the role specified OrganizationAccountAccessRole needs to actually be present in every account, and your user in the master account needs to have the privileges to assume this role.
Once your prerequisites are setup, you will be iterating through every account, and in each account using member_session to access boto3 resources in that account.

Error when querying Microsoft Graph API Shifts: "MS-APP-ACTS-AS header needs to be set for application context requests"

We are trying to query shifts in the Microsoft Graph API using a C# app, now that StaffHub got deprecated , in the past we were getting an Unknown Error which looked like a permissions issue.
In the docs I noticed permissions for Schedule.ReadAll and Schedule.ReadWriteAll so I added them to the application permissions in our App Registration in Azure.
Now when we send the request to https://graph.microsoft.com/beta/teams/{teamid}/schedule we get this error:
Microsoft.Graph.ServiceException: 'Code: Forbidden Message: {"error":{"code":"Forbidden","message":"MS-APP-ACTS-AS header needs to be set for application context requests.","details":[],"innererror":{"code":"MissingUserIdHeaderInAppContext"}}}
The documentation says the Schedule permissions are in private preview, are these required for querying a schedule & shifts, and if so, is it possible to request access to the private preview?
I'm in the same situation. It's possible to request private preview access (we have), but I'm guessing that it's primarily granted to Microsoft partners or at least have a connection at Microsoft.
The workaround for me has been getting access on behalf of a user. It does however require the user to enter username and password in order to get an access token, so it might not be a perfect solution for you. But it works. You need to add (and, I believe, grant admin consent for) delegated permissions for this to work, either Group.Read.All or Group.ReadWrite.All.
Edit:
I've got it working now. We have private preview access, so I'm not sure this will help you unless you do too, but as I understand it will be available eventually. Given your question, I presume you already have an access token.
Add MS-APP-ACT-AS as a header with the user ID of the user you want the Graph client to act as.
If you're using the Graph SDK for .NET Core you can just add a header to the authentication provider:
public IAuthenticationProvider GetAuthenticationProviderForActingAsUser(string userId, string accessToken)
{
return new DelegateAuthenticationProvider(
requestMessage =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
// Get event times in the current time zone.
requestMessage.Headers.Add("Prefer", "outlook.timezone=\"" + TimeZoneInfo.Local.Id + "\"");
requestMessage.Headers.Add("MS-APP-ACTS-AS", userId);
return Task.CompletedTask;
});
}
Then you call the graph service client:
var authenticationProvider = GetAuthenticationProviderForActingAsUser(userId, accessToken);
var graphClient = new GraphServiceClient(authenticationProvider);
You should then be able to fetch the shifts:
var shifts = await graphClient.Teams[teamId].Schedule.Shifts
.Request()
.AddAsync(shift);

Error 400: invalid_scope "https://www.googleapis.com/auth/chat.bot"

The documentation for the new google hangouts chat says that you need to authorize the scope https://www.googleapis.com/auth/chat.bot to do pretty much anything.
Here's the error:
While generating an authentication URL using their OAuth2 client I get the message that the scope is invalid. I don't have that problem if I use https://www.googleapis.com/auth/chat or some other scope like the one for google plus.
When I try to google things on in the API Explorer no combination of the URL or parts of the URL work either.
Here is my code to fetch the URL, seems to work just fine for everything else:
var {google} = require('googleapis');
var OAuth2 = google.auth.OAuth2;
var oauth2Client = new OAuth2(
"clientid-idididid.apps.googleusercontent.com",
"_secretsuff",
"http://localhost:3000/auth/google/callback"
);
var scopes = [
"https://www.googleapis.com/auth/chat", //Works
"https://www.googleapis.com/auth/chat.bot" // Does not work
];
var url = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: scopes,
});
console.log(url);
In case others are running across this problem I think I've figured this out. Google doesn't seem need this auth scope enabled by a domain user because it's already authorised on the domain when your testing your bot. The "authorisation" of these scopes are dictated by users in a domain adding/removing bots from spaces.
I'll go into a bit of detail if you're confused.
When you create a bot in the console for an organisation https://console.cloud.google.com/apis/api/chat.googleapis.com/ your bot is added to the domain and can be added to spaces by users. If then go over to to the credentials and create a service account you can use that json file credentials to access the API as your bot. The code below gets a list of the people in a space.
var { google } = require('googleapis');
var chat = google.chat("v1");
var key = require('./google_service-account-credentials.json');
var jwtClient = new google.auth.JWT(
key.client_email,
null,
key.private_key,
['https://www.googleapis.com/auth/chat.bot'], // an array of auth scopes
null
);
jwtClient.authorize(function (err, tokens) {
chat.spaces.members.list({
auth: jwtClient,
parent: "spaces/AAAAD4xtKcE"
}, function (err, resp) {
console.log(resp.data);
});
});
If you try to get a list of members on other spaces (and other domains) the bot will fail with the exact same error message:
"Bot is not a member of the space."
I assume if you list your bot on the marketplace and it gets added to different domains and spaces google's API makes sure that your bot can do what it's trying to do on a space by space basis. It would be annoying have to setup some authentication flow after a bot has already been added for it to do its job. This is also probably why the current REST api doesn't let you list spaces under domains, it's not the paradigm this API works under.
It may have to do with one of the following:
The scope is created for service accounts. Make sure you are accessing the REST API with a service account.
Make sure that the bot is added to the room or space and has access to what you want it do.
Make sure the Service account is part of the bot project that you are using for the bot.

How to get a full list of repositories that a user is allowed to access?

I have found bitbucket api like:
https://bitbucket.org/api/2.0/repositories/{teamname}
But this link return 301 status (moved permanently to !api/2.0/repositories/{teamname}).
Ok, but this one returns status 200 with zero repositories.
I provide two parameters as user and password, but nothing seems changed.
So, can anybody answer how to get full list of private repositories that allowed to specific user?
Atlassian Documentation - Repositories Endpoint provides a detail documentation on how to access the repositories.
The URL mentioned in bitbucket to GET a list of repositories for an account is:
GET https://api.bitbucket.org/2.0/repositories/{owner}
If you use the above URL it always retrieves the repositories where you are the owner. In order to retrieve full list of repositories that the user is member of, you should call:
GET https://api.bitbucket.org/2.0/repositories?role=member
You can apply following set of filters for role based on your needs.
To limit the set of returned repositories, apply the
role=[owner|admin|contributor|member] parameter where the roles are:
owner: returns all repositories owned by the current user.
admin: returns repositories to which the user has explicit
administrator access.
contributor: returns repositories to which the user has explicit write access.
member: returns repositories to which the user has explicit read
access.
Edit-1:
You can make use of Bitbucket REST browser for testing the request/response.(discontinued)
You should not use the API from the https://bitbucket.org/api domain.
Instead, you should always use https://api.bitbucket.org.
Now one reason you might be getting an empty result after following the redirect could be because some http clients will only send Basic Auth credentials if the server explicitly asks for them by returning a 401 response with the WWW-Authenticate response header.
The repositories endpoint does not require authentication. It will simply return the repos that are visible to anonymous users (which might well be an empty set in your case) and so clients that insist on a WWW-Authenticate challenge (there are many, including Microsoft Powershell) will not work as expected (note, curl always sends Basic Auth credentials eagerly, which makes it a good tool for testing).
Unfortunately, from what I see in the documentation, there is no way to list all private repositories which the user has access to.
GET https://api.bitbucket.org/2.0/repositories
"Returns a paginated list of all public repositories." according to the doco.
GET https://api.bitbucket.org/2.0/repositories/{owner}
"Returns a paginated list of all repositories owned by the specified account or UUID." according to the doco.
So, getting all private repositories not necessarily owned by the user is either not possible, or I haven't found the right endpoint, or the documentation is inacurate.
None of the answers above worked for me, so this is what I did. We'll use the Bitbucket REST API.
Authentication
You can't use your normal credentials. I created an API Password. I'm not sure how to get to this page via your browser, but go here: https://bitbucket.org/account/settings/app-passwords/
Create an App Password, then cut and save the password that Atlassian generates for you.
Curl
curl --user your_username:your_app_password https://api.bitbucket.org/2.0/repositories/your_workspace?pagelen=100
I piped that to jq and saved it to a file.
your_workspace you get from looking at the URL of any of your repositories.
Paging
The maximum pagelen appears to be 100. If you have more than 100 repos, then you might have to do this:
curl --user your_username:your_app_password https://api.bitbucket.org/2.0/repositories/your_workspace?pagelen=100&page=2
The JSON
The JSON isn't too bad. You want the "values" array. From there, look at links.clone, which might have two entries like this:
"clone": [
{
"href": "https://user#bitbucket.org/WORKSPACE/REPO.git",
"name": "https"
},
{
"href": "git#bitbucket.org:WORKSPACE/REPO.git",
"name": "ssh"
}
],
That's a cut & paste from my results with personal info changed. Also useful are two other fields:
"full_name": "WORKSPACE/repo",
"name": "Repo",
Expanding on blizzard's answer, here's a little node.js script I just wrote:
import axios from 'axios';
import fs from 'fs';
async function main() {
const bitbucket = axios.create({
baseURL: 'https://api.bitbucket.org/2.0',
auth: {
username: process.env.BITBUCKET_USERNAME!,
password: process.env.BITBUCKET_PASSWORD!,
}
});
const repos = [];
let next = 'repositories?role=member';
for(;;) {
console.log(`Fetching ${next}`)
const res = await bitbucket.get(next);
if(res.status < 200 || res.status >= 300) {
console.error(res);
return 1;
}
repos.push(...res.data.values);
if(!res.data.next) break;
next = res.data.next;
}
console.log(`Done; writing file`);
await fs.promises.writeFile(`${__dirname}/../data/repos.json`,JSON.stringify(repos,null,2),{encoding:'utf8'});
}
main().catch(err => {
console.error(err);
});

Resources