Skip to content
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

Allow using OIDC to fetch api tokens #3716

Merged
merged 78 commits into from
Aug 17, 2023
Merged

Allow using OIDC to fetch api tokens #3716

merged 78 commits into from
Aug 17, 2023

Conversation

segiddins
Copy link
Member

@segiddins segiddins commented Apr 16, 2023

Implements rubygems/rfcs#49

@codecov
Copy link

codecov bot commented Apr 16, 2023

Codecov Report

Merging #3716 (e005ff2) into master (7e19c19) will decrease coverage by 0.01%.
The diff coverage is 98.97%.

@@            Coverage Diff             @@
##           master    #3716      +/-   ##
==========================================
- Coverage   98.92%   98.92%   -0.01%     
==========================================
  Files         229      260      +31     
  Lines        5570     6026     +456     
==========================================
+ Hits         5510     5961     +451     
- Misses         60       65       +5     
Files Changed Coverage Δ
...nts/avo/fields/json_viewer_field/edit_component.rb 0.00% <0.00%> (ø)
app/controllers/concerns/avo_auditable.rb 89.47% <ø> (-0.53%) ⬇️
lib/types/array_of.rb 90.00% <90.00%> (ø)
app/models/oidc/api_key_role.rb 96.96% <96.96%> (ø)
...ontrollers/api/v1/oidc/api_key_roles_controller.rb 97.22% <97.22%> (ø)
app/avo/actions/base_action.rb 96.55% <100.00%> (+0.06%) ⬆️
app/avo/actions/refresh_oidc_provider.rb 100.00% <100.00%> (ø)
app/avo/fields/array_of_field.rb 100.00% <100.00%> (ø)
app/avo/fields/json_viewer_field.rb 100.00% <100.00%> (ø)
app/avo/fields/nested_field.rb 100.00% <100.00%> (ø)
... and 38 more

@rubygems-org-shipit rubygems-org-shipit bot temporarily deployed to oidc-api-token April 19, 2023 21:21 Inactive
@rubygems-org-shipit rubygems-org-shipit bot temporarily deployed to oidc-api-token April 19, 2023 22:00 Inactive
@rubygems-org-shipit rubygems-org-shipit bot temporarily deployed to oidc-api-token April 20, 2023 07:51 Inactive
@rubygems-org-shipit rubygems-org-shipit bot temporarily deployed to oidc-api-token April 20, 2023 19:05 Inactive
@rubygems-org-shipit rubygems-org-shipit bot temporarily deployed to oidc-api-token April 20, 2023 19:27 Inactive
@rubygems-org-shipit rubygems-org-shipit bot temporarily deployed to oidc-api-token April 20, 2023 20:55 Inactive
@rubygems-org-shipit rubygems-org-shipit bot temporarily deployed to oidc-api-token April 27, 2023 14:13 Inactive
@rubygems-org-shipit rubygems-org-shipit bot temporarily deployed to oidc-api-token July 7, 2023 03:22 Inactive
@segiddins segiddins changed the title WIP: Allow using OIDC to fetch api tokens Allow using OIDC to fetch api tokens Jul 19, 2023
@rubygems-org-shipit rubygems-org-shipit bot temporarily deployed to oidc-api-token July 19, 2023 19:44 Inactive
@rubygems-org-shipit rubygems-org-shipit bot temporarily deployed to oidc-api-token July 20, 2023 19:57 Inactive
@segiddins segiddins marked this pull request as ready for review July 20, 2023 19:59
Copy link
Member

@indirect indirect left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there docs somewhere for how to use this, either as a maintainer or as a developer publishing gems? I'm ok merging this without, but I do think we should have docs to point people at when we ask them to help us test it!

Copy link
Member

@jenshenny jenshenny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Put down a few initial questions.

field :issuer, as: :text, link_to_resource: true
field :configuration, as: :nested do
visible_on = %i[edit new]
OIDC::Provider::Configuration.then { (_1.required_attributes + _1.optional_attributes) - fields.map(&:id) }.each do |k|
Copy link
Member

