Google-api-dotnet-client: Provide a way to add scopes to ServiceAccountCredential

Created on 21 Aug 2020  路  7Comments  路  Source: googleapis/google-api-dotnet-client

After multiple attempts, I finally was able to connect to a g-suite account.

Many tutorials online suggested using below code:

private const string FileName = "service_account.json";
private const string User = "[email protected]";
private const string ApplicationName = "My Test App";
private static readonly string[] Scopes = { GmailService.ScopeConstants.MailGoogleCom, GmailService.Scope.GmailSettingsBasic }


using (var stream = new FileStream(FileName, FileMode.Open, FileAccess.Read))
{
    credentials = ServiceAccountCredential.FromServiceAccountData(stream);
}

but when trying to access my mailbox I got errors all the time.
Finally I initialized ServiceAccountCredentials like this:

credentials = GoogleCredential.FromFile(FileName)
                     .CreateScoped(Scopes)
                     .CreateWithUser(User)
                     .UnderlyingCredential as ServiceAccountCredential;

But this doesn't feel right.
Ideally, I'd like to be able to load service account credentials from a file, assign scopes, and user (if needed), but there is no API to do that on ServiceAccountCredential.

This would be the ideal way:

using (var stream = new FileStream(FileName, FileMode.Open, FileAccess.Read))
{
    credentials = ServiceAccountCredential
                         .FromServiceAccountData(stream)
                         .WithScopes(Scopes)
                         .WithUser(User);
}

If there is a better way please let me know.

question

All 7 comments

Could you clarify why you need the ServiceAccountCredential at all? For most use cases, using the GoogleCredential is fine, so you'd have:

var credentials = GoogleCredential.FromFile(FileName)
    .CreateScoped(Scopes)
    .CreateWithUser(User);

That looks better to me than your final snippet:

  • It's shorter, and expresses what you care about (loading a credential from a file and giving it scopes and a user) rather than the implementation details of using FileStream)
  • It's a single expression, so you can declare and initialize the variable in one go
  • You don't need to worry about the using statement to close the stream - it's done for you

If you do need a ServiceAccountCredential for some reason, I'd personally use the middle version, although with a cast instead of using as - the reasons in the list above outweigh the slight "wrapper" disadvantage of going via GoogleCredential in my view.

Could you clarify why you need the ServiceAccountCredential at all

I'm creating an Azure Function that will interact with a couple of mailboxes on a g-suite subscription.
The admin created an app and a service account for me, so I thought that I need to use ServiceAccountCredential.
I've just checked my POC app, and code works perfectly with GoogleCredentials

The problem was that in my first version I didn't add CreateWithUser(User) when creating credentials, so when I requested some info about a specific user I got errors.

var credentials = GoogleCredential.FromFile(FileName).CreateScoped(Scopes);

var gmailService = new GmailService(
    new BaseClientService.Initializer()
    {
        ApplicationName = ApplicationName,
        HttpClientInitializer = credentials
    });

var x = gmailService.Users.Settings.GetVacation(User);
var y = await x.ExecuteAsync();

Console.WriteLine(y.ResponseSubject);

and give me an error:

Google.GoogleApiException
HResult=0x80131500
Message=Google.Apis.Requests.RequestError
Precondition check failed. [400]
Errors [
Message[Precondition check failed.] Location[ - ] Reason[failedPrecondition] Domain[global]
]

Source=Google.Apis
StackTrace:
at Google.Apis.Requests.ClientServiceRequest1.<ParseResponse>d__31.MoveNext() in C:\Apiary\2020-07-09.14-48-36\Src\Support\Google.Apis\Requests\ClientServiceRequest.cs:line 250 at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Google.Apis.Requests.ClientServiceRequest1.d__27.MoveNext() in C:\Apiary\2020-07-09.14-48-36\Src\Support\Google.Apis\Requests\ClientServiceRequest.cs:line 213
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Google.Apis.Requests.ClientServiceRequest1.<ExecuteAsync>d__26.MoveNext() in C:\Apiary\2020-07-09.14-48-36\Src\Support\Google.Apis\Requests\ClientServiceRequest.cs:line 203 at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter1.GetResult()
at GmailVacations.Program.

