-
-
Notifications
You must be signed in to change notification settings - Fork 932
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
Scope API keys to a gem #2944
Scope API keys to a gem #2944
Conversation
@@ -37,11 +37,18 @@ def verify_mfa_requirement | |||
render plain: "Gem requires MFA enabled; You do not have MFA enabled yet.", status: :forbidden | |||
end | |||
|
|||
def verify_api_key_gem_scope | |||
return unless @api_key.rubygem && @api_key.rubygem != @rubygem |
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 want to highlight that rubygems.org sorely needs an authorization plugin (ex: pundit). A lot of tech debt has been accrued with checks like these all over the codebase.
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 felt a little guilty writing this line of code. Maybe delegating this logic to the model with something like def authorized_for_gem?(gem)
would be an improvement?
I want to highlight that rubygems.org sorely needs an authorization plugin (ex: pundit). A lot of tech debt has been accrued with checks like these all over the codebase.
Would it be worth creating an issue for this? I bet someone would be interested in looking into this (myself included).
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.
see #2967
IMO, we should create a join table. On the related note, I don't think we need rubygems has_many api_keys relation.
I think we should start with this. We would show validation errors for typos. |
eaf177a
to
0b61c13
Compare
def verify_api_key_gem_scope | ||
return unless @api_key.rubygem && @api_key.rubygem != @rubygem | ||
|
||
render plain: "You do not have permission. This API key is scoped to #{@api_key.rubygem.name}.", status: :forbidden |
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.
Should we really report back which gem is assigned to the key? I would expect same message with wrong (non-existing key) as for key with different scope. It is just invalid for this request (and it doesn't matter why). Exposing this info can make it it easier when you find out a key without having knowledge which gem is assigned to it.
Yes, in theory you can try to find all possible gems to the key and try one by one, but what to make it easier?
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 intent was to display why the request was invalid as much as possible, but it's true that we are making it easier for someone malicious to use this key as you mentioned. I think it's sufficient to say that the key isn't scoped to this gem, and the user can view the key on the UI to figure out which gem the key belongs to 👍
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 "invalid key" message and "key belongs to different gem" messages should be equal.
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 see what you mean. However, the behaviour between these two cases has a slight difference. For the invalid case, the key cannot be used in any api request while the key belongs to a different gem cannot be used in some owner and gem specific commands, so I'm having a bit of trouble crafting a consistent message.
For now, I have it as "Rubygem <name> cannot be scoped this API key".
but that does not sounds entirely correct for either cases. Instead I was thinking of changing in the "key belongs to a different gem" message to be something like this:
This API key does not have permission to modify this gem
while in the "invalid key case" have something more generic since it is set up so that there could be multiple reasons why something is invalid (eg. implementing the concept of expiries on keys).
This API key is invalid
wdyt?
0b61c13
to
8fbbfa6
Compare
@sonalkr132 Can you say anything about why you’d prefer a join table? Are you saying that we do want to support multiple gems per API key? If not (or not yet) then IMO it may be clearer and simpler to stick with a |
I am primarily concerned about api_table.rubygems_id pointing to gems that have changed ownership.
If we keep the association separate, we can delete the record (with ownership). I understand we have an authorization check in code (
I am no opinion about support for multiple gems support, if we receive a feature request with this, we will think about it. I do want to support limiting the scope to IP CIDR(s). Again, technically it can be done without a separate table but I would prefer keeping authorization data separate from data about api key. |
Thank you @sonalkr132, that’s a very helpful answer. If I understand correctly, you’d like to ensure that we clean up the table data when gem ownership changes (i.e. an We need to figure out how this should interact with the lifetime of an API key itself. @jenshenny pointed out that, if a key is created to be scoped to a single gem, it would be dangerous for it to then revert to an unscoped key when the ownership (and perhaps join record) is deleted. Does that mean we should still retain some data about key scope when an ownership is deleted, or alternatively that the key itself should be deleted at the same time? If a person loses ownership of a gem and later regains it, should their old API key start working again? Storing the
Okay, so you’re suggesting the IP CIDR scope could also live on the join model (e.g. |
b8bf7da
to
ce0c706
Compare
Yes, I would prefer dependent destroy but you are right it would mean that api_key reverts to unscoped key. Technically, we can use api_key.fk_id pointing to non-existent record as check, however, I don't want to keep dangling pointers in api_key either. I think we don't want to delete the API key because it may not be as intuitive for the user when they are debugging why their key is not working (given that they don't remember that the key was scoped). I guess metadata about the key being invalid would be the way unless someone has a better idea.
I understand. I would still prefer a more verbose schema over this.
yes |
7eecd20
to
77de212
Compare
Sorry for the delay... I updated the PR to reflect the suggestions raised. Updated the PR description with the changes (1. being adding a join model between api keys and ownership and 2. falling back to a dropdown). |
build_params = { user: current_user, hashed_key: hashed_key(key), **api_key_params } | ||
@api_key = ApiKey.new(build_params) |
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.
setting rubygem_id
is dependant on having user available, calling build
on current_user doesn't set the user until the rest of the params have been set.
A few suggestions on UI:
|
b8ad294
to
5b4c9c6
Compare
c02fb4c
to
228b722
Compare
228b722
to
a8cabfc
Compare
I'm not much of a UX expert, but I've done what I can to make the UX better as suggested
Screen.Recording.2022-05-06.at.10.55.30.PM.mov |
Thank you so much for working on this 💯 You can reach out in rubygems-org slack if you would like to test this in staging. |
Background
This PR allows users to select a gem scope an API key when creating and editing. Keys with this scope can only push, yank, add/remove owners with only the selected gem. All other behaviour would remain the same.
Data model additions
New join table
ApiKeyRubygemScope
that belong to a gem ownership and an API key. An API key can up to oneApiKeyRubygemScope
, ownerships can have manyApiKeyRubygemScope
.There's also a
soft_deleted
column on ApiKeys, which will be explained more in theWhat happens when an ownership is deleted?
section.Creating/Editing a Key with a Gem scope
If user finds themselves submitting an invalid rubygem id, an error will be displayed "Selected gem cannot be scoped to this key"
Screen.Recording.2022-04-04.at.11.48.36.PM.mov
CLI
Screen.Recording.2022-04-05.at.12.06.22.AM.mov
What happens when an ownership is deleted?
When an ownership is deleted, it dependently destroys all associated ApiKeyRubygemScopes. In turn, in a before_destroy callback, the ApiKeyRubygemScope updates the
soft_deleted_at
associated ApiKey. Soft deleted API keys cannot be edited or used. The user can only delete it. Thought aboutinvalid
being the name but that can be confusing will Rails' validationsScreen.Recording.2022-04-05.at.12.09.32.AM.mov