-
Notifications
You must be signed in to change notification settings - Fork 258
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: Add AWS Workload Identity Federation Support #386
Changes from 4 commits
5dbc165
6935145
ce07c11
a89e880
94e1935
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
# Copyright 2022 Google, Inc. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
require "os" | ||
|
||
module Google | ||
# Module Auth provides classes that provide Google-specific authorization | ||
# used to access Google APIs. | ||
module Auth | ||
# BaseClient is a class used to contain common methods that are required by any | ||
# Credentials Client, including AwsCredentials, ServiceAccountCredentials, | ||
# and UserRefreshCredentials. This is a superclass of Signet::OAuth2::Client | ||
# and has been created to create a generic interface for all credentials clients | ||
# to use, including ones which do not inherit from Signet::OAuth2::Client. | ||
module BaseClient | ||
AUTH_METADATA_KEY = :authorization | ||
|
||
# Updates a_hash updated with the authentication token | ||
def apply! a_hash, opts = {} | ||
# fetch the access token there is currently not one, or if the client | ||
# has expired | ||
fetch_access_token! opts if needs_access_token? | ||
a_hash[AUTH_METADATA_KEY] = "Bearer #{send token_type}" | ||
end | ||
|
||
# Returns a clone of a_hash updated with the authentication token | ||
def apply a_hash, opts = {} | ||
a_copy = a_hash.clone | ||
apply! a_copy, opts | ||
a_copy | ||
end | ||
|
||
# Whether the id_token or access_token is missing or about to expire. | ||
def needs_access_token? | ||
send(token_type).nil? || expires_within?(60) | ||
end | ||
|
||
# Returns a reference to the #apply method, suitable for passing as | ||
# a closure | ||
def updater_proc | ||
proc { |a_hash, opts = {}| apply a_hash, opts } | ||
end | ||
|
||
def on_refresh &block | ||
@refresh_listeners = [] unless defined? @refresh_listeners | ||
@refresh_listeners << block | ||
end | ||
|
||
def notify_refresh_listeners | ||
listeners = defined?(@refresh_listeners) ? @refresh_listeners : [] | ||
listeners.each do |block| | ||
block.call self | ||
end | ||
end | ||
|
||
def expires_within? | ||
raise "This method must be implemented by a subclass" | ||
end | ||
|
||
private | ||
|
||
def token_type | ||
raise "This method must be implemented by a subclass" | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
# Copyright 2015 Google, Inc. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
require "time" | ||
require "googleauth/credentials_loader" | ||
require "googleauth/external_account/aws_credentials" | ||
|
||
module Google | ||
# Module Auth provides classes that provide Google-specific authorization | ||
# used to access Google APIs. | ||
module Auth | ||
# Authenticates requests using External Account credentials, such | ||
# as those provided by the AWS provider. | ||
class ExternalAccountCredentials | ||
attr_reader :project_id | ||
attr_reader :quota_project_id | ||
|
||
# Create a ExternalAccountCredentials | ||
# | ||
# @param json_key_io [IO] an IO from which the JSON key can be read | ||
# @param scope [string|array|nil] the scope(s) to access | ||
def self.make_creds options = {} | ||
json_key_io, scope = options.values_at :json_key_io, :scope | ||
|
||
raise "a json file is required for external account credentials" unless json_key_io | ||
user_creds = read_json_key json_key_io | ||
|
||
Google::Auth::ExternalAccount::AwsCredentials.new( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are a few different types of external account credential files that exist right now. Let's make sure the file is intended to be AWS credentials before we use that class to create them, and raise an "unknown credential" error until we've implemented them. Use either of these methods: In GoLang, we make sure that the environment ID of the credential source is "AWS1" before we create AWS credentials (and could be AWS2, AWS3, etc for future versions of these credentials) In Python, we make sure that the subject token type of the credential source is "urn:ietf:params:aws:token-type:aws4_request" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 994456a |
||
audience: user_creds["audience"], | ||
scope: scope, | ||
subject_token_type: user_creds["subject_token_type"], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The token_url and service_account_impersonation_url need to be validated. See the java implementation: https://github.com/googleapis/google-auth-library-java/blob/main/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java#L219 |
||
token_url: user_creds["token_url"], | ||
credential_source: user_creds["credential_source"], | ||
service_account_impersonation_url: user_creds["service_account_impersonation_url"] | ||
) | ||
end | ||
|
||
# Reads the required fields from the JSON. | ||
def self.read_json_key json_key_io | ||
json_key = MultiJson.load json_key_io.read | ||
wanted = [ | ||
"audience", "subject_token_type", "token_url", "credential_source" | ||
] | ||
wanted.each do |key| | ||
raise "the json is missing the #{key} field" unless json_key.key? key | ||
end | ||
json_key | ||
end | ||
end | ||
end | ||
end |
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.
Do we need this module? Is there a reason that ExternalAccountCredentials can't extend from Signet::Oauth2::Client like the other credential classes in this library, instead of selecting a few of the methods from this module, and creating a superclass for it? It seems like it would just be easier to overwrite the methods from the client that don't 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.
My thinking here was that the ExternalAccountCredentials uses a completely different OAuth2 flow that is not supported by Signet. Since Signet doesn't support token exchange I figured it would just add confusion for other developers if I pulled in Signet and didn't use any of it's features. The current implementation is the most clear way i could think of to avoid confusion around what Signet does and doesn't do while still DRYing everything up. If you would prefer I just paste everything into the Signet file and include it everywhere I can go ahead and make that change and try it out, just figured I would explain my thinking and get your feedback before making the change.
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 think this makes sense. Let's mention Signet::Oauth2::Client in the comments, and mention that this has been created as a superclass for Signet for Credentials that don't need all of Signet's features
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.
Is this more reasonable? a89e880