Skip to content

Commit a2ae029

Browse files
authored
Allow using OIDC to fetch api tokens (#3716)
Implements rubygems/rfcs#49
1 parent 7e19c19 commit a2ae029

File tree

126 files changed

+3644
-741
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

126 files changed

+3644
-741
lines changed

.github/workflows/docker.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ on:
44
push:
55
branches:
66
- master
7+
- oidc-api-tokens
78
permissions:
89
contents: read
910
id-token: write

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ gem "octokit", "~> 6.1"
2525
gem "omniauth-github", "~> 2.0"
2626
gem "omniauth", "~> 2.1"
2727
gem "omniauth-rails_csrf_protection", "~> 1.0"
28+
gem "openid_connect", "~> 1.4"
2829
gem "pg", "~> 1.4"
2930
gem "puma", "~> 6.1"
3031
gem "rack", "~> 2.2"

Gemfile.lock

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ GEM
7171
tzinfo (~> 2.0)
7272
addressable (2.8.4)
7373
public_suffix (>= 2.0.2, < 6.0)
74+
aes_key_wrap (1.1.0)
7475
aggregate_assertions (0.2.0)
7576
minitest (~> 5.0)
7677
amazing_print (1.5.0)
@@ -79,6 +80,7 @@ GEM
7980
ffi (~> 1.14)
8081
ffi-compiler (~> 1.0)
8182
ast (2.4.2)
83+
attr_required (1.0.1)
8284
autoprefixer-rails (10.4.13.0)
8385
execjs (~> 2)
8486
avo (2.35.0)
@@ -250,6 +252,7 @@ GEM
250252
httparty (0.21.0)
251253
mini_mime (>= 1.0.0)
252254
multi_xml (>= 0.5.2)
255+
httpclient (2.8.3)
253256
i18n (1.14.1)
254257
concurrent-ruby (~> 1.0)
255258
inline_svg (1.9.0)
@@ -263,6 +266,11 @@ GEM
263266
railties (>= 4.2.0)
264267
thor (>= 0.14, < 2.0)
265268
json (2.6.3)
269+
json-jwt (1.15.3)
270+
activesupport (>= 4.2)
271+
aes_key_wrap
272+
bindata
273+
httpclient
266274
jwt (2.7.0)
267275
kaminari (1.2.2)
268276
activesupport (>= 4.1.0)
@@ -370,6 +378,17 @@ GEM
370378
omniauth-rails_csrf_protection (1.0.1)
371379
actionpack (>= 4.2)
372380
omniauth (~> 2.0)
381+
openid_connect (1.4.2)
382+
activemodel
383+
attr_required (>= 1.0.0)
384+
json-jwt (>= 1.15.0)
385+
net-smtp
386+
rack-oauth2 (~> 1.21)
387+
swd (~> 1.3)
388+
tzinfo
389+
validate_email
390+
validate_url
391+
webfinger (~> 1.2)
373392
opensearch-api (1.0.0)
374393
multi_json
375394
opensearch-dsl (0.2.1)
@@ -407,6 +426,12 @@ GEM
407426
rack (2.2.8)
408427
rack-attack (6.7.0)
409428
rack (>= 1.0, < 4)
429+
rack-oauth2 (1.21.3)
430+
activesupport
431+
attr_required
432+
httpclient
433+
json-jwt (>= 1.11.0)
434+
rack (>= 2.1.0)
410435
rack-protection (3.0.5)
411436
rack
412437
rack-test (2.1.0)
@@ -565,6 +590,10 @@ GEM
565590
sprockets (>= 3.0.0)
566591
statsd-instrument (3.5.11)
567592
stringio (3.0.2)
593+
swd (1.3.0)
594+
activesupport (>= 3)
595+
attr_required (>= 0.0.5)
596+
httpclient (>= 2.4)
568597
terser (1.1.17)
569598
execjs (>= 0.3.0, < 3)
570599
thor (1.2.2)
@@ -588,6 +617,12 @@ GEM
588617
unpwn (1.0.0)
589618
bloomer (~> 1.0)
590619
pwned (~> 2.0)
620+
validate_email (0.1.6)
621+
activemodel (>= 3.0)
622+
mail (>= 2.2.5)
623+
validate_url (1.0.15)
624+
activemodel (>= 3.0.0)
625+
public_suffix
591626
validates_formatting_of (0.9.0)
592627
activemodel
593628
version_gem (1.1.1)
@@ -604,6 +639,9 @@ GEM
604639
openssl (>= 2.2)
605640
safety_net_attestation (~> 0.4.0)
606641
tpm-key_attestation (~> 0.12.0)
642+
webfinger (1.2.0)
643+
activesupport
644+
httpclient (>= 2.4)
607645
webmock (3.18.1)
608646
addressable (>= 2.8.0)
609647
crack (>= 0.3.2)
@@ -667,6 +705,7 @@ DEPENDENCIES
667705
omniauth (~> 2.1)
668706
omniauth-github (~> 2.0)
669707
omniauth-rails_csrf_protection (~> 1.0)
708+
openid_connect (~> 1.4)
670709
opensearch-dsl (~> 0.2.0)
671710
opensearch-ruby (~> 1.0)
672711
pg (~> 1.4)

app/avo/actions/base_action.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
class BaseAction < Avo::BaseAction
2+
include SemanticLogger::Loggable
3+
24
field :comment, as: :textarea, required: true,
35
help: "A comment explaining why this action was taken.<br>Will be saved in the audit log.<br>Must be more than 10 characters."
46

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

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

49-
delegate :error, :avo, :keep_modal_open, :redirect_to, :inform, :action_name, :succeed,
51+
delegate :error, :avo, :keep_modal_open, :redirect_to, :inform, :action_name, :succeed, :logger,
5052
to: :@action
5153

5254
set_callback :handle, :before do
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
class RefreshOIDCProvider < BaseAction
2+
self.name = "Refresh OIDC Provider"
3+
self.visible = lambda {
4+
current_user.team_member?("rubygems-org") && view == :show
5+
}
6+
7+
self.message = lambda {
8+
"Are you sure you would like to refresh #{record.issuer}?"
9+
}
10+
11+
self.confirm_button_label = "Refresh"
12+
13+
class ActionHandler < ActionHandler
14+
def handle_model(provider)
15+
connection = Faraday.new(provider.issuer, request: { timeout: 2 }) do |f|
16+
f.request :json
17+
f.response :logger, logger, headers: false, errors: true, bodies: true
18+
f.response :raise_error
19+
f.response :json
20+
end
21+
resp = connection.get("/.well-known/openid-configuration")
22+
23+
provider.configuration = resp.body
24+
provider.jwks = connection.get(provider.configuration.jwks_uri).body
25+
26+
provider.save!
27+
end
28+
end
29+
end

app/avo/fields/array_of_field.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
class ArrayOfField < Avo::Fields::BaseField
2+
def initialize(name, field:, field_options: {}, **args, &block)
3+
super(name, **args, &nil)
4+
5+
@make_field = lambda do |id:, index: nil, value: nil|
6+
items_holder = Avo::ItemsHolder.new
7+
items_holder.field(id, name: index&.to_s || self.name, as: field, required: -> { false }, value:, **field_options, &block)
8+
items_holder.items.sole.hydrate(view:, resource:)
9+
end
10+
end
11+
12+
def value(...)
13+
value = super(...)
14+
Array.wrap(value)
15+
end
16+
17+
def template_member
18+
@make_field[id: "#{id}[NEW_RECORD]"]
19+
end
20+
21+
def fill_field(model, key, value, params)
22+
value = value.each_value.map do |v|
23+
template_member.fill_field(NestedField::Holder.new, :item, v, params).item
24+
end
25+
super(model, key, value, params)
26+
end
27+
28+
def members
29+
value.each_with_index.map do |value, idx|
30+
id = "#{self.id}[#{idx}]"
31+
@make_field[id:, index: idx, value:]
32+
end
33+
end
34+
35+
def to_permitted_param
36+
@make_field[id:].to_permitted_param
37+
end
38+
end
Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
class JsonViewerField < Avo::Fields::BaseField
1+
class JsonViewerField < Avo::Fields::CodeField
22
def initialize(name, **args, &)
3-
super(name, **args, &)
4-
@theme = args[:theme].present? ? args[:theme].to_s : "default"
5-
@height = args[:height].present? ? args[:height].to_s : "auto"
6-
@tab_size = args[:tab_size].presence || 2
7-
@indent_with_tabs = args[:indent_with_tabs].presence || false
8-
@line_wrapping = args[:line_wrapping].presence || true
3+
super(name, **args, language: :javascript, line_wrapping: true, &)
94
end
105

11-
attr_reader :height, :theme, :tab_size, :indent_with_tabs, :line_wrapping
6+
def value(...)
7+
super&.then { JSON.pretty_generate(_1.as_json) }
8+
end
129
end

app/avo/fields/nested_field.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
class NestedField < Avo::Fields::BaseField
2+
include Avo::Concerns::HasFields
3+
4+
def initialize(name, stacked: true, **args, &block)
5+
@items_holder = Avo::ItemsHolder.new
6+
hide_on [:index]
7+
super(name, stacked:, **args, &nil)
8+
instance_exec(&block) if block
9+
end
10+
11+
def fields(**_kwargs)
12+
@items_holder.items.grep Avo::Fields::BaseField
13+
end
14+
15+
def field(name, **kwargs, &)
16+
@items_holder.field(name, **kwargs, &)
17+
end
18+
19+
def fill_field(model, key, value, params)
20+
value = value.to_h.to_h do |k, v|
21+
[k, get_field(k).fill_field(Holder.new, :item, v, params).item]
22+
end
23+
24+
super(model, key, value, params)
25+
end
26+
27+
def to_permitted_param
28+
{ super => fields.map(&:to_permitted_param) }
29+
end
30+
31+
class Holder
32+
attr_accessor :item
33+
end
34+
end

app/avo/resources/api_key_resource.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ class ApiKeyResource < Avo::BaseResource
22
self.title = :name
33
self.includes = []
44

5+
class ExpiredFilter < ScopeBooleanFilter; end
6+
filter ExpiredFilter, arguments: { default: { expired: false, unexpired: true } }
7+
58
field :id, as: :id, hide_on: :index
69

710
field :name, as: :text, link_to_resource: true
@@ -10,6 +13,7 @@ class ApiKeyResource < Avo::BaseResource
1013
field :last_accessed_at, as: :date_time
1114
field :soft_deleted_at, as: :date_time
1215
field :soft_deleted_rubygem_name, as: :text
16+
field :expires_at, as: :date_time
1317

1418
field :enabled_scopes, as: :tags
1519

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

2933
field :api_key_rubygem_scope, as: :has_one
3034
field :ownership, as: :has_one
35+
field :oidc_id_token, as: :has_one
3136
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
class OIDCApiKeyRoleResource < Avo::BaseResource
2+
self.title = :token
3+
self.includes = []
4+
self.model_class = ::OIDC::ApiKeyRole
5+
# self.search_query = -> do
6+
# scope.ransack(id_eq: params[:q], m: "or").result(distinct: false)
7+
# end
8+
9+
field :token, as: :text, link_to_resource: true, readonly: true
10+
field :id, as: :id, link_to_resource: true, hide_on: :index
11+
# Fields generated from the model
12+
field :name, as: :text
13+
field :provider, as: :belongs_to
14+
field :user, as: :belongs_to
15+
field :api_key_permissions, as: :nested do
16+
field :valid_for, as: :text, format_using: :iso8601
17+
field :scopes, as: :tags, suggestions: ApiKey::API_SCOPES.map { { label: _1, value: _1 } }
18+
field :gems, as: :tags, suggestions: -> { Rubygem.limit(10).pluck(:name).map { { value: _1, label: _1 } } }
19+
end
20+
field :access_policy, as: :nested do
21+
field :statements, as: :array_of, field: :nested do
22+
field :effect, as: :select, options: { "Allow" => "allow" }, default: "Allow"
23+
field :principal, as: :nested, field_options: { stacked: false } do
24+
field :oidc, as: :text
25+
end
26+
field :conditions, as: :array_of, field: :nested, field_options: { stacked: false } do
27+
field :operator, as: :select, options: OIDC::AccessPolicy::Statement::Condition::OPERATORS.index_by(&:titleize)
28+
field :claim, as: :text
29+
field :value, as: :text
30+
end
31+
end
32+
end
33+
34+
field :id_tokens, as: :has_many
35+
# add fields here
36+
end

0 commit comments

Comments
 (0)