Skip to content

Commit fb8d33b

Browse files
author
James Healy
committed
WIP: hacking and slashing my way to a Buildkite Trusted Publisher
1 parent d92a7bc commit fb8d33b

File tree

13 files changed

+230
-14
lines changed

13 files changed

+230
-14
lines changed

app/controllers/oidc/rubygem_trusted_publishers_controller.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,14 @@ def index
1212
end
1313

1414
def new
15+
trusted_publisher = if params[:trusted_publisher] == "buildkite"
16+
buildkite_trusted_publisher
17+
else
18+
gh_actions_trusted_publisher
19+
end
20+
1521
render OIDC::RubygemTrustedPublishers::NewView.new(
16-
rubygem_trusted_publisher: @rubygem.oidc_rubygem_trusted_publishers.new(trusted_publisher: gh_actions_trusted_publisher)
22+
rubygem_trusted_publisher: @rubygem.oidc_rubygem_trusted_publishers.new(trusted_publisher: trusted_publisher)
1723
)
1824
end
1925

@@ -60,6 +66,10 @@ def find_rubygem_trusted_publisher
6066
@rubygem_trusted_publisher = authorize @rubygem.oidc_rubygem_trusted_publishers.find(params[:id])
6167
end
6268

69+
def buildkite_trusted_publisher
70+
OIDC::TrustedPublisher::Buildkite.new
71+
end
72+
6373
def gh_actions_trusted_publisher
6474
github_params = helpers.github_params(@rubygem)
6575

app/models/oidc/provider.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class OIDC::Provider < ApplicationRecord
1212
has_many :audits, as: :auditable, dependent: :nullify
1313

1414
GITHUB_ACTIONS_ISSUER = "https://token.actions.githubusercontent.com".freeze
15+
BUILDKITE_ISSUER = "https://agent.buildkite.com".freeze
1516

1617
def self.github_actions
1718
find_by(issuer: GITHUB_ACTIONS_ISSUER)
@@ -43,6 +44,8 @@ def trusted_publisher_class
4344
case issuer
4445
when GITHUB_ACTIONS_ISSUER
4546
OIDC::TrustedPublisher::GitHubAction
47+
when BUILDKITE_ISSUER
48+
OIDC::TrustedPublisher::Buildkite
4649
end
4750
end
4851

app/models/oidc/trusted_publisher.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ def self.table_name_prefix
44
end
55

66
def self.all
7-
[GitHubAction]
7+
[GitHubAction, Buildkite]
88
end
99
end
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
class OIDC::TrustedPublisher::Buildkite < ApplicationRecord
2+
has_many :rubygem_trusted_publishers, class_name: "OIDC::RubygemTrustedPublisher", as: :trusted_publisher, dependent: :destroy,
3+
inverse_of: :trusted_publisher
4+
has_many :pending_trusted_publishers, class_name: "OIDC::PendingTrustedPublisher", as: :trusted_publisher, dependent: :destroy,
5+
inverse_of: :trusted_publisher
6+
has_many :rubygems, through: :rubygem_trusted_publishers
7+
has_many :api_keys, dependent: :destroy, inverse_of: :owner, as: :owner
8+
9+
validates :organization_slug, :pipeline_slug,
10+
presence: true, length: { maximum: Gemcutter::MAX_FIELD_LENGTH }
11+
12+
validate :unique_publisher
13+
14+
def self.for_claims(claims)
15+
organization_slug = claims.fetch(:organization_slug)
16+
pipeline_slug = claims.fetch(:pipeline_slug)
17+
18+
where(organization_slug:, pipeline_slug:).first!
19+
end
20+
21+
def self.permitted_attributes
22+
%i[organization_slug pipeline_slug]
23+
end
24+
25+
def self.build_trusted_publisher(params)
26+
params = params.reverse_merge(organization_slug: nil, pipeline_slug: nil)
27+
find_or_initialize_by(params)
28+
end
29+
30+
def self.publisher_name = "Buildkite"
31+
32+
def payload
33+
{
34+
name:,
35+
organization_slug:,
36+
pipeline_slug:
37+
}
38+
end
39+
40+
delegate :as_json, to: :payload
41+
42+
def organization_slug_condition
43+
OIDC::AccessPolicy::Statement::Condition.new(
44+
operator: "string_equals",
45+
claim: "organization_slug",
46+
value: organization_slug
47+
)
48+
end
49+
50+
def pipeline_slug_condition
51+
OIDC::AccessPolicy::Statement::Condition.new(
52+
operator: "string_equals",
53+
claim: "pipeline_slug",
54+
value: pipeline_slug
55+
)
56+
end
57+
58+
def audience_condition
59+
OIDC::AccessPolicy::Statement::Condition.new(
60+
operator: "string_equals",
61+
claim: "aud",
62+
value: Gemcutter::HOST
63+
)
64+
end
65+
66+
def to_access_policy(jwt)
67+
# TODO what to do with jwt here?
68+
# TODO should we be checking the audience claim?
69+
common_conditions = [organization_slug_condition, pipeline_slug_condition].compact
70+
OIDC::AccessPolicy.new(
71+
statements: [
72+
OIDC::AccessPolicy::Statement.new(
73+
effect: "allow",
74+
principal: OIDC::AccessPolicy::Statement::Principal.new(
75+
oidc: OIDC::Provider::BUILDKITE_ISSUER
76+
),
77+
conditions: common_conditions
78+
)
79+
]
80+
)
81+
end
82+
83+
#class SigstorePolicy
84+
# def initialize(trusted_publisher)
85+
# @trusted_publisher = trusted_publisher
86+
# end
87+
88+
# def verify(cert)
89+
# # 1.3.6.1.4.1.57264.1.14 is `Source Repository Ref` - AKA Branch or Tag
90+
# ref = cert.openssl.find_extension("1.3.6.1.4.1.57264.1.14")&.value_der&.then { OpenSSL::ASN1.decode(_1).value }
91+
# Sigstore::Policy::Identity.new(
92+
# identity: "https://github.com/#{@trusted_publisher.repository}/#{@trusted_publisher.workflow_slug}@#{ref}",
93+
# issuer: OIDC::Provider::BUILDKITE_ISSUER
94+
# ).verify(cert)
95+
# end
96+
#end
97+
98+
#def to_sigstore_identity_policy
99+
# SigstorePolicy.new(self)
100+
#end
101+
102+
def name
103+
"#{self.class.publisher_name} #{organization_slug}/#{pipeline_slug}"
104+
end
105+
106+
def owns_gem?(rubygem) = rubygem_trusted_publishers.exists?(rubygem: rubygem)
107+
108+
def ld_context
109+
LaunchDarkly::LDContext.create(
110+
key: "#{model_name.singular}-key-#{id}",
111+
kind: "trusted_publisher",
112+
name: name
113+
)
114+
end
115+
116+
private
117+
118+
def unique_publisher
119+
return unless self.class.exists?(
120+
organization_slug: organization_slug,
121+
pipeline_slug: pipeline_slug,
122+
)
123+
124+
errors.add(:base, "publisher already exists")
125+
end
126+
127+
end
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
3+
class OIDC::TrustedPublisher::Buildkite::FormComponent < ApplicationComponent
4+
prop :buildkite_form, reader: :public
5+
6+
def view_template
7+
buildkite_form.fields_for :trusted_publisher do |trusted_publisher_form|
8+
field trusted_publisher_form, :text_field, :organization_slug, autocomplete: :off
9+
field trusted_publisher_form, :text_field, :pipeline_slug, autocomplete: :off
10+
end
11+
end
12+
13+
private
14+
15+
def field(form, type, name, optional: false, **)
16+
form.label name, class: "form__label" do
17+
plain form.object.class.human_attribute_name(name)
18+
span(class: "t-text--s") { " (#{t('form.optional')})" } if optional
19+
end
20+
form.send(type, name, class: helpers.class_names("form__input", "tw-border tw-border-red-500" => form.object.errors.include?(name)), **)
21+
p(class: "form__field__instructions") { t("oidc.trusted_publisher.buildkite.#{name}_help_html") }
22+
end
23+
end
24+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class OIDC::TrustedPublisher::Buildkite::TableComponent < ApplicationComponent
2+
prop :buildkite, reader: :public
3+
4+
def view_template
5+
dl(class: "tw-flex tw-flex-col sm:tw-grid sm:tw-grid-cols-2 tw-items-baseline tw-gap-4 full-width overflow-wrap") do
6+
dt(class: "description__heading ") { "Organization Slug" }
7+
dd { code { buildkite.organization_slug } }
8+
9+
dt(class: "description__heading ") { "Pipeline Slug" }
10+
dd { code { buildkite.pipeline_slug } }
11+
end
12+
end
13+
end

