-
-
Notifications
You must be signed in to change notification settings - Fork 936
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
WIP: Add Buildkite as a trusted publisher #5437
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,6 @@ def self.table_name_prefix | |
end | ||
|
||
def self.all | ||
[GitHubAction] | ||
[GitHubAction, Buildkite] | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
class OIDC::TrustedPublisher::Buildkite < ApplicationRecord | ||
has_many :rubygem_trusted_publishers, class_name: "OIDC::RubygemTrustedPublisher", as: :trusted_publisher, dependent: :destroy, | ||
inverse_of: :trusted_publisher | ||
has_many :pending_trusted_publishers, class_name: "OIDC::PendingTrustedPublisher", as: :trusted_publisher, dependent: :destroy, | ||
inverse_of: :trusted_publisher | ||
has_many :rubygems, through: :rubygem_trusted_publishers | ||
has_many :api_keys, dependent: :destroy, inverse_of: :owner, as: :owner | ||
|
||
validates :organization_slug, :pipeline_slug, | ||
presence: true, length: { maximum: Gemcutter::MAX_FIELD_LENGTH } | ||
|
||
validate :unique_publisher | ||
|
||
def self.for_claims(claims) | ||
organization_slug = claims.fetch(:organization_slug) | ||
pipeline_slug = claims.fetch(:pipeline_slug) | ||
|
||
where(organization_slug:, pipeline_slug:).first! | ||
end | ||
|
||
def self.permitted_attributes | ||
%i[organization_slug pipeline_slug] | ||
end | ||
|
||
def self.build_trusted_publisher(params) | ||
params = params.reverse_merge(organization_slug: nil, pipeline_slug: nil) | ||
find_or_initialize_by(params) | ||
end | ||
|
||
def self.publisher_name = "Buildkite" | ||
|
||
def payload | ||
{ | ||
name:, | ||
organization_slug:, | ||
pipeline_slug: | ||
} | ||
end | ||
|
||
delegate :as_json, to: :payload | ||
|
||
def organization_slug_condition | ||
OIDC::AccessPolicy::Statement::Condition.new( | ||
operator: "string_equals", | ||
claim: "organization_slug", | ||
value: organization_slug | ||
) | ||
end | ||
|
||
def pipeline_slug_condition | ||
OIDC::AccessPolicy::Statement::Condition.new( | ||
operator: "string_equals", | ||
claim: "pipeline_slug", | ||
value: pipeline_slug | ||
) | ||
end | ||
|
||
def audience_condition | ||
OIDC::AccessPolicy::Statement::Condition.new( | ||
operator: "string_equals", | ||
claim: "aud", | ||
value: Gemcutter::HOST | ||
) | ||
end | ||
|
||
def to_access_policy(jwt) | ||
# TODO what to do with jwt here? | ||
# TODO should we be checking the audience claim? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, audience claim should be checked, same as is done for the GHA publisher |
||
common_conditions = [organization_slug_condition, pipeline_slug_condition].compact | ||
OIDC::AccessPolicy.new( | ||
statements: [ | ||
OIDC::AccessPolicy::Statement.new( | ||
effect: "allow", | ||
principal: OIDC::AccessPolicy::Statement::Principal.new( | ||
oidc: OIDC::Provider::BUILDKITE_ISSUER | ||
), | ||
conditions: common_conditions | ||
) | ||
] | ||
) | ||
end | ||
|
||
#class SigstorePolicy | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we add a push test that uses a build kite trusted publisher and includes an attestation? |
||
# def initialize(trusted_publisher) | ||
# @trusted_publisher = trusted_publisher | ||
# end | ||
|
||
# def verify(cert) | ||
# # 1.3.6.1.4.1.57264.1.14 is `Source Repository Ref` - AKA Branch or Tag | ||
# ref = cert.openssl.find_extension("1.3.6.1.4.1.57264.1.14")&.value_der&.then { OpenSSL::ASN1.decode(_1).value } | ||
# Sigstore::Policy::Identity.new( | ||
# identity: "https://github.com/#{@trusted_publisher.repository}/#{@trusted_publisher.workflow_slug}@#{ref}", | ||
# issuer: OIDC::Provider::BUILDKITE_ISSUER | ||
# ).verify(cert) | ||
# end | ||
#end | ||
|
||
#def to_sigstore_identity_policy | ||
# SigstorePolicy.new(self) | ||
#end | ||
|
||
def name | ||
"#{self.class.publisher_name} #{organization_slug}/#{pipeline_slug}" | ||
end | ||
|
||
def owns_gem?(rubygem) = rubygem_trusted_publishers.exists?(rubygem: rubygem) | ||
|
||
def ld_context | ||
LaunchDarkly::LDContext.create( | ||
key: "#{model_name.singular}-key-#{id}", | ||
kind: "trusted_publisher", | ||
name: name | ||
) | ||
end | ||
|
||
private | ||
|
||
def unique_publisher | ||
return unless self.class.exists?( | ||
organization_slug: organization_slug, | ||
pipeline_slug: pipeline_slug, | ||
) | ||
|
||
errors.add(:base, "publisher already exists") | ||
end | ||
|
||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# frozen_string_literal: true | ||
|
||
class OIDC::TrustedPublisher::Buildkite::FormComponent < ApplicationComponent | ||
prop :buildkite_form, reader: :public | ||
|
||
def view_template | ||
buildkite_form.fields_for :trusted_publisher do |trusted_publisher_form| | ||
field trusted_publisher_form, :text_field, :organization_slug, autocomplete: :off | ||
field trusted_publisher_form, :text_field, :pipeline_slug, autocomplete: :off | ||
end | ||
end | ||
|
||
private | ||
|
||
def field(form, type, name, optional: false, **) | ||
form.label name, class: "form__label" do | ||
plain form.object.class.human_attribute_name(name) | ||
span(class: "t-text--s") { " (#{t('form.optional')})" } if optional | ||
end | ||
form.send(type, name, class: helpers.class_names("form__input", "tw-border tw-border-red-500" => form.object.errors.include?(name)), **) | ||
p(class: "form__field__instructions") { t("oidc.trusted_publisher.buildkite.#{name}_help_html") } | ||
end | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
class OIDC::TrustedPublisher::Buildkite::TableComponent < ApplicationComponent | ||
prop :buildkite, reader: :public | ||
|
||
def view_template | ||
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 | ||
dt(class: "description__heading ") { "Organization Slug" } | ||
dd { code { buildkite.organization_slug } } | ||
|
||
dt(class: "description__heading ") { "Pipeline Slug" } | ||
dd { code { buildkite.pipeline_slug } } | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,7 +18,8 @@ def view_template | |
end | ||
|
||
p do | ||
button_to t(".create"), new_rubygem_trusted_publisher_path(rubygem.slug), class: "form__submit", method: :get | ||
button_to t(".create") + " Buildkite", new_rubygem_trusted_publisher_path(rubygem.slug), params: {trusted_publisher: "buildkite"}, class: "form__submit", method: :get | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. instead of multiple buttons here, what if the trusted publisher new view reloaded the form when the selector for trusted publisher type was changed? |
||
button_to t(".create") + " Github Actions", new_rubygem_trusted_publisher_path(rubygem.slug), params: {trusted_publisher: "github_actions"}, class: "form__submit", method: :get | ||
end | ||
|
||
header(class: "gems__header push--s") do | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,22 +14,31 @@ def view_template | |
title_content | ||
|
||
div(class: "t-body") do | ||
p do | ||
"New Trusted Publisher: #{rubygem_trusted_publisher.trusted_publisher.class.publisher_name}" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i18n ? |
||
end | ||
form_with( | ||
model: rubygem_trusted_publisher, | ||
url: rubygem_trusted_publishers_path(rubygem_trusted_publisher.rubygem.slug) | ||
) do |f| | ||
f.label :trusted_publisher_type, class: "form__label" | ||
f.select :trusted_publisher_type, OIDC::TrustedPublisher.all.map { |type| | ||
[type.publisher_name, type.polymorphic_name] | ||
}, {}, class: "form__input form__select" | ||
|
||
render OIDC::TrustedPublisher::GitHubAction::FormComponent.new( | ||
github_action_form: f | ||
) | ||
f.hidden_field :trusted_publisher_type | ||
|
||
render form_component(f) | ||
f.submit class: "form__submit" | ||
end | ||
end | ||
end | ||
|
||
delegate :rubygem, to: :rubygem_trusted_publisher | ||
|
||
private | ||
|
||
def form_component(form) | ||
case rubygem_trusted_publisher.trusted_publisher | ||
when OIDC::TrustedPublisher::Buildkite then OIDC::TrustedPublisher::Buildkite::FormComponent.new(buildkite_form: form) | ||
when OIDC::TrustedPublisher::GitHubAction then OIDC::TrustedPublisher::GitHubAction::FormComponent.new(github_action_form: form) | ||
else | ||
raise "oh no" | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
<%= render OIDC::TrustedPublisher::Buildkite::TableComponent.new(buildkite:) %> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
<%= render OIDC::TrustedPublisher::GitHubAction::TableComponent.new(github_action:) %> | ||
<%= render OIDC::TrustedPublisher::GitHubAction::TableComponent.new(github_action:) %> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
class CreateOIDCTrustedPublisherBuildkite < ActiveRecord::Migration[8.0] | ||
disable_ddl_transaction! | ||
|
||
def change | ||
create_table :oidc_trusted_publisher_buildkites do |t| | ||
t.string :organization_slug, null: false | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are these two slugs both immutable, or is it possible for someone to take over an organization name that was previously owned by someone else? |
||
t.string :pipeline_slug, null: false | ||
|
||
t.timestamps | ||
end | ||
|
||
add_index :oidc_trusted_publisher_buildkites, | ||
%i[organization_slug pipeline_slug], | ||
unique: true, name: "index_oidc_trusted_publisher_buildkite_claims", algorithm: :concurrently | ||
end | ||
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.
I think we're OK to ignore it, since there are no cross-claim consistency checks we need to enforce, unlike with GitHub (where we make sure the workflow source matches up with the commit being built)