What I'm trying to accomplish:
I have been working the past couple days to implement automation into my company's GoogleApps setup. I need to suspend users that have been inactive for 90+ days, deleting them a week after that. Before deletion, I need to backup any files found in their Google Drive.
Why I haven't accomplished it:
I have attempted this with a few different methods. First I was using the Admin SDK to interface with DirectoryServices, listing users, and suspending as necessary. This interfacing would also allow me to delete users when needed. Now, I need only backup their Google Drive files. For this, it's to my understanding that I need a Service Account to impersonate users, download their files, and relocate them (off to a server in bulk storage probably). I don't think this is possible as an Admin programatically because I believe UserCredentials requires user authorization for each user I intend to interface with via the Drive API, manually. On the other hand, a service account should allow me to impersonate users without authorization, provided the proper delegation.
I have created a ServiceAccount, provided it with Domain-Wide-Delegation, enabled the appropriate APIs (Admin SDK, Drive API) through the developer console, and provided the service account with appropriate scope through Google Admin Console security settings. I think that's all the setup I needed to do.
private static async Task<bool> AuthenticateServiceAccount(string serviceAccountEmail, string keyFilePath)
{
if (!File.Exists(keyFilePath)) // make sure the file we're looking for actually exists
{
Console.WriteLine("Could not find ServAcct key file. Authentication failed!");
return false;
}
string[] scopes = new string[] { DirectoryService.Scope.AdminDirectoryUser, DriveService.Scope.Drive };
// Generate certificate using our key file
var certificate = new X509Certificate2(keyFilePath, "notasecret", X509KeyStorageFlags.Exportable);
try
{
// Create our credential from our certificate, using scopes
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
Scopes = scopes
}.FromCertificate(certificate));
// Create our services
dirSvc = new DirectoryService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "GoogleAppAutomation"
});
drvSvc = new DriveService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "GoogleAppAutomation"
});
} catch(Exception e)
{
Console.WriteLine(e.InnerException);
return false;
}
return true;
}
I am able to authenticate the service account in my code using the SA's email, and the P12 cert. Creation of the DirectoryService and DriveService objects using the ServiceAccountCredential works fine. However, when i go to access the services, I am presented with errors that I have failed to find any help for online.
DirectoryService
// Acquire list of active users
UsersResource.ListRequest req = dirSvc.Users.List();
req.Customer = "my_customer";
req.MaxResults = 500; // 500 is max allowed. Must use paging (.pageToken) for more
req.OrderBy = UsersResource.ListRequest.OrderByEnum.Email;
IList<User> users;
try
{
users = req.Execute().UsersValue; // Execute request for user list
}
catch (Exception e)
{
throw new Exception("User List Exception", e);
}
Sure enough, the catch block will throw an exception containing:
Google.Apis.Requests.RequestError
Domain not found. [404]
Errors [
Message[Domain not found.] Location[ - ] Reason[notFound] Domain[global]
]
DriveService
FilesResource.ListRequest lr = drvSvc.Files.List();
IList<Google.Apis.Drive.v2.Data.File> fl = lr.Execute().Items;
foreach (Google.Apis.Drive.v2.Data.File file in fl)
{
Console.WriteLine("{0}", file.Title);
}
Just as above, the Execute() method throws an exception...
Google.Apis.Requests.RequestError
Daily Limit for Unauthenticated Use Exceeded. Continued use requires signup. [403]
Errors [
Message[Daily Limit for Unauthenticated Use Exceeded. Continued use requires signup.] Location[ - ] Reason[dailyLimitExceededUnreg] Domain[usageLimits]
]
Looking in the Developer Console, this API has had no use at all in the Quota log.
So, I suppose my question is a very broad: what am I doing wrong? :(
Edit:
I believe the issue pertaining to Google Drive access relates to a lack of Billing setup. I cannot be sure of this as of yet because my company is being very slow to get such a thing setup. If anyone can confirm or deny this for me, that'd help!
As for the Directory Service issue, I'll just use the Admin SDK to access the users for suspension/deletion, and the service account for Drive backup. I already have the Admin SDK working for this purpose, so no further assistance is needed in that regard (though an explanation of the error would be awesome since it doesn't appear to be documented whatsoever online!)
Related
Having spent hours looking for an answer on how to access the Gmail API with the use of a service account and saw that I can't, unless I'm using a GSuite account that it's provided with domain-wide authorization, I came here to ask you if there's a way to actually create labels using the said API and a private account. I'm using Visual Studio 2019 and C#. In the "developers.google.com" there's a tool called "Try this API" and I can create a label using my OAuth 2.0 just fine, and the .NET Quickstart found here also works in listing my labels. But why can't it let me create labels? I have enabled all of the scopes possible for this to work.
This is the error I am getting:
"Google.GoogleApiException: 'Google.Apis.Requests.RequestError
Request had insufficient authentication scopes. [403]
Errors [
Message[Insufficient Permission] Location[ - ] Reason[insufficientPermissions] Domain[global]" enter image description here
The method Lables.create requires permissions in order to create labels on the users account. The user must have consented to that permission.
the error message
Google.Apis.Requests.RequestError Request had insufficient authentication scopes.
Is telling you that the user has not consented to the proper scope. The user must have consented to one of the following scopes
If you followed the quick start then you probably only included GmailService.Scope.GmailReadonly. You will need to change the scope and request authorization of the user again. Note that the tutorial you are following is not for service account authencation but rather for Oauth2 authentication.
service account
string ApplicationName = "Gmail API .NET Quickstart";
const string serviceAccount = "xxxxx-smtp#xxxxx-api.iam.gserviceaccount.com";
var certificate = new X509Certificate2(#"c:\xxxxx-api-ed4859a67674.p12", "notasecret", X509KeyStorageFlags.Exportable);
var gsuiteUser = "xxxxx#xxxx.com";
var serviceAccountCredentialInitializer = new ServiceAccountCredential.Initializer(serviceAccount)
{
User = gsuiteUser,
Scopes = new[] { GmailService.Scope.GmailSend, GmailService.Scope.GmailLabels }
}.FromCertificate(certificate);
var credential = new ServiceAccountCredential(serviceAccountCredentialInitializer);
if (!credential.RequestAccessTokenAsync(CancellationToken.None).Result)
throw new InvalidOperationException("Access token failed.");
var service = new GmailService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = ApplicationName,
});
My web service is currently doing basic username/password authentication in order to subscribe the exchange user for receiving the events (like new mail event etc) like below:
var service = new ExchangeService(exchangeVersion)
{
KeepAlive = true,
Url = new Uri("some autodiscovery url"),
Credentials = new NetworkCredential(username, password)
};
var subscription = service.SubscribeToPushNotifications(
new[] { inboxFolderFoldeID },
new Uri("some post back url"),
15,
null,
EventType.NewMail,
EventType.Created,
EventType.Deleted,
EventType.Modified,
EventType.Moved,
EventType.Copied);
Now, I am supposed to replace the authentication mechanism to use OAuth protocol. I saw some examples but all of them seem to be talking about authenticating the client (https://msdn.microsoft.com/en-us/library/office/dn903761%28v=exchg.150%29.aspx?f=255&MSPPError=-2147217396) but nowhere I was able to find an example of how to authenticate an exchange user with OAuth protocol. Any code sample will help a lot. Thanks.
It's not clear what you mean with 'web service' and how you currently get the username and password. If that is some kind of website where the user needs to login or pass credentials, then you'll have to start an OAuth2 grant from the browser as in redirecting the clients browser to the authorize endpoint to start implicit grant or code grant. The user will be presented a login screen on the OAuth2 server (and not in your application), once the user logs in a code or access token (depending on the grant) will be returned to your application which you can use in the ExchangeService constructor.
If that 'web' service is some service that runs on the users computer you can use one of the methods described below.
Get AccessToken using AuthenticationContext
The example seems to be based on an older version of the AuthenticationContext class.
The other version seems to be newer, also the AcquireToken is now renamed to AcquireTokenAsync / AcquireTokenSilentAsync.
No matter which version you're using, you will not be able to pass username and password like you're doing in your current code. However, you can let the AcquireToken[Async] method prompt for credentials to the user. Which, let's be honest, is more secure then letting your application deal with those user secrets directly. Before you know, you'll be storing plain text passwords in a database (hope you aren't already).
In both versions, those methods have a lot of overloads all with different parameters and slightly different functionality. For your use-case I think these are interesting:
New: AcquireTokenAsync(string, string, Uri, IPlatformParameters) where IPlatformParameters could be new PlatformParameters(PromptBehavior.Auto)
Old: AcquireToken(string, string, Uri, PromptBehavior where prompt behavior could be PromptBehavior.Auto
Prompt behavior auto, in both vesions, means: the user will be asked for credentials when they're not already cached. Both AuthenticationContext constructors allow you to pass a token-cache which is something you can implement yourself f.e. to cache tokens in memory, file or database (see this article for an example file cache implementation).
Get AccessToken manually
If you really want to pass in the user credentials from code without prompting the user, there is always a way around. In this case you'll have to implement the Resource Owner Password Credentials grant as outlined in OAuth2 specificatioin / RFC6749.
Coincidence or not, I have an open-source library called oauth2-client-handler that implements this for use with HttpClient, but anyway, if you want to go this route you can dig into that code, especially starting from this method.
Use Access Token
Once you have an access token, you can proceed with the samples on this MSDN page, f.e.:
var service = new ExchangeService(exchangeVersion)
{
KeepAlive = true,
Url = new Uri("some autodiscovery url"),
Credentials = new OAuthCredentials(authenticationResult.AccessToken))
};
In case someone is still struggling to get it to work. We need to upload a certificate manifest on azure portal for the application and then use the same certificate to authenticate the client for getting the access token. For more details please see: https://blogs.msdn.microsoft.com/exchangedev/2015/01/21/building-daemon-or-service-apps-with-office-365-mail-calendar-and-contacts-apis-oauth2-client-credential-flow/
Using the example code in this Microsoft Document as the starting point and these libraries:
Microsoft Identity Client 4.27
EWS Managed API v2.2
I am able to successfully authenticate and connect with Exchange on Office 365.
public void Connect_OAuth()
{
var cca = ConfidentialClientApplicationBuilder
.Create ( ConfigurationManager.AppSettings[ "appId" ] )
.WithClientSecret( ConfigurationManager.AppSettings[ "clientSecret" ] )
.WithTenantId ( ConfigurationManager.AppSettings[ "tenantId" ] )
.Build();
var ewsScopes = new string[] { "https://outlook.office365.com/.default" };
AuthenticationResult authResult = null;
try
{
authResult = cca.AcquireTokenForClient( ewsScopes ).ExecuteAsync().Result;
}
catch( Exception ex )
{
Console.WriteLine( "Error: " + ex );
}
try
{
var ewsClient = new ExchangeService();
ewsClient.Url = new Uri( "https://outlook.office365.com/EWS/Exchange.asmx" );
ewsClient.Credentials = new OAuthCredentials( authResult.AccessToken );
ewsClient.ImpersonatedUserId = new ImpersonatedUserId( ConnectingIdType.SmtpAddress, "ccc#pppsystems.co.uk" );
ewsClient.HttpHeaders.Add( "X-AnchorMailbox", "ccc#pppsystems.co.uk" );
var folders = ewsClient.FindFolders( WellKnownFolderName.MsgFolderRoot, new FolderView( 10 ) );
foreach( var folder in folders )
{
Console.WriteLine( "" + folder.DisplayName );
}
}
catch( Exception ex )
{
Console.WriteLine( "Error: " + ex );
}
}
The Microsoft example code did not work - the async call to AcquireTokenForClient never returned.
By calling AcquireTokenForClient in a separate try catch block catching a general Exception, removing the await and using .Result, this now works - nothing else was changed.
I realise that this is not best practice but, both with and without the debugger, the async call in the original code never returned.
In the Azure set-up:
A client secret text string was used - a x509 certificate was not necessary
The configuration was 'app-only authentication'
Hope this helps someone avoid hours of frustration.
I am attempting to transfer ownership from a Service Account created document to another user who resides within the same Google Apps account using the code below but am getting the following error
The resource body includes fields which are not directly writable. [403]
Errors [Message[The resource body includes fields which are not directly writable.] Location[ - ] Reason[fieldNotWritable] Domain[global]]
var service = GetService();
try
{
var permission = GetPermission(fileId, email);
permission.Role = "owner";
var updatePermission = service.Permissions.Update(permission, fileId, permission.Id);
updatePermission.TransferOwnership = true;
return updatePermission.Execute();
}
catch (Exception e)
{
Console.WriteLine("An error occurred: " + e.Message);
}
return null;
Commenting out // permission.Role = "owner"; returns the error below
The transferOwnership parameter must be enabled when the permission role is 'owner'. [403] Errors [Message[The transferOwnership parameter must be enabled when the permission role is 'owner'.] Location[transferOwnership - parameter] Reason[forbidden] Domain[global]]
Assigning any other permissions works fine. Therefore, is this a limitation of the Service Account not being able to transfer ownership to any other account that doesn't use the #gserviceaccount.com email address (i.e. our-project#appspot.gserviceaccount.com > email#domain.com)?
The email#domain.com email address has been created and is managed within Google Apps.
In the case, it is not achievable, any pointers on where to look next? We need multiple users to have the ability to create documents ad hoc and assign permissions and transfer ownership on the fly via the API.
Thanks
I have found the answer and am posting for anyone else who comes across this question.
You can not use the 'Service Account Key JSON file' as recommended by Google.
You need to use the p.12 certificate file for authentication.
The code to create a drive service for mimicking accounts is as follows.
public DriveService GetService(string certificatePath, string certificatePassword, string googleAppsEmailAccount, string emailAccountToMimic, bool allowWrite = true)
{
var certificate = new X509Certificate2(certificatePath, certificatePassword, X509KeyStorageFlags.Exportable);
var credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(googleAppsEmailAccount)
{
Scopes = new[] { allowWrite ? DriveService.Scope.Drive : DriveService.Scope.DriveReadonly },
User = emailAccountToMimic
}.FromCertificate(certificate));
// Create the service.
return new DriveService(new BaseClientService.Initializer
{
HttpClientInitializer = credential,
ApplicationName = ApplicationName
});
}
You need to follow the steps listed here to delegate domain-wide authority to the service account.
Allow 5 to 10 minutes after completing step 4.
You can now create documents under the 'emailAccountToMimic' user which sets them to be the owner during creation.
I don't think it is possible to transfer the ownership from a non-ServiceAccount to a ServiceAccount, vice versa.
If you do that interactively, you will get the below error:
Typically, the document can be created and owned by the users and ownership transfer can be done using their own credentials. You will also have the option to impersonate as the owner if your Service Account is granted with the domain-wide delegation correctly.
Have a really strange issue going on. Trying to implement Google's OAuth 2.0 in a ASP.NET (non-MVC) scenario to Google Calendar API. I see the token response in the storage area, no errors encountered.
Here's the code:
public CalendarService Credential(string sUserID)
{
CalendarService service = new CalendarService();
var folder = #"C:\TEMP\GoogleStorage";
UserCredential credential = null;
string clientId = "{client id is redacted}";
string clientSecret = "{client secret is redacted}";
string[] scopes = new string[] {CalendarService.Scope.Calendar};
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
new ClientSecrets
{
ClientId = clientId,
ClientSecret = clientSecret
},
scopes,
sUserID,
CancellationToken.None,
new FileDataStore(folder)
).Result;
}
service = new CalendarService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Interpose Concept"
});
return service;
}
Here's the series of events:
I hit the above code and get a successful credential and a successful service returned. Google Account A using Chrome gets prompted to allow application to access the calendar. A token in stored at c:\temp\GoogleStorage
I am able to use the returned service to query User A's calendar.
Google Account B (simulating a different user hitting the same web server) using Firefox goes through the same code. A different UserID is passed into Credential based on this new session. Again, there's a successful credential to Google, this time userID is different, but this time no requiring to allow access to their Calendar. I am able to successfully return the service, but the service is for Google Account A's calendar.
I am certain I am missing something along the lines of being able to segregate the different Google Accounts, but I don't see how to separate the different users hitting this web server other than the user parameter in the AuthorAsync call I make. I further don't understand why it would retain the first account's credential.
What's blowing my mind is this seems to be working (first request is good, no errors, can query the calendar), but all subsequent requests seem to be tied to the first credential request that works.
What am I not understanding?
I am tring to write to Freebase using MQLWrite.
I manage to write using the Freebase Query.
I enter the url and get an error saying
Login is Required
I am using C#.
another thing to mention is I do not need a user consent. I created a service account in Google Developer console, and tried using the following code from this url:
https://code.google.com/p/google-api-dotnet-client/source/browse/Plus.ServiceAccount/Program.cs?repo=samples&r=406dd0081ca556a81621b910eac4445e3309ad1e&spec=svn.samples.406dd0081ca556a81621b910eac4445e3309ad1e
public class Program
{
// A known public activity.
private static String ACTIVITY_ID = "z12gtjhq3qn2xxl2o224exwiqruvtda0i";
public static void Main(string[] args)
{
Console.WriteLine("Plus API - Service Account");
Console.WriteLine("==========================");
String serviceAccountEmail = "SERVICE_ACCOUNT_EMAIL_HERE";
var certificate = new X509Certificate2(#"key.p12", "notasecret", X509KeyStorageFlags.Exportable);
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
Scopes = new[] { PlusService.Scope.PlusMe }
}.FromCertificate(certificate));
// Create the service.
var service = new PlusService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Plus API Sample",
});
Activity activity = service.Activities.Get(ACTIVITY_ID).Execute();
Console.WriteLine(" Activity: " + activity.Object.Content);
Console.WriteLine(" Video: " + activity.Object.Attachments[0].Url);
Console.WriteLine("Press any key to continue...");
Console.ReadKey();
}
}
I do not what ACTIVITY_ID means, is that the same as CLIENT_ID?
In the above example they use plus but i want to use Freebase and cannot find the scope property any where in the namespace.
Appreciate any kind help... :)
From documentation:
MQL Write requires Authorization. Make sure your authentication is working correctly.
Your application must use OAuth 2.0 to authorize requests. No other authorization protocols are supported.
Using MQL Write
MQL Write supports legacy developer applications that write to
Freebase. In order to use MQL Write, developers must contact Freebase
and request additional quota using the MQL Write Quota Access Request
form.
I have been able to get access with Public Access key only so far.
// Simple API example
// Public API access = is from developer console its different then OAuth. (its at the bottom)
var service = new FreebaseService(new BaseClientService.Initializer
{
ApplicationName = "Discovery Sample",
ApiKey = "{Public API access}",
});
The Nuget package appears to be missing Google.Apis.Auth which is used for OAuth access. Either the API doesn't support Oauth access which cant be since the documentation states you need Oauth to access the write features. Or there is something wrong with the NuGet package.
Can you try and run your write against this? See if it works or not.