401 Access denied. You are not authorized to read activity records - google-api

I'm trying to get data from Reports API.
I get access token for service account and using it in GET request. Response always
{
"error": {
"errors": [
{
"domain": "global",
"reason": "authError",
"message": "Access denied. You are not authorized to read activity records.",
"locationType": "header",
"location": "Authorization"
}
],
"code": 401,
"message": "Access denied. You are not authorized to read activity records."
}
}
I'm using Java for request. Without Google API library (client requirement). Source code is
String urlString = "https://www.googleapis.com/admin/reports/v1/activity/users/all/applications/drive?maxResults=25";
URL url = new URL(urlString);
HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
// optional default is GET
urlConnection.setRequestMethod("GET");
urlConnection.setDoInput(true);
// Add request header.
urlConnection.setRequestProperty("Authorization", "Bearer " + accessToken.getValue());
int responseCode = urlConnection.getResponseCode();
System.out.println("\nSending 'GET' request to URL : " + urlString);
System.out.println("Response Code : " + responseCode);
BufferedReader bufferedReader;
if (responseCode == 200) {
bufferedReader = new BufferedReader(
new InputStreamReader(urlConnection.getInputStream()));
} else {
bufferedReader = new BufferedReader(
new InputStreamReader(urlConnection.getErrorStream()));
}
String inputLine;
StringBuffer stringBuffer = new StringBuffer();
while ((inputLine = bufferedReader.readLine()) != null) {
stringBuffer.append(inputLine);
}
bufferedReader.close();
System.out.println(stringBuffer.toString());
Can you, please, help me what I'm missing?
Regards,
Aleks.

"Access denied. You are not authorized to read activity records.",
means just that the user you are authecated with does not have access to do what you are trying to do. To use a service account with this api you need to set up domain wide delegation
In enterprise applications you may want to programmatically access a user's data without any manual authorization on their part. In G Suite domains, the domain administrator can grant third-party applications with 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.
Go to your G Suite domain’s Admin console.
Select Security from the list of controls. If you don't see Security listed, select More controls from the gray bar at the bottom of the page, then select Security from the list of controls.
Select Advanced settings from the list of options.
Select Manage third party OAuth Client access in the Authentication section.
In the Client name field enter the service account's Client ID.
In the One or More API Scopes field enter the list of scopes that your application should be granted access to (see image below). For example if you need domain-wide access to activity reports enter: https://www.googleapis.com/auth/admin.reports.audit.readonly
Click the Authorize button.