@jenshenny jenshenny Aug 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The create provider page is a bit overwhelming. I can see that this form won't be used often, however could we separate the common required attributes from the optional ones?

As an aside, continuing from the point that this form won't be used often... I feel like the provider model feels very abstract/confusing and felt like hardcoding the provider information would have been fine since the majority would use GH actions and there might a few additional ones. However, the model is created already so transitioning to something more hardcoded might not be worth it.

nit: _1 is nifty, though I would prefer to have it be named so it's more readable.

Screenshot 2023-08-04 at 1 24 54 AM

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jenshenny given this is the admin interface, I'm not too concerned

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed it's not blocking. Just raising that this can be iterated on so other admins are able to create/modify providers more easily when it comes to that point.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The modification is done automatically via an action that fetches from the well-known URLs

@@ -0,0 +1,38 @@
class OIDC::IdToken < ApplicationRecord
belongs_to :api_key_role, class_name: "OIDC::ApiKeyRole", foreign_key: "oidc_api_key_role_id", inverse_of: :id_tokens
belongs_to :provider, class_name: "OIDC::Provider", foreign_key: "oidc_provider_id", inverse_of: :id_tokens
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To my knowledge, if api_key_role belongs to a provider, wouldn't it be redundant (and error prone) to have an id token be associated with a provider as you can access the provider through api key role.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so right now it's there because of the JTI uniqueness validation, but I can work around that

@@ -7,7 +7,7 @@ class ApiKeysController < ApplicationController

def index
@api_key = session.delete(:api_key)
@api_keys = current_user.api_keys
@api_keys = current_user.api_keys.unexpired
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't tested all the way so that the OIDC flow returns a API key yet but I'm guessing that index table will contain OIDC generated API keys and "manually" created keys. To avoid confusion between the two (they work and are created differently from each other), I believe it'll be better to have a section with just "manually" created keys and another section for api keys generated for each api key role (with the expiry date)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they work identically to each other, the only difference is how they are created

for now, im avoiding touching the HTML pages, wanting to wait until we're satisfied with the bones of this and ready to roll it out to GA to do that work

Copy link
Member

@jenshenny jenshenny Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see that the key name is namespaced by the name of the API key role so a user can differentiate it by that. I would want to see these keys under a API key role section at some point where I can see all keys up to the last x days (user can only see keys that are active and can't see soft deleted keys right now, it'll be even better show the api key role to a specific gem push)

Curious if this is what you had in mind.

Eg.

#API Key roles

##<name>
Scopes:
Expiry:
Gems:

<list of keys>

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

honestly, I haven't thought about the HTML yet, my plan is to start on that once this ships

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, sounds great! I think this is necessary enough to be included in the first release and not for GA.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and here it is :)

app/controllers/passwords_controller.rb Show resolved Hide resolved
Copy link
Member

@jenshenny jenshenny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was able to test this out! Super cool to see this in action ✨ Thanks for putting in all this work

@@ -7,7 +7,7 @@ class ApiKeysController < ApplicationController

def index
@api_key = session.delete(:api_key)
@api_keys = current_user.api_keys
@api_keys = current_user.api_keys.unexpired
Copy link
Member

@jenshenny jenshenny Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see that the key name is namespaced by the name of the API key role so a user can differentiate it by that. I would want to see these keys under a API key role section at some point where I can see all keys up to the last x days (user can only see keys that are active and can't see soft deleted keys right now, it'll be even better show the api key role to a specific gem push)

Curious if this is what you had in mind.

Eg.

#API Key roles

##<name>
Scopes:
Expiry:
Gems:

<list of keys>

Use a :through relationship instead
@@ -0,0 +1,18 @@
require "test_helper"

class OIDC::ProviderPolicyTest < ActiveSupport::TestCase
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • fill out test methods

@@ -0,0 +1,18 @@
require "test_helper"

class OIDC::Provider::ConfigurationPolicyTest < ActiveSupport::TestCase
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • fill out test methods