app/views/oidc/rubygem_trusted_publishers/index_view.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ def view_template
1818
end
1919

2020
p do
21-
button_to t(".create"), new_rubygem_trusted_publisher_path(rubygem.slug), class: "form__submit", method: :get
21+
button_to t(".create") + " Buildkite", new_rubygem_trusted_publisher_path(rubygem.slug), params: {trusted_publisher: "buildkite"}, class: "form__submit", method: :get
22+
button_to t(".create") + " Github Actions", new_rubygem_trusted_publisher_path(rubygem.slug), params: {trusted_publisher: "github_actions"}, class: "form__submit", method: :get
2223
end
2324

2425
header(class: "gems__header push--s") do

app/views/oidc/rubygem_trusted_publishers/new_view.rb

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,31 @@ def view_template
1414
title_content
1515

1616
div(class: "t-body") do
17+
p do
18+
"New Trusted Publisher: #{rubygem_trusted_publisher.trusted_publisher.class.publisher_name}"
19+
end
1720
form_with(
1821
model: rubygem_trusted_publisher,
1922
url: rubygem_trusted_publishers_path(rubygem_trusted_publisher.rubygem.slug)
2023
) do |f|
21-
f.label :trusted_publisher_type, class: "form__label"
22-
f.select :trusted_publisher_type, OIDC::TrustedPublisher.all.map { |type|
23-
[type.publisher_name, type.polymorphic_name]
24-
}, {}, class: "form__input form__select"
25-
26-
render OIDC::TrustedPublisher::GitHubAction::FormComponent.new(
27-
github_action_form: f
28-
)
24+
f.hidden_field :trusted_publisher_type
25+
26+
render form_component(f)
2927
f.submit class: "form__submit"
3028
end
3129
end
3230
end
3331

3432
delegate :rubygem, to: :rubygem_trusted_publisher
33+
34+
private
35+
36+
def form_component(form)
37+
case rubygem_trusted_publisher.trusted_publisher
38+
when OIDC::TrustedPublisher::Buildkite then OIDC::TrustedPublisher::Buildkite::FormComponent.new(buildkite_form: form)
39+
when OIDC::TrustedPublisher::GitHubAction then OIDC::TrustedPublisher::GitHubAction::FormComponent.new(github_action_form: form)
40+
else
41+
raise "oh no"
42+
end
43+
end
3544
end
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<%= render OIDC::TrustedPublisher::Buildkite::TableComponent.new(buildkite:) %>
2+
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<%= render OIDC::TrustedPublisher::GitHubAction::TableComponent.new(github_action:) %>
1+
<%= render OIDC::TrustedPublisher::GitHubAction::TableComponent.new(github_action:) %>

0 commit comments

Comments
 (0)