Just the below code. Two things are very important here: email-id i.e. SERVICE_ACCOUNT_EMAIL and json file SERVICE_ACCOUNT_PKCS12_FILE_PATH:
Source: https://developers.google.com/admin-sdk/reports/v1/guides/delegation
I am using the the GO version of it and it works like a charm after spending 2 days on it :)) (by the way GO version can be found here: https://developers.google.com/admin-sdk/directory/v1/guides/delegation#go)
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson.JacksonFactory;
import com.google.api.services.admin.reports.Reports;
import com.google.api.services.admin.reports.ReportsScopes;
...
/** Email of the Service Account */
private static final String SERVICE_ACCOUNT_EMAIL = "<some-id>#developer.gserviceaccount.com";
/** Path to the Service Account's Private Key file */
private static final String SERVICE_ACCOUNT_PKCS12_FILE_PATH = "/path/to/<public_key_fingerprint>-privatekey.p12";
/**
* Build and returns a Reports service object authorized with the service accounts
* that act on behalf of the given user.
*
* #param userEmail The email of the user. Needs permissions to access the Admin APIs.
* #return Reports service object that is ready to make requests.
*/
public static Reports getReportsService(String userEmail) throws GeneralSecurityException,
IOException, URISyntaxException {
HttpTransport httpTransport = new NetHttpTransport();
JacksonFactory jsonFactory = new JacksonFactory();
GoogleCredential credential = new GoogleCredential.Builder()
.setTransport(httpTransport)
.setJsonFactory(jsonFactory)
.setServiceAccountId(SERVICE_ACCOUNT_EMAIL)
.setServiceAccountScopes(ReportsScopes.ADMIN_REPORTS_AUDIT_READONLY)
.setServiceAccountUser(userEmail)
.setServiceAccountPrivateKeyFromP12File(
new java.io.File(SERVICE_ACCOUNT_PKCS12_FILE_PATH))
.build();
Reports service = new Reports.Builder(httpTransport, jsonFactory, null)
.setHttpRequestInitializer(credential).build();
return service;
}

Related

Application Permission support for Dynamics Customer Engagement Web API

We are planning to move from Organization Service to Common Data Service Web API so we could utilize OAuth 2.0 authentication instead of a service account which customer has some security concerns.
Once we did some prototype, we discovered that the Web API authentication is a little different from typical Graph API authentication. It only supports Delegated Permission. Thus a user credential must be presented for acquiring the access token.
Here is the Azure AD Graph API permission for CRM Web API:
Here is the code in acquiring the access token for the sample code at Web API Global Discovery Service Sample (C#)
string GlobalDiscoUrl = "https://globaldisco.crm.dynamics.com/";
AuthenticationContext authContext = new AuthenticationContext("https://login.microsoftonline.com", false);
UserCredential cred = new UserCredential(username, password);
AuthenticationResult authResult = authContext.AcquireToken(GlobalDiscoUrl, clientId, cred);
Here is another similar post Connect to Dynamics 365 Customer Engagement web services using OAuth although it is more than one year old.
Do you know when MS would support Application permission to completely eliminate the user from authentication? Or there is any particular reason to keep the user here. Thanks for any insights.
[Update 1]
With below answer from James, I did the modification for the code, here is my code
string clientId = "3f4b24d8-61b4-47df-8efc-1232a72c8817";
string secret = "xxxxx";
ClientCredential cred = new ClientCredential(clientId, secret);
string GlobalDiscoUrl = "https://globaldisco.crm.dynamics.com/";
AuthenticationContext authContext = new AuthenticationContext("https://login.microsoftonline.com/common", false);
AuthenticationResult authResult = authContext.AcquireToken(GlobalDiscoUrl, cred);
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
client.Timeout = new TimeSpan(0, 2, 0);
client.BaseAddress = new Uri(GlobalDiscoUrl);
HttpResponseMessage response = client.GetAsync("api/discovery/v1.0/Instances", HttpCompletionOption.ResponseHeadersRead).Result;
if (response.IsSuccessStatusCode)
{
//Get the response content and parse it.
string result = response.Content.ReadAsStringAsync().Result;
JObject body = JObject.Parse(result);
JArray values = (JArray)body.GetValue("value");
if (!values.HasValues)
{
return new List<Instance>();
}
return JsonConvert.DeserializeObject<List<Instance>>(values.ToString());
}
else
{
throw new Exception(response.ReasonPhrase);
}
}
so I am able to acquire the access token, but it still could not access the global discovery services.
Here is what the access token looks like:
{
"aud": "https://globaldisco.crm.dynamics.com/",
"iss": "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/",
"iat": 1565802457,
"nbf": 1565802457,
"exp": 1565806357,
"aio": "42FgYEj59uDNtwvxTLnprU0NYt49AA==",
"appid": "3f4b24d8-61b4-47df-8efc-1232a72c8817",
"appidacr": "1",
"idp": "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/",
"tid": "f8cdef31-a31e-4b4a-93e4-5f571e91255a",
"uti": "w8uwKBSPM0y7tdsfXtAgAA",
"ver": "1.0"
}
By the way, we did already create the application user inside CRM by following the instruction.
Anything I am missing here?
[Update 2]
For WhoAmI request, there are different results. If I am using latest MSAL and with authority "https://login.microsoftonline.com/AzureADDirectoryID/oauth2/authorize", I would be able to get the correct result. If I am using MSAL with "https://login.microsoftonline.com/common/oauth2/authorize", it won't work, I would get unauthorized error. If I am using ADAL 2.29, it is not working for both authority. Here is the working code:
IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create("3f4b24d8-61b4-47df-8efc-1232a72cxxxx")
.WithClientSecret("xxxxxx")
// .WithAuthority("https://login.microsoftonline.com/common/oauth2/authorize", false)
.WithAuthority("https://login.microsoftonline.com/3a984a19-7f55-4ea3-a422-2d8771067f87/oauth2/authorize", false)
.Build();
var authResult = app.AcquireTokenForClient(new String[] { "https://crmxxxxx.crm5.dynamics.com/.default" }).ExecuteAsync().Result;
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
client.Timeout = new TimeSpan(0, 2, 0);
client.BaseAddress = new Uri("https://crm525842.api.crm5.dynamics.com/");
HttpResponseMessage response = client.GetAsync("api/data/v9.1/WhoAmI()", HttpCompletionOption.ResponseHeadersRead).Result;
if (response.IsSuccessStatusCode)
{
//Get the response content.
string result = response.Content.ReadAsStringAsync().Result;
Console.WriteLine(result);
}
else
{
throw new Exception(response.ReasonPhrase);
}
The documentation isn't the easiest to follow, but from what I understand you should start with Use OAuth with Common Data Service.
You then have two subtle options when registering your app. The second does not require the Access Dynamics 365/Common Data Service as organization users permission
Giving access to Common Data Service
If your app will be a client which allows the authenticated user to
perform operations, you must configure the application to have the
Access Dynamics 365 as organization users delegated permission.
Or
If your app will use Server-to-Server (S2S) authentication, this step
is not required. That configuration requires a specific system user
and the operations will be performed by that user account rather than
any user that must be authenticated.
This is elaborated further.
Connect as an app
Some apps you will create are not intended to be run interactively by
a user. ... In these cases you can create a special application user
which is bound to an Azure Active Directory registered application and
use either a key secret configured for the app or upload a X.509
certificate. Another benefit of this approach is that it doesn't
consume a paid license.
Register your app
When registering an app you follow many of the same steps ... with the
following exceptions:
You do not need to grant the Access Dynamics 365 as organization users permission.
You will still have a system user record in Dynamics to represent the application registration. This supports a range of basic Dynamics behaviours and allows you to apply Dynamics security to you app.
As opposed to a username and password you can then use the secret to connect.
string serviceUrl = "https://yourorg.crm.dynamics.com";
string clientId = "<your app id>";
string secret = "<your app secret>";
AuthenticationContext authContext = new AuthenticationContext("https://login.microsoftonline.com/common", false);
ClientCredential credential = new ClientCredential(clientId, secret);
AuthenticationResult result = authContext.AcquireToken(serviceUrl, credential);
string accessToken = result.AccessToken;
Or a certificate.
string CertThumbPrintId = "DC6C689022C905EA5F812B51F1574ED10F256FF6";
string AppID = "545ce4df-95a6-4115-ac2f-e8e5546e79af";
string InstanceUri = "https://yourorg.crm.dynamics.com";
string ConnectionStr = $#"AuthType=Certificate;
SkipDiscovery=true;url={InstanceUri};
thumbprint={CertThumbPrintId};
ClientId={AppID};
RequireNewInstance=true";
using (CrmServiceClient svc = new CrmServiceClient(ConnectionStr))
{
if (svc.IsReady)
{
...
}
}
You may also want to check out Build web applications using Server-to-Server (S2S) authentication which appears to be a similar (but different).
Use server-to-server (S2S) authentication to securely and seamlessly
communicate with Common Data Service with your web applications and
services. S2S authentication is the common way that apps registered on
Microsoft AppSource use to access the Common Data Service data of
their subscribers. ... Rather than user credentials, the application is authenticated based on a service principal identified by an Azure AD Object ID value which is stored in the application user record.
Aside; if you are currently using the Organization Service .NET object, that is being migrated to using the Web API internally.
Microsoft Dynamics CRM 2011 endpoint
The Dynamics 365 SDK assemblies will be updated to use the Web API.
This update will be fully transparent to you and any code written
using the SDK itself will be supported.

When I connect to an Account using the Drive API, I get an email with Appname of 'QuickStart'

I have recently setup an account in Google APIs and Services. In this account I enabled Google Drive integration. In the Drive UI Integration section, I have setup the Application Name as per this screenshot: https://www.screencast.com/t/5VphcOkfAXyN
In the Credentials section which I setup for this application, I also entered my application name: https://www.screencast.com/t/3XudlhbE
When I connect to my account via OpenAuth, I get a confirmation email to the connected account, from Google, which tells me that 'Quickstart connected to your Google Account'. I am expecting that the email would tell me 'My App Name connected to your Google Account': https://www.screencast.com/t/fmSXswXS
I have searched through my Google Account settings but I can't find anywhere where it says 'Quickstart'. What do I need to do in order to ensure that my actual app name is the one that appears on the confirmation email, and in the 'Apps with access to your account' permissions page (https://www.screencast.com/t/hcHSkDr5)?
Here is my FlowMetadata as requested in the comments:
public class GoogleDriveAppFlowMetadata : FlowMetadata
{
private static readonly IAuthorizationCodeFlow flow =
new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = AppConfig.CloudStorage.Google.Drive.ClientId,
ClientSecret = AppConfig.CloudStorage.Google.Drive.ClientSecret,
},
Scopes = new[] { DriveService.Scope.Drive },
DataStore = new GoogleDriveDataStore(),
});
public override string AuthCallback
{
get { return #"/GoogleDriveAuthCallback/IndexAsync"; }
}
public override string GetUserId(Controller controller)
{
if (controller.HttpContext.Session[SessionConstants.CloudStorageAccount] == null)
throw new Exception("CloudStorageAccount was not populated!");
CloudStorageDto cs = (CloudStorageDto)controller.HttpContext.Session[SessionConstants.CloudStorageAccount];
return $"{controller.User.GetSessionToken()}_{cs.Id}";
}
public override IAuthorizationCodeFlow Flow
{
get { return flow; }
}
}
I found the setting in the OAuth Consent Screen tab - this is the one that I needed: https://screencast.com/t/cI4lZKmNWeKp