@@ -0,0 +1,18 @@
require "test_helper"

class OIDC::IdTokenPolicyTest < ActiveSupport::TestCase
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • fill out test methods

@@ -0,0 +1,18 @@
require "test_helper"

class OIDC::ApiKeyRolePolicyTest < ActiveSupport::TestCase
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • fill out test methods

@@ -0,0 +1,7 @@
require "test_helper"

class OIDC::IdTokenTest < ActiveSupport::TestCase
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • test the model

@@ -0,0 +1,38 @@
class OIDC::IdToken < ApplicationRecord
belongs_to :api_key_role, class_name: "OIDC::ApiKeyRole", foreign_key: "oidc_api_key_role_id", inverse_of: :id_tokens
belongs_to :provider, class_name: "OIDC::Provider", foreign_key: "oidc_provider_id", inverse_of: :id_tokens
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so right now it's there because of the JTI uniqueness validation, but I can work around that

@@ -7,7 +7,7 @@ class ApiKeysController < ApplicationController

def index
@api_key = session.delete(:api_key)
@api_keys = current_user.api_keys
@api_keys = current_user.api_keys.unexpired
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they work identically to each other, the only difference is how they are created

for now, im avoiding touching the HTML pages, wanting to wait until we're satisfied with the bones of this and ready to roll it out to GA to do that work

app/controllers/passwords_controller.rb Show resolved Hide resolved
@@ -7,7 +7,7 @@ class ApiKeysController < ApplicationController

def index
@api_key = session.delete(:api_key)
@api_keys = current_user.api_keys
@api_keys = current_user.api_keys.unexpired
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

honestly, I haven't thought about the HTML yet, my plan is to start on that once this ships

@rubygems-org-shipit rubygems-org-shipit bot temporarily deployed to oidc-api-token August 4, 2023 23:17 Inactive
@rubygems-org-shipit rubygems-org-shipit bot temporarily deployed to oidc-api-token August 14, 2023 23:34 Inactive
Copy link
Member

@jenshenny jenshenny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some comments regarding the UX of the basic views.

The PR is getting kind of large though. I would be 👍 in merging everything except 73bf7c9 and have another PR for the views. I would also be open to making the enhancements to the views too! Just let me know.

Comment on lines 722 to 724
api_key_roles: OIDC API Key Roles
show:
api_key_role_name: "API Key Role %{name}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
api_key_roles: OIDC API Key Roles
show:
api_key_role_name: "API Key Role %{name}"
api_key_roles: OIDC API key roles
show:
api_key_role_name: "API key role %{name}"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would users understand what an API key role actually is? It reveals too much of the implementation details. I would prefer provider or issuer even though it does conflict with some fields in the model.

Suggested change
api_key_roles: OIDC API Key Roles
show:
api_key_role_name: "API Key Role %{name}"
api_key_roles: API key provider (OIDC)
show:
api_key_role_name: "API key provider %{name}"

<code><%= api_key_role.token %></code>
</td>
<td class="owners__cell" data-title="Provider">
<%= link_to api_key_role.provider.issuer, api_key_role.provider.issuer %>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When this is setup on prod, could we name the issuer Github Actions? is provider.issuer the same as provider.configuration.issuer? If so, could we remove provider.issuer and add a name attribute and store it as Github Actions.

Screenshot 2023-08-14 at 8 16 58 PM

<% @api_key_roles.each do |api_key_role| %>
<tr class="owners__row">
<td class="owners__cell" data-title="Name">
<%= link_to api_key_role.name, profile_oidc_api_key_role_path(api_key_role.token) %>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit hard to tell that the link goes to the show page. Can we add a Details button on the side of each row like the edit button in API keys?
Screenshot 2023-08-14 at 8 25 11 PM

Comment on lines 15 to 17
<div class="push--s">
<code><pre><%= JSON.pretty_generate @api_key_role.api_key_permissions.as_json %></pre></code>
</div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Showing the raw json is a bit hard to read. Why not use the scopes, valid_for and gems from OIDC::ApiKeyPermissions?

