Skip to content

Commit

Permalink
Allow using OIDC to fetch api tokens (#3716)
Browse files Browse the repository at this point in the history
Implements rubygems/rfcs#49
  • Loading branch information
segiddins committed Aug 17, 2023
1 parent 7e19c19 commit a2ae029
Show file tree
Hide file tree
Showing 126 changed files with 3,644 additions and 741 deletions.
1 change: 1 addition & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- master
- oidc-api-tokens
permissions:
contents: read
id-token: write
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ gem "octokit", "~> 6.1"
gem "omniauth-github", "~> 2.0"
gem "omniauth", "~> 2.1"
gem "omniauth-rails_csrf_protection", "~> 1.0"
gem "openid_connect", "~> 1.4"
gem "pg", "~> 1.4"
gem "puma", "~> 6.1"
gem "rack", "~> 2.2"
Expand Down
39 changes: 39 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ GEM
tzinfo (~> 2.0)
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
aes_key_wrap (1.1.0)
aggregate_assertions (0.2.0)
minitest (~> 5.0)
amazing_print (1.5.0)
Expand All @@ -79,6 +80,7 @@ GEM
ffi (~> 1.14)
ffi-compiler (~> 1.0)
ast (2.4.2)
attr_required (1.0.1)
autoprefixer-rails (10.4.13.0)
execjs (~> 2)
avo (2.35.0)
Expand Down Expand Up @@ -250,6 +252,7 @@ GEM
httparty (0.21.0)
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
inline_svg (1.9.0)
Expand All @@ -263,6 +266,11 @@ GEM
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.6.3)
json-jwt (1.15.3)
activesupport (>= 4.2)
aes_key_wrap
bindata
httpclient
jwt (2.7.0)
kaminari (1.2.2)
activesupport (>= 4.1.0)
Expand Down Expand Up @@ -370,6 +378,17 @@ GEM
omniauth-rails_csrf_protection (1.0.1)
actionpack (>= 4.2)
omniauth (~> 2.0)
openid_connect (1.4.2)
activemodel
attr_required (>= 1.0.0)
json-jwt (>= 1.15.0)
net-smtp
rack-oauth2 (~> 1.21)
swd (~> 1.3)
tzinfo
validate_email
validate_url
webfinger (~> 1.2)
opensearch-api (1.0.0)
multi_json
opensearch-dsl (0.2.1)
Expand Down Expand Up @@ -407,6 +426,12 @@ GEM
rack (2.2.8)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-oauth2 (1.21.3)
activesupport
attr_required
httpclient
json-jwt (>= 1.11.0)
rack (>= 2.1.0)
rack-protection (3.0.5)
rack
rack-test (2.1.0)
Expand Down Expand Up @@ -565,6 +590,10 @@ GEM
sprockets (>= 3.0.0)
statsd-instrument (3.5.11)
stringio (3.0.2)
swd (1.3.0)
activesupport (>= 3)
attr_required (>= 0.0.5)
httpclient (>= 2.4)
terser (1.1.17)
execjs (>= 0.3.0, < 3)
thor (1.2.2)
Expand All @@ -588,6 +617,12 @@ GEM
unpwn (1.0.0)
bloomer (~> 1.0)
pwned (~> 2.0)
validate_email (0.1.6)
activemodel (>= 3.0)
mail (>= 2.2.5)
validate_url (1.0.15)
activemodel (>= 3.0.0)
public_suffix
validates_formatting_of (0.9.0)
activemodel
version_gem (1.1.1)
Expand All @@ -604,6 +639,9 @@ GEM
openssl (>= 2.2)
safety_net_attestation (~> 0.4.0)
tpm-key_attestation (~> 0.12.0)
webfinger (1.2.0)
activesupport
httpclient (>= 2.4)
webmock (3.18.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
Expand Down Expand Up @@ -667,6 +705,7 @@ DEPENDENCIES
omniauth (~> 2.1)
omniauth-github (~> 2.0)
omniauth-rails_csrf_protection (~> 1.0)
openid_connect (~> 1.4)
opensearch-dsl (~> 0.2.0)
opensearch-ruby (~> 1.0)
pg (~> 1.4)
Expand Down
4 changes: 3 additions & 1 deletion app/avo/actions/base_action.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class BaseAction < Avo::BaseAction
include SemanticLogger::Loggable

field :comment, as: :textarea, required: true,
help: "A comment explaining why this action was taken.<br>Will be saved in the audit log.<br>Must be more than 10 characters."

Expand Down Expand Up @@ -46,7 +48,7 @@ def initialize( # rubocop:disable Metrics/ParameterLists

attr_reader :models, :fields, :current_user, :arguments, :resource

delegate :error, :avo, :keep_modal_open, :redirect_to, :inform, :action_name, :succeed,
delegate :error, :avo, :keep_modal_open, :redirect_to, :inform, :action_name, :succeed, :logger,
to: :@action

set_callback :handle, :before do
Expand Down
29 changes: 29 additions & 0 deletions app/avo/actions/refresh_oidc_provider.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
class RefreshOIDCProvider < BaseAction
self.name = "Refresh OIDC Provider"
self.visible = lambda {
current_user.team_member?("rubygems-org") && view == :show
}

self.message = lambda {
"Are you sure you would like to refresh #{record.issuer}?"
}

self.confirm_button_label = "Refresh"

class ActionHandler < ActionHandler
def handle_model(provider)
connection = Faraday.new(provider.issuer, request: { timeout: 2 }) do |f|
f.request :json
f.response :logger, logger, headers: false, errors: true, bodies: true
f.response :raise_error
f.response :json
end
resp = connection.get("/.well-known/openid-configuration")

provider.configuration = resp.body
provider.jwks = connection.get(provider.configuration.jwks_uri).body

provider.save!
end
end
end
38 changes: 38 additions & 0 deletions app/avo/fields/array_of_field.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
class ArrayOfField < Avo::Fields::BaseField
def initialize(name, field:, field_options: {}, **args, &block)
super(name, **args, &nil)

@make_field = lambda do |id:, index: nil, value: nil|
items_holder = Avo::ItemsHolder.new
items_holder.field(id, name: index&.to_s || self.name, as: field, required: -> { false }, value:, **field_options, &block)
items_holder.items.sole.hydrate(view:, resource:)
end
end

def value(...)
value = super(...)
Array.wrap(value)
end

def template_member
@make_field[id: "#{id}[NEW_RECORD]"]
end

def fill_field(model, key, value, params)
value = value.each_value.map do |v|
template_member.fill_field(NestedField::Holder.new, :item, v, params).item
end
super(model, key, value, params)
end

def members
value.each_with_index.map do |value, idx|
id = "#{self.id}[#{idx}]"
@make_field[id:, index: idx, value:]
end
end

def to_permitted_param
@make_field[id:].to_permitted_param
end
end
13 changes: 5 additions & 8 deletions app/avo/fields/json_viewer_field.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
class JsonViewerField < Avo::Fields::BaseField
class JsonViewerField < Avo::Fields::CodeField
def initialize(name, **args, &)
super(name, **args, &)
@theme = args[:theme].present? ? args[:theme].to_s : "default"
@height = args[:height].present? ? args[:height].to_s : "auto"
@tab_size = args[:tab_size].presence || 2
@indent_with_tabs = args[:indent_with_tabs].presence || false
@line_wrapping = args[:line_wrapping].presence || true
super(name, **args, language: :javascript, line_wrapping: true, &)
end

attr_reader :height, :theme, :tab_size, :indent_with_tabs, :line_wrapping
def value(...)
super&.then { JSON.pretty_generate(_1.as_json) }
end
end
34 changes: 34 additions & 0 deletions app/avo/fields/nested_field.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
class NestedField < Avo::Fields::BaseField
include Avo::Concerns::HasFields

def initialize(name, stacked: true, **args, &block)
@items_holder = Avo::ItemsHolder.new
hide_on [:index]
super(name, stacked:, **args, &nil)
instance_exec(&block) if block
end

def fields(**_kwargs)
@items_holder.items.grep Avo::Fields::BaseField
end

def field(name, **kwargs, &)
@items_holder.field(name, **kwargs, &)
end

def fill_field(model, key, value, params)
value = value.to_h.to_h do |k, v|
[k, get_field(k).fill_field(Holder.new, :item, v, params).item]
end

super(model, key, value, params)
end

def to_permitted_param
{ super => fields.map(&:to_permitted_param) }
end

class Holder
attr_accessor :item
end
end
5 changes: 5 additions & 0 deletions app/avo/resources/api_key_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ class ApiKeyResource < Avo::BaseResource
self.title = :name
self.includes = []

class ExpiredFilter < ScopeBooleanFilter; end
filter ExpiredFilter, arguments: { default: { expired: false, unexpired: true } }

field :id, as: :id, hide_on: :index

field :name, as: :text, link_to_resource: true
Expand All @@ -10,6 +13,7 @@ class ApiKeyResource < Avo::BaseResource
field :last_accessed_at, as: :date_time
field :soft_deleted_at, as: :date_time
field :soft_deleted_rubygem_name, as: :text
field :expires_at, as: :date_time

field :enabled_scopes, as: :tags

Expand All @@ -28,4 +32,5 @@ class ApiKeyResource < Avo::BaseResource

field :api_key_rubygem_scope, as: :has_one
field :ownership, as: :has_one
field :oidc_id_token, as: :has_one
end
36 changes: 36 additions & 0 deletions app/avo/resources/oidc_api_key_role_resource.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
class OIDCApiKeyRoleResource < Avo::BaseResource
self.title = :token
self.includes = []
self.model_class = ::OIDC::ApiKeyRole
# self.search_query = -> do
# scope.ransack(id_eq: params[:q], m: "or").result(distinct: false)
# end

field :token, as: :text, link_to_resource: true, readonly: true
field :id, as: :id, link_to_resource: true, hide_on: :index
# Fields generated from the model
field :name, as: :text
field :provider, as: :belongs_to
field :user, as: :belongs_to
field :api_key_permissions, as: :nested do
field :valid_for, as: :text, format_using: :iso8601
field :scopes, as: :tags, suggestions: ApiKey::API_SCOPES.map { { label: _1, value: _1 } }
field :gems, as: :tags, suggestions: -> { Rubygem.limit(10).pluck(:name).map { { value: _1, label: _1 } } }
end
field :access_policy, as: :nested do
field :statements, as: :array_of, field: :nested do
field :effect, as: :select, options: { "Allow" => "allow" }, default: "Allow"
field :principal, as: :nested, field_options: { stacked: false } do
field :oidc, as: :text
end
field :conditions, as: :array_of, field: :nested, field_options: { stacked: false } do
field :operator, as: :select, options: OIDC::AccessPolicy::Statement::Condition::OPERATORS.index_by(&:titleize)
field :claim, as: :text
field :value, as: :text
end
end
end

field :id_tokens, as: :has_many
# add fields here
end
23 changes: 23 additions & 0 deletions app/avo/resources/oidc_id_token_resource.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class OIDCIdTokenResource < Avo::BaseResource
self.title = :id
self.includes = []
self.model_class = ::OIDC::IdToken
# self.search_query = -> do
# scope.ransack(id_eq: params[:q], m: "or").result(distinct: false)
# end

field :id, as: :id
# Fields generated from the model
field :api_key_role, as: :belongs_to
field :provider, as: :has_one
field :api_key, as: :has_one

heading "JWT"
field :claims, as: :key_value, stacked: true do
model.jwt.fetch("claims")
end
field :header, as: :key_value, stacked: true do
model.jwt.fetch("header")
end
# add fields here
end
23 changes: 23 additions & 0 deletions app/avo/resources/oidc_provider_resource.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class OIDCProviderResource < Avo::BaseResource
self.title = :issuer
self.includes = []
self.model_class = ::OIDC::Provider
# self.search_query = -> do
# scope.ransack(id_eq: params[:q], m: "or").result(distinct: false)
# end

action RefreshOIDCProvider

# Fields generated from the model
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|
field k, as: (k.to_s.end_with?("s_supported") ? :tags : :text), visible: ->(_) { visible_on.include?(view) || value.send(k).present? }
end
end
field :jwks, as: :array_of, field: :json_viewer, hide_on: :index
field :api_key_roles, as: :has_many
# add fields here
field :id, as: :id
end
1 change: 1 addition & 0 deletions app/avo/resources/version_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class IndexedFilter < ScopeBooleanFilter; end
field :yanked_at, as: :date_time, sortable: true

field :pusher, as: :belongs_to, class: "User"
field :pusher_api_key, as: :belongs_to, class: "ApiKey"

tabs do
tab "Metadata", description: "Metadata that comes from the gemspec" do
Expand Down
Loading

0 comments on commit a2ae029

Please sign in to comment.