New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(cloudclient): support id_token with impersonated_service_account type #273
feat(cloudclient): support id_token with impersonated_service_account type #273
Conversation
} | ||
logger.Sugar().Warnf("%s. Using Application Default Credentials to fetch id_token again. "+ | ||
"This should never happen in prod env.", err.Error()) | ||
gts, err := google.DefaultTokenSource(ctx) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand what the idea is here but have we thought about the implications of making it easier to run backend services as user identities? The effective permissions the service gets will vary depending on the permissions of the user. What use case is this solving?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to develop a backend service by running it locally and using dev_env database and other downstream services in dev_env. I guess the user impersonation with some specific service account can allow us to do local development much easier and no need for encrypted keys anywhere.
Just some back ground: As we use CloudSQL IAM user, so in dev env, we have a service A with service_account SA connects to a postgres and also calling onto service B.
And if I want to locally run a copy of service A which can also connect to dev env's postgres and service B, the simplest way seems to just impersonate my local principal employee account as SA and load the service up locally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we have security concerns, I think perhaps one way to solve this is to find a way to make sure we don't have Service Account Token Creator
role on SAs used in production environment. I had to add this Service Account Token Creator
role on the dev env SA that I was impersonating to make it work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@liufuyang After our discussion at the backend guild, I understand much better! So we assume that we are running this in combination with SA impersonation, so that the DefaultTokenSource
will generate impersonated SA tokens instead of personal identity tokens?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I think that is the idea :)
So basically one would need to do:
gcloud --impersonate-service-account xxx@xxx.iam.gserviceaccount.com auth application-default login
, (cat the local application_default_credentials.json key file seeing"type": "impersonated_service_account"
)
And then load the service locally up by setting the GOOGLE_APPLICATION_CREDENTIALS
env to the application_default_credentials.json file.
Then, without this change, one will see an error such as:
idtoken: credential must be service_account, found "impersonated_service_account"
Currently, I made the code to allow both authorized_user
and impersonated_service_account
to pass but apparently I can change it to only allow impersonated_service_account
to pass as this for sure scopes the access right onto that impersonated account, so feels more aligned with what we need and not giving extra privliges.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it better to do this on the client side and pass the proper token there?
Also have you tried doing local calls to the service you tested? I recently attempted this on the client side and it failed because Google washes away the signature of a user token (even with this workaround) and the token can therefore not be used to call other Google services.
I have tested this locally and it works 😆 (And I have to call the service with a token that is generated also from the impersonated service_account) |
84bdbdc
to
0e7c93a
Compare
0e7c93a
to
5363c01
Compare
@@ -21,8 +25,28 @@ func DialService(ctx context.Context, target string, opts ...grpc.DialOption) (* | |||
audience := "https://" + trimPort(target) | |||
idTokenSource, err := idtoken.NewTokenSource(ctx, audience, option.WithAudiences(audience)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO now that we do much more stuff we should factor this out to a newTokenSource
method or similar that configures a token source
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am new to Goland and also the code base is new, probably need some guidance on how to do this exactly if we need the change in this PR :)
5363c01
to
a2ef119
Compare
@alethenorio Forgot to answer your first question - this cloudrunner dail is called when the services loads up, without the suggested change in this PR, the service just stops and gave error log such as:
So without a runnable service locally, we cannot test any token passing from the client side :) |
a2ef119
to
4e62a45
Compare
cloudclient/dial.go
Outdated
// Related issue page: https://github.com/googleapis/google-api-go-client/issues/873 | ||
defaultCred, defaultCredErr := google.FindDefaultCredentials(ctx) | ||
if defaultCredErr == nil { | ||
var credMap map[string]interface{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in Go it's usually better to unmarshal JSON into a struct, when you want a specific field:
var credential struct {
Type string `json:"type"`
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aha, that is nice.
cloudclient/dial.go
Outdated
return nil, err | ||
} | ||
|
||
logger.Sugar().Warnf("%s. Using Application Default Credentials to fetch id_token again. "+ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO avoid the sugared API, it doesn't add much readability over the default API, and that's what's used most commonly
839f584
to
495f1f5
Compare
cloudclient/dial.go
Outdated
@@ -16,13 +21,59 @@ import ( | |||
"google.golang.org/grpc/keepalive" | |||
) | |||
|
|||
type credTypeHolder struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since this is only used in one place, please use this as an inline var
type like I showed in the example
495f1f5
to
0685e8f
Compare
|
||
logger.Warn( | ||
"Using Application Default Credentials to fetch id_token again. This should never happen in prod env.", | ||
zap.String("ignored_error", err.Error()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: use camelCase for field names since this generates JSON - and prefer zap.Error
for errors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I missed this, will create a patch to update it then :)
While trying to run a backend service locally (with SA impersonation) and calling other internal backend services, I encountered an error like this:
While google's
google-api-go-client
is not supporting using Application Default Credentials (via the employee account auth), there seems to be a workaround mentioned in the issue and here we basically implement the same.Tested this locally while using it and it seems working correctly.