Comment on lines 110 to 112
<% if current_user.oidc_api_key_roles.any? %>
<p><%= button_to t("oidc.api_key_roles.index.api_key_roles"), profile_oidc_api_key_roles_path, method: "get", class: "form__submit" %></p>
<% end %>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I would prefer to have the api_key_roles index view on the same page rather than having it separate. The button placement looks a bit off.

Comment on lines 29 to 45
<tr class="owners__row owners__header">
<td class="owners__cell"><%= OIDC::IdToken.human_attribute_name(:created_at) %></td>
<td class="owners__cell"><%= ApiKey.human_attribute_name(:expires_at) %></td>
<td class="owners__cell"><%= OIDC::IdToken.human_attribute_name(:jti) %></td>
<td class="owners__cell"><%= OIDC::IdToken.human_attribute_name(:claims) %></td>
<td class="owners__cell"><%= OIDC::IdToken.human_attribute_name(:header) %></td>
</tr>
</thead>
<tbody class="t-body">
<% @api_key_role.id_tokens.each do |id_token| %>
<tr class="owners__row <%= 'owners__row__invalid' if id_token.api_key.expired? %>">
<td class="owners__cell"><%= id_token.created_at %></td>
<td class="owners__cell"><%= id_token.api_key.expires_at %></td>
<td class="owners__cell"><code><%= id_token.jti %></code></td>
<td class="owners__cell"><code><%= id_token.claims.to_json %></code></td>
<td class="owners__cell"><code><%= id_token.header.to_json %></code></td>
</tr>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty overwhelming. I think we can just get by with expired_at, created_at, and jti.

<tr class="owners__row owners__header">
<td class="owners__cell"><%= OIDC::IdToken.human_attribute_name(:created_at) %></td>
<td class="owners__cell"><%= ApiKey.human_attribute_name(:expires_at) %></td>
<td class="owners__cell"><%= OIDC::IdToken.human_attribute_name(:jti) %></td>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of Jti for this, could we set it to JWT ID?


<h3 class="t-list__heading"><%= OIDC::ApiKeyRole.human_attribute_name(:access_policy) %></h3>
<div class="push--s">
<code><pre><%= JSON.pretty_generate @api_key_role.access_policy.as_json %></pre></code>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine for now, but for GA, I don't want to show raw json.

@segiddins
Copy link
Member Author

I mean, i didnt want to do the HTML in this PR but it seemed like it was a blocker

@jenshenny
Copy link
Member

jenshenny commented Aug 15, 2023

Apologies if it sounded like the HTML was a blocker for this PR specifically 😅 To rephrase my original comment, I agree with your plan to work on it after this PR was merged, as long as the views are going to be created before any announcements of the alpha release are sent out.

@segiddins
Copy link
Member Author

@jenshenny reverted the HTML, will revisit it as soon as this merges (will keep going on the same branch so we can use the same shipit env for it).

I don't plan on announcing the alpha -- the goal is to get a handful of folks (e.g. those on the rubygems / rubygems.org teams) using the feature before announcing it at all / opening it up for wider adoption. That way, we can make any adjustments as needed before committing to the API in any way.

Copy link
Member

@jenshenny jenshenny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't plan on announcing the alpha -- the goal is to get a handful of folks (e.g. those on the rubygems / rubygems.org teams) using the feature before announcing it at all / opening it up for wider adoption. That way, we can make any adjustments as needed before committing to the API in any way.

Oh ok, I misunderstood what alpha entails - that makes sense, thanks for clarifying!

@rubygems-org-shipit rubygems-org-shipit bot temporarily deployed to oidc-api-token August 17, 2023 23:11 Inactive
@segiddins segiddins merged commit a2ae029 into master Aug 17, 2023
13 of 14 checks passed
@segiddins segiddins deleted the oidc-api-tokens branch August 17, 2023 23:19
@segiddins segiddins restored the oidc-api-tokens branch August 17, 2023 23:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants