I'm currently trying to develop a simple Node JS program deployed on a GCP Cloud Function, to use Google Calendar API with a Service Account.
The program is simple, i just want to create an event and add attendees.
My code works well, however i can't add attendees with the service account i have this error :
There was an error contacting the Calendar service: Error: Service accounts cannot invite attendees without Domain-Wide Delegation of Authority.
The API are activated and i allowed the calendar scope on the GSuite Admin Security, my service account have the delegation authority checked on GCP.
I search a lot a solution but nothing solve my problem..
This is the code of my module :
const { google } = require('googleapis');
const sheets = google.sheets('v4');
const SCOPES = ['https://www.googleapis.com/auth/spreadsheets','https://www.googleapis.com/auth/calendar'];
let privatekey = require("../creds.json");
async function getAuthToken() {
let jwtClient = new google.auth.JWT(
privatekey.client_email,
null,
privatekey.private_key,
SCOPES);
//authenticate request
jwtClient.authorize(function (err, tokens) {
if (err) {
console.log(err);
return;
} else {
console.log("Successfully connected!");
}
});
return jwtClient;
}
async function insertEvents(auth,items) {
const calendar = google.calendar({ version: 'v3', auth });
var startDate = new Date();
var endDate = new Date();
startDate.setHours(14, 0, 0, 0);
endDate.setHours(15, 0, 0, 0);
var attendees = [];
items.forEach(function(item){
attendees.push({email: item[2]});
});
var event = {
summary: 'Stack Roulette : It\'s Coffee Time =)',
location: 'Partout, mais avec un café',
description: "",
start: {
dateTime: startDate.toISOString(),
timeZone: 'Europe/London'
},
end: {
dateTime: endDate.toISOString(),
timeZone: 'Europe/London'
},
attendees: attendees,
reminders: {
useDefault: false,
overrides: [
{ method: 'email', minutes: 24 * 60 },
{ method: 'popup', minutes: 10 }
]
},
conferenceData: {
createRequest: {requestId: Math.random().toString(36).substring(2, 10),
conferenceSolution: {
key: {
type: "hangoutsMeet"
}
},}
}
};
calendar.events.insert(
{
auth: auth,
calendarId: 'primary',
resource: event,
conferenceDataVersion: 1
},
function(err, event) {
if (err) {
console.log(
'There was an error contacting the Calendar service: ' + err
);
return;
}
console.log('Event created: %s', event.data.htmlLink);
}
);
}
module.exports = {
getAuthToken,
getSpreadSheetValues,
insertEvents
}
I precise, there is no Frontend for my application, the code run with a cloud Function like an API Endpoint.
PS : it's not a Firebase Cloud Function, but GCP Cloud Function
If i remove attendees from event creation, it's work well but can't see the event.
Thx for your help
To set up Domain-Wide Delegation of Authority
Visit your cloud console
Go to IAM & Admin -> Service Accounts
Chose the service account you using in your script
Check Enable G Suite Domain-wide Delegation
If you are sure that you already enabled the domain-wide delegation for the correct service account and you encounter issues only when creating events with attendees:
Your issue is likely related to this issue reported on Google's Issue Tracker:
Currently there are restrictions for creation of events with attendees with a service account, especially when the attendees are outside of your domain.
When you create jwtClient, I think you have to put user primary email to JWT method.
Like:
let jwtClient = new google.auth.JWT(
privatekey.client_email,
null,
privatekey.private_key,
SCOPES,
"yourUserPrimaryEmail"); // here
Related
Goal
Assign a role dialogflow.admin to a service account I created for a project using the Node.js Client library for Google APIs.
Issue
When I try to update my service accounts IAM Policy and add a role to the service account. I get an error that the role is not supported for this resource type.
I am trying to give my service account the Dialogflow API Admin Role roles/dialogflow.admin
The method in the Node.js client library I am using is iam.projects.serviceAccounts.setIamPolicy.
I have already managed to create the service account with this Node.js client library with a function shown here.
async function createServiceAccount(projectID, serviceAccountID){
const authClient = await auth.getClient();
var request = {
name: "projects/"+projectID,
resource: {
"accountId": serviceAccountID,
"serviceAccount": {
"description" : "Service Account for project: "+projectID+" for DialogFlow authentication with VA",
"displayName": "VA Dialogflow Service Account "+projectID
}
},
auth: authClient,
};
await iam.projects.serviceAccounts.create(request, function(err, response) {
if (err) {
console.error(err);
return;
}
console.log(JSON.stringify(response, null, 2));
});
}
after this function runs, and I am sure the service account is created, I run my function that is meant to set the roles of this service account.
async function setServiceAccountRoles(projectID, serviceAccountID){
const authClient = await auth.getClient();
var request = {
resource_: "projects/"+projectID+"/serviceAccounts/"+serviceAccountID,
resource: {
policy: {
bindings: [
{
// role: "projects/"+projectID+"roles/dialogflow.admin",
role: "roles/dialogflow.admin",
"members": [
"serviceAccount:"+serviceAccountID
]
}
],
version: 1
}
},
auth: authClient,
};
await iam.projects.serviceAccounts.setIamPolicy(request, function(err, response) {
if (err) {
console.error(err);
return;
}
console.log(JSON.stringify(response, null, 2));
});
}
Error
When I run this function I am give this error:
code: 400,
errors: [
{
message: 'Role roles/dialogflow.admin is not supported for this resource.',
domain: 'global',
reason: 'badRequest'
}
]
I have used these following resources to get this far:
https://cloud.google.com/iam/docs/reference/rest/v1/projects.serviceAccounts/setIamPolicy
https://cloud.google.com/iam/docs/reference/rest/v1/Policy
https://cloud.google.com/iam/docs/granting-changing-revoking-access#granting_access_to_a_service_account_for_a_resource
Alternative methods.
I have tried changing the role to a project specific path like this:
async function setServiceAccountRoles(projectID, serviceAccountID){
const authClient = await auth.getClient();
var request = {
resource_: "projects/"+projectID+"/serviceAccounts/"+serviceAccountID,
resource: {
policy: {
bindings: [
{
role: "projects/"+projectID+"roles/dialogflow.admin",
"members": [
"serviceAccount:"+serviceAccountID
]
}
],
version: 1
}
},
auth: authClient,
};
await iam.projects.serviceAccounts.setIamPolicy(request, function(err, response) {
if (err) {
console.error(err);
return;
}
console.log(JSON.stringify(response, null, 2));
});
}
however, with this I get the error : message: "Role (projects/va-9986d601/roles/dialogflow.admin) does not exist in the resource's hierarchy.",
Is it possible that the only way to update my service account's roles and permission is through the console or gcloud commands? If so, is there any recommended ways of running said gcloud commands through the node.js client library or from a node application itself?
You are trying to set an IAM policy on the service account. That is used to grant other identities access to the service account itself.
You should modify the IAM binding for the project and not for the service account.
Use the getIamPolicy and setIamPolicy. Examples are included with the documentation.
WARNING: be very careful writing code that modifies a project's bindings. If you overwrite the bindings you can easily lock yourself out of your project. Then you would need to open a paid support ticket with Google Cloud Support. Practice with a throw away project.
I am emailing an HTML link of a Google calendar event generated by Google API to users but they are unable to edit the event, they can only view it when they click the link. I am creating this event with a service account and sharing with other users.
How can I ensure these events are editable in the user's calendar?
This is a link to the code I am using:
const { google } = require('googleapis');
// Provide the required configuration
const CREDENTIALS = JSON.parse(process.env.CREDENTIALS);
const calendarId = process.env.CALENDAR_ID;
// Google calendar API settings
const SCOPES = ['https://www.googleapis.com/auth/calendar.events'];
const calendar = google.calendar({version : "v3"});
const auth = new google.auth.JWT(
CREDENTIALS.client_email,
null,
CREDENTIALS.private_key,
SCOPES,
'email_used_to_configure_the_service_account#gmail.com'
);
auth.authorize(function (err, tokens) {
if (err) {
console.log(err);
return;
} else {
console.log("Successfully connected!");
}
});
//fetching the even object from the db to get evnt.name and co
const saveEvent = {
summary: event.name,
location: event.room.host.location,
description: event.extra,
colorId: 3,
start: {
dateTime: event.startDate,
timeZone: 'Africa/Lagos',
},
end: {
dateTime: event.endDate,
timeZone: 'Africa/Lagos',
},
organizer: {
email: 'email_used_to_configure_the_service_account#gmail.com',
displayName: 'display name',
self: true
},
attendees: [{ email: 'email of recepient of event' }]
//visibility: 'public'
}
async function generateLink(){
try{
const val = await calendar.events.insert({ auth: auth, calendarId: calendarId, resource: saveEvent, sendNotifications: true });
if(val.status === 200 && val.statusText === 'OK'){
console.log('CREATED', val);
return val.data.htmlLink;
}
return console.log('NOT CREATED')
} catch(error){
console.log(`Error ${error}`);
return;
}
}
const link = await generateLink();
let mailData = {
name: user.name ? user.name : user.firstname,
token: `${config.get('platform.url')}/event-accepted/${invite.token}`,
coverImage: event.gallery.link,
eventName: event.name,
hostName: host.name? host.name : host.firstname,
venue: event.venue,
date: event.startDate,
time: event.startDate,
attendees: '',
ticketNo: '',
cost: event.amount,
action: link // htmlLink that takes you to the calendar where user can edit event.
}
mail.sendTemplate({
template: 'acceptEventEmail',
to: u.email.value,
context: mailData
});
Current Behaviour
Expected behaviour
P.S: Code has been added to the question
YOu created the event using a Service account, think of a service account as a dummy user. When it created the event it became the owner / organizer of the event and there for only the service account can make changes to it.
You either need the service account to update it and set someone else as organizer
"organizer": {
"email": "me#gmail.com",
"displayName": "L P",
"self": true
},
Service accounts cannot invite attendees without Domain-Wide Delegation of Authority
In enterprise applications you may want to programmatically access users data without any manual authorization on their part. In Google Workspace domains, the domain administrator can grant to third party applications domain-wide access to its users' data—this is referred as domain-wide delegation of authority. To delegate authority this way, domain administrators can use service accounts with OAuth 2.0.
Problem:
When using the service user auth and inserting / updating a calendar event, the reminders are not overridden
The event is inserted / updated correct APART from the reminders are always at the default (email > 10m, popup > 30m).
Context:
Node.js using standard libraries below
Valid service account with downloaded credentials.json
Service account (service-user-account#myapp.iam.gserviceaccount.com) has write access to myuser#gmail.com calendar
Code:
const {google} = require('googleapis')
const {auth} = require('google-auth-library')
const credentials = require('./credentials.json')
const addEvent = async (auth) => {
const calendar = google.calendar({version: 'v3', auth})
const insertRes = await calendar.events.insert({
calendarId: 'myuser#gmail.com',
resource: {
summary: 'Test API',
start: {
dateTime: '2020-06-02T12:55:00.000',
timeZone: 'Europe/London'
},
end: {
dateTime: '2020-06-02T12:56:00.000',
timeZone: 'Europe/London'
},
reminders: {
useDefault: false,
overrides: [
{method: 'popup', 'minutes': 5}
]
}
}
})
console.log('insertRes', insertRes.data)
}
const getAuth = async () => {
let client = auth.fromJSON(credentials)
client.scopes = ['https://www.googleapis.com/auth/calendar', 'https://www.googleapis.com/auth/calendar.events']
return client
}
const init = async () => {
const auth = await getAuth()
await addEvent(auth)
}
init()
Response: from console.log(insertRes)
{ kind: 'calendar#event',
etag: '"3182200547452000"',
id: '6063phndufgppo8rfev1XXXXXX',
status: 'confirmed',
htmlLink:
'https://www.google.com/calendar/event?eid=NjA2M3BobmR1ZmdwcG84cmZldjFjdWh2YzQgZGFuZ2FyZmllbGR1a0Bnb29nbGVtYWlsXXXXXX',
created: '2020-06-02T12:17:53.000Z',
updated: '2020-06-02T12:17:53.768Z',
summary: 'Test API',
creator:
{ email: 'service-user-account#myapp.iam.gserviceaccount.com' },
organizer: { email: 'myuser#googlemail.com', self: true },
start:
{ dateTime: '2020-06-02T12:55:00+01:00',
timeZone: 'Europe/London' },
end:
{ dateTime: '2020-06-02T12:56:00+01:00',
timeZone: 'Europe/London' },
iCalUID: '6063phndufgppo8rfev1XXXXXX#google.com',
sequence: 0,
reminders: { useDefault: false, overrides: [{"method":"popup","minutes":5}] }
}
Hopefully someone can shed a light on the issue for me.
Thanks
It seems to be a bug, already reported on Google's Public Issue Tracker
Give it a "star" to show that more people are affected and to receive updates on the issue.
The service account user is a user more or less just like you. One can only modify reminders for the current user, not for other users.
The reminder is set, but for the "wrong" user. Try to get the event through api with the service account user, you will see the reminder there. Add a reminder with your own user through UI and do the api request again, you will not be able to see the new reminder.
If you want to set reminders on events for yourself, be sure to use your own account to modify the event, for example with OAuth 2.0.
I am trying to connect to the G-Suite's User directory using the google-admin-sdk. I am using an API Key for authorization and I am not able to reach a successful execution.
Here is the code snippet that I'm using:
import { google } from 'googleapis';
import uuid from 'uuid/v4';
const API_KEY = 'my api key goes here';
google.admin({
version: 'directory_v1',
auth: API_KEY
}).users.list({
customer: 'my_customer',
maxResults: 10,
orderBy: 'email',
}, (err, res: any) => {
if (err) { return console.error('The API returned an error:', err.message); }
const users = res.data.users;
if (users.length) {
console.log('Users:');
users.forEach((user: any) => {
console.log(`${user.primaryEmail} (${user.name.fullName})`);
});
} else {
console.log('No users found.');
}
});
Output:
Login Required
Can someone tell me what I am doing wrong here?
Also, how do I proceed further for listening to the events emitted by the Google API?
---UPDATE---
Here is the snippet that works for me now:
import { JWT } from 'google-auth-library';
import { google } from 'googleapis';
// Importing the serivce account credentials
import { credentials } from './credentials';
const scopes = ['https://www.googleapis.com/auth/admin.directory.user'];
const adminEmail = 'admin_account_email_address_goes_here';
const myDomain = 'domain_name_goes_here';
async function main () {
const client = new JWT(
credentials.client_email,
undefined,
credentials.private_key,
scopes,
adminEmail
);
await client.authorize();
const service = google.admin('directory_v1');
const res = await service.users.list({
domain: myDomain,
auth: client
});
console.log(res);
}
main().catch(console.error);
--- Bonus Tip ---
If you face any Parse Errors while using other methods of the directory, remember to JSON.stringify the request body. For example, on the admin.users.watch method:
// Watch Request
const channelID = 'channel_id_goes_here';
const address = 'https://your-domain.goes/here/notifications';
const ttl = 3600; // Or any other TTL that you can think of
const domain = 'https://your-domain.goes';
const body = {
id: channelID,
type: 'web_hook',
address,
params: {
ttl,
},
};
// Remember to put this in an async function
const res = await service.users.watch({
domain,
customer: 'my_customer',
auth: client, // get the auth-client from above
event: 'add'
}, {
headers: {
'Content-Type': 'application/json'
},
// This is the important part
body: JSON.stringify(body),
});
As you can see in the official documentation, every request sent "to the Directory API must include an authorization token". In order to authorize your request, you have to use OAuth 2.0.
You are providing an API key instead, which is not appropriate for this process. API keys are usually used for accessing public data, not users' private data as in your current situation.
You should follow the steps provided in the Node.js Quickstart instead:
First, obtain client credentials from the Google API Console.
Second, authorize the client: obtain an access token after setting the user credentials and the appropriate scopes (a process accomplish in functions authorize and getNewToken in the Quickstart).
Finally, once the client is authorized, call the API (function listUsers).
Update:
If you want to use a Service Account for this, you will have to follow these steps:
Grant domain-wide delegation to the Service Account by following the steps specified here.
In the Cloud console, create a private key for the Service Account and download the corresponding JSON file. Copy it to your directory.
Use the Service Account to impersonate a user who has access to this resource (an Admin account). This is achieved by indicating the user's email address when creating the JWT auth client, as indicated in the sample below.
The code could be something along the following lines:
const {google} = require('googleapis');
const key = require('./credentials.json'); // The name of the JSON you downloaded
const jwtClient = new google.auth.JWT(
key.client_email,
null,
key.private_key,
['https://www.googleapis.com/auth/admin.directory.user'],
"admin#domain" // Please change this accordingly
);
// Create the Directory service.
const service = google.admin({version: 'directory_v1', auth: jwtClient});
service.users.list({
customer: 'my_customer',
maxResults: 10,
orderBy: 'email',
}, (err, res) => {
if (err) return console.error('The API returned an error:', err.message);
const users = res.data.users;
if (users.length) {
console.log('Users:');
users.forEach((user) => {
console.log(`${user.primaryEmail} (${user.name.fullName})`);
});
} else {
console.log('No users found.');
}
});
Reference:
Directory API: Authorize Requests
Directory API: Node.js Quickstart
Delegate domain-wide authority to your service account
Google Auth Library for Node.js
I hope this is of any help.
I wrote a Node/Express application and want to access the events from a resource calendar via a service account.
It works fine when I use my personal calendar but with resource calendars it gives me a blank page.
That's what I have to far. What do I have to change to make it work?
let express = require('express');
let app = express();
let {google} = require('googleapis');
const calendar = google.calendar('v3');
const port = 3000;
const key = require('./credentials.json');
const jwtClient = new google.auth.JWT(
key.client_email,
null,
key.private_key,
[ 'https://www.googleapis.com/auth/calendar' ],
null
);
jwtClient.authorize(function(err) {
if (!err) {
console.log("Authorization worked!");
}
});
const calendarID = 'XXXXX#resource.calendar.google.com';
app.get('/', function(req, res) {
calendar.events.list({
auth: jwtClient,
calendarId: calendarID,
timeMin: (new Date()).toISOString(),
maxResults: 2,
singleEvents: true,
orderBy: 'startTime'
}, function(err, resp) {
res.json(resp);
});
});
app.listen(port, function(err){
if (!err) {
console.log("XXXXX is running on port", port);
} else {
console.log(JSON.stringify(err));
}
});
Any help would be appreciated :)
Service accounts work like regular accounts - they have their own permissions, and cannot access a calendar's information unless they have explicitly been given access to it.
In order to overcome this issue you can:
Share the calendar to your Service Account. You simply have to share it as you would with any regular user, albeit assigning the Service Account's e-mail.
Use Domain-Wide delegation in case you are using G Suite (not a regular/consumer account). You can learn more in the provided hyperlink. This feature allows you to "impersonate" another user of the domain, which will allow you not only to retrieve the events but also modify the calendar and events as you please, as you would have "owner" rights over it.