d__4.MoveNext() in C:\Users\tj\source\repos\GmailVacations\Program.cs:line 69
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at GmailVacations.Program.
(String[] args)

That was the main reason why I moved to ServiceAccountCredential, but it wasn't needed.
After adding CreateWithUser(User); the code works for a single mailbox (the one I specified in CreateWithUser)

Now I need to find a way to create a single GmailService object that will interact with multiple mailboxes.
Is this even doable?

Okay, that sounds like a different question at that point - and one which would be better asked of the GMail team. It's possible that there's some permission you can give to the service account in order to access multiple user accounts without impersonation, but I don't know.

What you could do is keep the result of GoogleCredential.FromFile(FileName).CreateScoped(Scopes) cached somewhere, so you just need to call CreateWithUser each time you need to access a different user. I believe that you'd still need a separate GmailService object each time though. @amanda-tarafa is that right, or does your recent work allow the credentials to be effectively overridden on a per-request basis?

@jskeet thanks for the reply.
The idea was to create a single GoogleCredential with scopes (but without user impersonation) and a single GmailService for it.

Right now I'm updating vacation setting like this:

var req = gmailService.Users.Settings.UpdateVacation(settings, "me").Execute();

but with not impersonated GoogleCredential and GmailService I could call this:

var req1 = gmailService.Users.Settings.UpdateVacation(settings, "[email protected]").Execute();
var req2 = gmailService.Users.Settings.UpdateVacation(settings, "[email protected]").Execute();

or ideally, batch those requests into one.

Again, that's really a question for the Gmail API team. The maintainers of this repository don't know the details of every single Google API - we're focused on making sure you can represent API calls accurately from C#.

I suggest you follow one of the routes on the Gmail API support page to get API-specific help. I wouldn't be at all surprised if this were infeasible though - that you really need to use impersonation in order to access account-specific aspects via the API.

As @jskeet suggested, for something like you are describing in your last comment, you should confirm with the Gmail API team. I strongly suspect you'll need impersonation though and thus a different credential per call. You won't need to have multiple GmailServices though, you can do something as follows:

// No credentials in the service client. You can keep just one service client.
GmailService gmailService = new GmailService(new BaseClientService.Initializer
{
    ApplicationName = ApplicationName
});

// Scoped credential, no impersonation. You can keep this cached.
GoogleCredential baseCredential = GoogleCredential.FromFile(FileName).CreateScoped(Scopes);

// Impersonated credentials. You can keep these cached or create them on the fly.
GoogleCredential userA = baseCredential.CreateWithUser("[email protected]");
GoogleCredential userB = baseCredential.CreateWithUser("[email protected]");

// Create and execute a request with a per-call credential for eahc user
var result1 = gmailService.Users.Settings.UpdateVacation(settings, "me").AddCredential(userA).Execute();
var result2 = gmailService.Users.Settings.UpdateVacation(settings, "me").AddCredential(userB).Execute();

I believe that batching should work as well, although I haven't tried it. But as far as I know, batching will use the credential set on each individual requests, if any, over one set at the batch request level. So, something like this should work:

// Create a request with a per-call credential for eahc user
var req1 = gmailService.Users.Settings.UpdateVacation(settings, "me").AddCredential(userA);
var req2 = gmailService.Users.Settings.UpdateVacation(settings, "me").AddCredential(userB);

var batch = new BatchRequest(gmailService);

// Add the requests to the batch
batch.Queue(req1, callback);
batch.Queue(req2, callback);

await batch.ExecuteAsync();

Closing as we seem to have addressed all your questions.

Was this page helpful?
0 / 5 - 0 ratings