-
-
Notifications
You must be signed in to change notification settings - Fork 924
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
Conversation
297b4de
to
5439eaf
Compare
Codecov Report
@@ 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
|
473fa53
to
44807ef
Compare
68a4e97
to
ce1b7f3
Compare
ce1b7f3
to
0c37bf0
Compare
0c37bf0
to
0768e2e
Compare
0768e2e
to
6a66c70
Compare
ffbff8e
to
9fb8546
Compare
f70c4c1
to
473fe23
Compare
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.
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!
8001606
to
d491702
Compare
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.
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| |
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.
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.
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.
@jenshenny given this is the admin interface, I'm not too concerned
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.
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.
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.
The modification is done automatically via an action that fetches from the well-known URLs
app/models/oidc/id_token.rb
Outdated
@@ -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 |
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.
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.
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.
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 |
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.
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)?
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.
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
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.
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>
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.
honestly, I haven't thought about the HTML yet, my plan is to start on that once this ships
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.
Gotcha, sounds great! I think this is necessary enough to be included in the first release and not for GA.
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.
and here it is :)
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.
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 |
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.
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 |
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.
- fill out test methods
@@ -0,0 +1,18 @@ | |||
require "test_helper" | |||
|
|||
class OIDC::Provider::ConfigurationPolicyTest < ActiveSupport::TestCase |
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.
- fill out test methods
@@ -0,0 +1,18 @@ | |||
require "test_helper" | |||
|
|||
class OIDC::IdTokenPolicyTest < ActiveSupport::TestCase |
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.
- fill out test methods
@@ -0,0 +1,18 @@ | |||
require "test_helper" | |||
|
|||
class OIDC::ApiKeyRolePolicyTest < ActiveSupport::TestCase |
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.
- fill out test methods
@@ -0,0 +1,7 @@ | |||
require "test_helper" | |||
|
|||
class OIDC::IdTokenTest < ActiveSupport::TestCase |
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.
- test the model
app/models/oidc/id_token.rb
Outdated
@@ -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 |
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.
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 |
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.
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
@@ -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 |
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.
honestly, I haven't thought about the HTML yet, my plan is to start on that once this ships
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 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.
config/locales/en.yml
Outdated
api_key_roles: OIDC API Key Roles | ||
show: | ||
api_key_role_name: "API Key Role %{name}" |
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.
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}" |
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.
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.
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 %> |
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.
<% @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) %> |
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.
<div class="push--s"> | ||
<code><pre><%= JSON.pretty_generate @api_key_role.api_key_permissions.as_json %></pre></code> | ||
</div> |
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.
Showing the raw json is a bit hard to read. Why not use the scopes, valid_for and gems from OIDC::ApiKeyPermissions
?
app/views/api_keys/index.html.erb
Outdated
<% 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 %> |
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: 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.
<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> |
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.
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> |
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.
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> |
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.
This is fine for now, but for GA, I don't want to show raw json.
I mean, i didnt want to do the HTML in this PR but it seemed like it was a blocker |
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. |
This reverts commit 73bf7c9.
@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. |
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 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!
Implements rubygems/rfcs#49