Login Required 401 using Google ServiceAccountCredential using Google Admin Directory API

I have tried to follow the simple example listed here: https://developers.google.com/admin-sdk/directory/v1/quickstart/dotnet
The difference is I generated a Service Account Credential, and assigned it as a Delegate with the Role Project Owner, so it has full access. I also assigned it the proper namespaces for scopes.
Here it has access to orgunits which is what I'm trying to list in the Directory API
Here is my service account defined
Here are my credentials
I downloaded the JSON for the credential and added it to my project. I can confirm that the code loades the ServiceAccountCredential and successfully authenticates and gets an access token by inspecting the debugger.
But then I pass the credential to the Service Initializer, and when I create and execute a request it fails with
{"Google.Apis.Requests.RequestError\r\nLogin Required [401]\r\nErrors [\r\n\tMessage[Login Required] Location[Authorization - header] Reason[required] Domain[global]\r\n]\r\n"}
Here's the code:
using Google.Apis.Auth.OAuth2;
using Google.Apis.Services;
using System;
using System.Collections.Generic;
using System.IO;
namespace DirectoryQuickstart
{
class Program
{
static string[] Scopes = { DirectoryService.Scope.AdminDirectoryUser, DirectoryService.Scope.AdminDirectoryOrgunit };
static string ApplicationName = "slea-crm";
static string Secret = "gsuite-secret.json";
static void Main(string[] args)
{
ServiceAccountCredential sac = GoogleCredential.FromFile(Secret).CreateScoped(Scopes).UnderlyingCredential as ServiceAccountCredential;
var token = sac.GetAccessTokenForRequestAsync().Result;
// Create Directory API service.
var service = new DirectoryService(new BaseClientService.Initializer()
{
HttpClientInitializer = sac,
ApplicationName = ApplicationName,
});
OrgunitsResource.ListRequest request = service.Orgunits.List(customerId: "REDACTED");
IList<OrgUnit> orgUnits = request.Execute().OrganizationUnits;
if (orgUnits != null && orgUnits.Count > 0)
{
foreach (var orgUnit in orgUnits)
{
Console.WriteLine("{0} ({1})", orgUnit.Name, orgUnit.OrgUnitPath);
}
}
else
{
Console.WriteLine("No orgunits found.");
}
Console.Read();
}
}
}
Here is the content of my JSON secret (with redactions)
What am I missing here?
EDIT: OK, I breakpoint the code while it generates the request, and I can see that no where does it set the Authorization token bearer in the headers. Why? I would expect this HttpClientInitializer class to take care of that, since the API docs say it knows how to handle that, and every example on the internet I've found shows it just passing the credential into the service initializer. But when I walked through it, even though the credential has already been granted an access token and one exists within it, nowhere does the request have the header updated.
The only thing I can see is there is some way to add an HTTP request interceptor where possibly I could do this myself, but wow, this seems really...bizarre -- after all this work they did on the dotnet client SDK, I honestly could have just written direct to the HTTP API and it would have been a lot simpler and easier to follow.
The missing piece of the puzzle is this line:
ServiceAccountCredential sac = GoogleCredential.FromFile(Secret)
.CreateScoped(Scopes)
.UnderlyingCredential as ServiceAccountCredential;
Needs to be modified to this:
static string userName = "admin#yourdomain.com" // valid user in your org
ServiceAccountCredential sac = GoogleCredential.FromFile(Secret)
.CreateScoped(Scopes)
.CreateWithUser(userName)
.UnderlyingCredential as ServiceAccountCredential;
Java/Python/Go sample of doing similar is here: https://developers.google.com/admin-sdk/directory/v1/guides/delegation#create_the_service_account_and_its_credentials
This has been answered but adding more details here. If anyone wants to impersonate user to upload file on google drive using Service account. Follow these steps
Create Service Account
Enable Site Wide delegation for service account
Get Service account client ID
Enable Client ID to use Google Drive API using Google Admin Console->Manage API
Use the below C# code to upload file
public static DriveService GetService()
{
string[] scopes = new string[] { DriveService.Scope.Drive };
//"SERVICE_ACCOUNT_EMAIL_HERE";
String serviceAccountEmail = "test-417#elated-graph-261115.iam.gserviceaccount.com";
// Scope and user email id which you want to impersonate
var initializer = new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
Scopes = scopes,
User = "yourEmail#domain.com"
};
//get private key, from .JSON file
var credential = new ServiceAccountCredential(initializer.FromPrivateKey("-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCkHeAicu6uFQn0\n7KUVTjgZ68nQui8+c8NmKW8aW8vhkBIKfdewXFECiUlTMPyI+HXbubsCK5Dl2xBS\nnphLq6YyE0xEQxNFLYHwfUKuzGQ2rV+qObcZ0mLZjCaf+pw3YiRVuU6OtslLJKJH\n-----END PRIVATE KEY-----\n"));
// Create the service.
var service = new DriveService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "DriveAPI",
});
service.HttpClient.Timeout = TimeSpan.FromMinutes(100);
return service;
}
That's it, we are done above Code is using Impersonation/Delegation for uploading file on Google Drive using Service account
Reference : Upload file to Google Drive using Service Account in C# MVC (With Impersonation)

Using Google API to modify google contacts

I want to start developing with Google API’s with a .NET client. For first step I tried to get all google contacts and now I want to insert contacts.
I have read a lot about Google API’s to CRUD (create, read, update and delete) contacts. There are the Contact API, the People API and other(?). What is the best way to CRUD contacts?
UserCredential credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
new ClientSecrets
{
ClientId = "xyz.apps.googleusercontent.com",
ClientSecret = " xyz"
},
new[] { "profile", "https://www.google.com/m8/feeds/contacts/xy%40gmail.com/full" },
"me",
CancellationToken.None).Result;
// Create the service.
var peopleService = new PeopleService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "WindowsClient-Google-Sync",
});
ListRequest listRequest = peopleService.People.Connections.List("people/me");
listRequest.SyncToken = null;
ListConnectionsResponse result = listRequest.Execute();
foreach (Person person in result.Connections)
{
foreach (Name name in person.Names)
{
Console.WriteLine("Name: " + name.DisplayName);
}
}
How can I extend this sample to create or update contacts?
Thanks
Andreas
If you will check Google Contacts API:
The Google Contacts API allows client applications to view and update a user's contacts. Contacts are stored in the user's Google Account; most Google services have access to the contact list.
Your client application can use the Google Contacts API to create new contacts, edit or delete existing contacts, and query for contacts that match particular criteria
Creating Contact
To create a new contact, send an authorized POST request to the user's contacts feed URL with contact data in the body.
The URL is of the form:
https://www.google.com/m8/feeds/contacts/{userEmail}/full
Upon success, the server responds with an HTTP 201 Created status code and the created contact entry with some additional elements and properties (shown in bold) that are set by the server, such as id, various link elements and properties.
import com.google.gdata.client.contacts.ContactsService;
import com.google.gdata.data.contacts.ContactEntry;
import com.google.gdata.data.contacts.ContactGroupFeed;
import com.google.gdata.data.extensions.City;
import com.google.gdata.data.extensions.Country;
import com.google.gdata.data.extensions.Email;
import com.google.gdata.data.extensions.ExtendedProperty;
import com.google.gdata.data.extensions.FormattedAddress;
import com.google.gdata.data.extensions.FullName;
import com.google.gdata.data.extensions.Im;
import com.google.gdata.data.extensions.Name;
import com.google.gdata.data.extensions.PhoneNumber;
import com.google.gdata.data.extensions.PostCode;
import com.google.gdata.data.extensions.Region;
import com.google.gdata.data.extensions.Street;
import com.google.gdata.data.extensions.StructuredPostalAddress;
// ...
public static ContactEntry createContact(ContactsService myService) {
// Create the entry to insert.
ContactEntry contact = new ContactEntry();
// Set the contact's name.
Name name = new Name();
final String NO_YOMI = null;
name.setFullName(new FullName("Elizabeth Bennet", NO_YOMI));
name.setGivenName(new GivenName("Elizabeth", NO_YOMI));
name.setFamilyName(new FamilyName("Bennet", NO_YOMI))
contact.setName(name);
contact.setContent(new PlainTextConstruct("Notes"));
// Set contact's e-mail addresses.
Email primaryMail = new Email();
primaryMail.setAddress("liz#gmail.com");
primaryMail.setDisplayName("E. Bennet");
primaryMail.setRel("http://schemas.google.com/g/2005#home");
primaryMail.setPrimary(true);
contact.addEmailAddress(primaryMail);
Email secondaryMail = new Email();
secondaryMail.setAddress("liz#example.com");
secondaryMail.setRel("http://schemas.google.com/g/2005#work");
secondaryMail.setPrimary(false);
contact.addEmailAddress(secondaryMail);
// Set contact's phone numbers.
PhoneNumber primaryPhoneNumber = new PhoneNumber();
primaryPhoneNumber.setPhoneNumber("(206)555-1212");
primaryPhoneNumber.setRel("http://schemas.google.com/g/2005#work");
primaryPhoneNumber.setPrimary(true);
contact.addPhoneNumber(primaryPhoneNumber);
PhoneNumber secondaryPhoneNumber = new PhoneNumber();
secondaryPhoneNumber.setPhoneNumber("(206)555-1213");
secondaryPhoneNumber.setRel("http://schemas.google.com/g/2005#home");
contact.addPhoneNumber(secondaryPhoneNumber);
// Set contact's IM information.
Im imAddress = new Im();
imAddress.setAddress("liz#gmail.com");
imAddress.setRel("http://schemas.google.com/g/2005#home");
imAddress.setProtocol("http://schemas.google.com/g/2005#GOOGLE_TALK");
imAddress.setPrimary(true);
contact.addImAddress(imAddress);
// Set contact's postal address.
StructuredPostalAddress postalAddress = new StructuredPostalAddress();
postalAddress.setStreet(new Street("1600 Amphitheatre Pkwy"));
postalAddress.setCity(new City("Mountain View"));
postalAddress.setRegion(new Region("CA"));
postalAddress.setPostcode(new PostCode("94043"));
postalAddress.setCountry(new Country("US", "United States"));
postalAddress.setFormattedAddress(new FormattedAddress("1600 Amphitheatre Pkwy Mountain View"));
postalAddress.setRel("http://schemas.google.com/g/2005#work");
postalAddress.setPrimary(true);
contactOne.addStructuredPostalAddress(postalAddress);
// Ask the service to insert the new entry
URL postUrl = new URL("https://www.google.com/m8/feeds/contacts/default/full");
ContactEntry createdContact = myService.insert(postUrl, contact);
System.out.println("Contact's ID: " + createdContact.getId());
return createdContact;
}
Update Contact
To update a contact, first retrieve the contact entry, modify the data and send an authorized PUT request to the contact's edit URL with the modified contact entry in the body.
The URL is of the form:
https://www.google.com/m8/feeds/contacts/userEmail/full/{contactId}
To ensure that the data sent to the API doesn't overwrite another client's changes, the contact entry's Etag should be provided in the request header.
If-Match: Etag
Upon success, the server responds with an HTTP 200 OK status code and the updated contact entry.
public static ContactEntry updateContactName(
ContactsService myService, URL contactURL)
throws ServiceException, IOException {
// First retrieve the contact to updated.
ContactEntry entryToUpdate = myService.getEntry(contactURL, ContactEntry.class);
entryToUpdate.getName().getFullName().setValue("New Name");
entryToUpdate.getName().getGivenName().setValue("New");
entryToUpdate.getName().getFamilyName().setValue("Name");
URL editUrl = new URL(entryToUpdate.getEditLink().getHref());
try {
ContactEntry contactEntry = myService.update(editUrl, entryToUpdate);
System.out.println("Updated: " + contactEntry.getUpdated().toString());
return contactEntry;
} catch (PreconditionFailedException e) {
// Etags mismatch: handle the exception.
}
return null;
}
Delete Contact
To delete a contact, send an authorized DELETE request to the contact's edit URL.
The URL is of the form:
https://www.google.com/m8/feeds/contacts/{userEmail}/full/{contactId}
To ensure that the data sent to the API doesn't overwrite another client's changes, the contact entry's Etag should be provided in the request header.
If-Match: Etag
Upon success, the server responds with an HTTP 200 OK status code.
public static void deleteContact(ContactsService myService, URL contactURL)
throws ServiceException, IOException {
// Retrieving the contact is required in order to get the Etag.
ContactEntry contact = myService.getEntry(contactURL, ContactEntry.class);
try {
contact.delete();
} catch (PreconditionFailedException e) {
// Etags mismatch: handle the exception.
}
}
while People API:
The People API lets you list authenticated users' Contacts and retrieve profile information for authenticated users and their contacts.
For example, let's say the authenticated user, Jen, has Fabian and Ranjith in her private contacts. When your app calls people.connections.list to retrieve a list of her connections, Jen is presented with a consent screen asking to give the app access to the list. If Jen consents, the app retrieves a list containing Fabian and Ranjith (with a resource name for each person). The app can then call people.get, passing in a resource name, to get private contact and public profile data for each person.

Google Directory API returns Not Authorized when call users().list().execute()

I need to read the list of users (and groups) from my google domain.
So I went to my Google APIs Console and enabled Admin SDK and created a Service Account to call Google APIs. I use the following google libraries
google-api-services-admin-directory_v1-rev11-1.16.0-rc.jar
google-api-client-1.16.0-rc.jar
My code is
/*
* Global instance of the JSON factory.
*/
final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
/*
Global instance of the HTTP transport.
*/
HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
Collection<String> scopeList = new ArrayList<>();
scopeList.add(DirectoryScopes.ADMIN_DIRECTORY_USER);
scopeList.add(DirectoryScopes.ADMIN_DIRECTORY_GROUP);
scopeList.add(DirectoryScopes.ADMIN_DIRECTORY_GROUP_MEMBER);
GoogleCredential credential = new GoogleCredential.Builder()
.setTransport(httpTransport)
.setJsonFactory(JSON_FACTORY)
.setServiceAccountId("nnnnnn#developer.gserviceaccount.com")
.setServiceAccountScopes(scopeList)
.setServiceAccountPrivateKeyFromP12File(new File("/Path/To/KeyFile/nnnnn-privatekey.p12"))
// .setServiceAccountUser("admin#mydomain.org")
.build();
Directory admin = new Directory.Builder(httpTransport, JSON_FACTORY, credential)
.setApplicationName("Test")
.setHttpRequestInitializer(credential).build();
Users users = admin.users().list().setDomain("mydomain.org").execute();
And I receive this on the last line
Error
{
"code" : 403,
"errors" : [ {
"domain" : "global",
"message" : "Not Authorized to access this resource/api",
"reason" : "forbidden"
} ],
"message" : "Not Authorized to access this resource/api"
}
at com.google.api.client.googleapis.json.GoogleJsonResponseException.from(GoogleJsonResponseException.java:145)
at com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest.newExceptionOnError(AbstractGoogleJsonClientRequest.java:113)
at com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest.newExceptionOnError(AbstractGoogleJsonClientRequest.java:40)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest$1.interceptResponse(AbstractGoogleClientRequest.java:312)
at com.google.api.client.http.HttpRequest.execute(HttpRequest.java:1045)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.executeUnparsed(AbstractGoogleClientRequest.java:410)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.executeUnparsed(AbstractGoogleClientRequest.java:343)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.execute(AbstractGoogleClientRequest.java:460)
If I uncomment the commented line (.setServiceAccountUser("admin#mydomain.org")) then I get a different error
Exception in thread "main" com.google.api.client.auth.oauth2.TokenResponseException: 400 Bad Request
{
"error" : "access_denied"
}
at com.google.api.client.auth.oauth2.TokenResponseException.from(TokenResponseException.java:105)
at com.google.api.client.auth.oauth2.TokenRequest.executeUnparsed(TokenRequest.java:287)
at com.google.api.client.auth.oauth2.TokenRequest.execute(TokenRequest.java:307)
at com.google.api.client.googleapis.auth.oauth2.GoogleCredential.executeRefreshToken(GoogleCredential.java:269)
at com.google.api.client.auth.oauth2.Credential.refreshToken(Credential.java:489)
at com.google.api.client.auth.oauth2.Credential.intercept(Credential.java:217)
at com.google.api.client.http.HttpRequest.execute(HttpRequest.java:858)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.executeUnparsed(AbstractGoogleClientRequest.java:410)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.executeUnparsed(AbstractGoogleClientRequest.java:343)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.execute(AbstractGoogleClientRequest.java:460)
My account (admin#mydomain.org) is a super administrator. I suspect the Service Account needs to be granted access for the users API scope. But I can not find where I can grant this access. I have a classic UI mode of my Google CPanel. And I don't have "Manage client API access" page in the Advanced tools.
Also I'm not sure what I should use as an Application Name at .setApplicationName("Test").
Thanks for any help
you can go to "security" settings in the admin console (admin.google.com/AdminHome?chromeless=1&pli=1#SecuritySettings:); then click on advance settings > Manage third party OAuth Client access. After this map your client id(generated from appconsole code.google.com/apis/console under API access for oath2) and "One or More API Scopes". Use comma separated scopes as mentioned there. For google directory you can use https://www.googleapis.com/auth/admin.directory.group,https://www.googleapis.com/auth/admin.directory.user
Hope after this it works :)
I had the exact same problem, and was stucked on this sample
What helped me :
1/ I did not Delegate domain-wide authority to your service account, as suggested by Jay Lee. But after that, I still had the problem.
2/ Then, according to this post, the call to setServiceAccountUser(yourAdminAccount#yourDomain.com) is mandatory.
Google Directory API works with Compute Engine default service account, you do not need to setup Google Drive domain-wide. The only thing: you have to set serviceAccountUser, which is not supported in JSON based credentials. So you can
use P12 keys
user JSON credentials with workaround:
make credential copy:
import static com.google.api.client.googleapis.util.Utils.getDefaultJsonFactory;
import static com.google.api.client.googleapis.util.Utils.getDefaultTransport;
private static final String APPLICATION_NAME = "AnyAppName";
private final List<String> SCOPES = ImmutableList.of(
DirectoryScopes.ADMIN_DIRECTORY_GROUP_MEMBER, DirectoryScopes.ADMIN_DIRECTORY_USER, DirectoryScopes.ADMIN_DIRECTORY_GROUP);
private Directory service;
#PostConstruct
void init() throws GeneralSecurityException, IOException {
GoogleCredential credential;
try (InputStream is = new FileInputStream("./config/client_secret.json")) {
credential = GoogleCredential.fromStream(is);
}
GoogleCredential credentialWithUser = new GoogleCredential.Builder()
.setTransport(getDefaultTransport())
.setJsonFactory(getDefaultJsonFactory())
.setServiceAccountUser("admin#yourdomain.ru") // <--- mail of domain's admin
.setServiceAccountId(credential.getServiceAccountId())
.setServiceAccountScopes(SCOPES)
.setServiceAccountPrivateKey(credential.getServiceAccountPrivateKey())
.setServiceAccountPrivateKeyId(credential.getServiceAccountPrivateKeyId())
.setTokenServerEncodedUrl(credential.getTokenServerEncodedUrl()).build();
service = new Directory.Builder(getDefaultTransport(), getDefaultJsonFactory(), credentialWithUser).setApplicationName(APPLICATION_NAME).build();
}
public void members() throws IOException {
Members members = service.members().list("groupName#yourdomain.ru").execute();
System.out.println(members);
}
For my trial G Suite account it works!
You can grant the service account access to certain scopes in the Control Panel as explained in the Google Drive domain-wide documentation. Just use Admin SDK scopes instead.
The application name is used in the User-Agent header of requests and so is not overly important, just use your apps name and maybe version.

Resources