[rubygems/rubygems] Add --attestation option to gem push
Signed-off-by: Samuel Giddins <segiddins@segiddins.me> https://github.com/rubygems/rubygems/commit/a5412d9a0e
This commit is contained in:
parent
b4969348bf
commit
b70c1bb150
@ -30,7 +30,7 @@ The push command will use ~/.gem/credentials to authenticate to a server, but yo
|
|||||||
end
|
end
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
super "push", "Push a gem up to the gem server", host: host
|
super "push", "Push a gem up to the gem server", host: host, attestations: []
|
||||||
|
|
||||||
@user_defined_host = false
|
@user_defined_host = false
|
||||||
|
|
||||||
@ -45,6 +45,11 @@ The push command will use ~/.gem/credentials to authenticate to a server, but yo
|
|||||||
@user_defined_host = true
|
@user_defined_host = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
add_option("--attestation FILE",
|
||||||
|
"Push with sigstore attestations") do |value, options|
|
||||||
|
options[:attestations] << value
|
||||||
|
end
|
||||||
|
|
||||||
@host = nil
|
@host = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -88,9 +93,17 @@ The push command will use ~/.gem/credentials to authenticate to a server, but yo
|
|||||||
|
|
||||||
def send_push_request(name, args)
|
def send_push_request(name, args)
|
||||||
rubygems_api_request(*args, scope: get_push_scope) do |request|
|
rubygems_api_request(*args, scope: get_push_scope) do |request|
|
||||||
request.body = Gem.read_binary name
|
body = Gem.read_binary name
|
||||||
request.add_field "Content-Length", request.body.size
|
if options[:attestations].any?
|
||||||
|
request.set_form([
|
||||||
|
["gem", body, { filename: name, content_type: "application/octet-stream" }],
|
||||||
|
get_attestations_part,
|
||||||
|
], "multipart/form-data")
|
||||||
|
else
|
||||||
|
request.body = body
|
||||||
request.add_field "Content-Type", "application/octet-stream"
|
request.add_field "Content-Type", "application/octet-stream"
|
||||||
|
request.add_field "Content-Length", request.body.size
|
||||||
|
end
|
||||||
request.add_field "Authorization", api_key
|
request.add_field "Authorization", api_key
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -107,4 +120,15 @@ The push command will use ~/.gem/credentials to authenticate to a server, but yo
|
|||||||
def get_push_scope
|
def get_push_scope
|
||||||
:push_rubygem
|
:push_rubygem
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_attestations_part
|
||||||
|
bundles = "[" + options[:attestations].map do |attestation|
|
||||||
|
Gem.read_binary(attestation)
|
||||||
|
end.join(",") + "]"
|
||||||
|
[
|
||||||
|
"attestations",
|
||||||
|
bundles,
|
||||||
|
{ content_type: "application/json" },
|
||||||
|
]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -102,6 +102,47 @@ class TestGemCommandsPushCommand < Gem::TestCase
|
|||||||
@fetcher.last_request["Content-Type"]
|
@fetcher.last_request["Content-Type"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_execute_attestation
|
||||||
|
@response = "Successfully registered gem: freewill (1.0.0)"
|
||||||
|
@fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK")
|
||||||
|
|
||||||
|
File.write("#{@path}.sigstore.json", "attestation")
|
||||||
|
@cmd.options[:args] = [@path]
|
||||||
|
@cmd.options[:attestations] = ["#{@path}.sigstore.json"]
|
||||||
|
|
||||||
|
@cmd.execute
|
||||||
|
|
||||||
|
assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class
|
||||||
|
content_length = @fetcher.last_request["Content-Length"].to_i
|
||||||
|
assert_equal content_length, @fetcher.last_request.body.length
|
||||||
|
assert_equal "multipart", @fetcher.last_request.main_type, @fetcher.last_request.content_type
|
||||||
|
assert_equal "form-data", @fetcher.last_request.sub_type
|
||||||
|
assert_include @fetcher.last_request.type_params, "boundary"
|
||||||
|
boundary = @fetcher.last_request.type_params["boundary"]
|
||||||
|
|
||||||
|
parts = @fetcher.last_request.body.split(/(?:\r\n|\A)--#{Regexp.quote(boundary)}(?:\r\n|--)/m)
|
||||||
|
refute_empty parts
|
||||||
|
assert_empty parts[0]
|
||||||
|
parts.shift # remove the first empty part
|
||||||
|
|
||||||
|
p1 = parts.shift
|
||||||
|
p2 = parts.shift
|
||||||
|
assert_equal "\r\n", parts.shift
|
||||||
|
assert_empty parts
|
||||||
|
|
||||||
|
assert_equal [
|
||||||
|
"Content-Disposition: form-data; name=\"gem\"; filename=\"#{@path}\"",
|
||||||
|
"Content-Type: application/octet-stream",
|
||||||
|
nil,
|
||||||
|
Gem.read_binary(@path),
|
||||||
|
].join("\r\n").b, p1
|
||||||
|
assert_equal [
|
||||||
|
"Content-Disposition: form-data; name=\"attestations\"",
|
||||||
|
nil,
|
||||||
|
"[#{Gem.read_binary("#{@path}.sigstore.json")}]",
|
||||||
|
].join("\r\n").b, p2
|
||||||
|
end
|
||||||
|
|
||||||
def test_execute_allowed_push_host
|
def test_execute_allowed_push_host
|
||||||
@spec, @path = util_gem "freebird", "1.0.1" do |spec|
|
@spec, @path = util_gem "freebird", "1.0.1" do |spec|
|
||||||
spec.metadata["allowed_push_host"] = "https://privategemserver.example"
|
spec.metadata["allowed_push_host"] = "https://privategemserver.example"
|
||||||
|
@ -103,10 +103,21 @@ class Gem::FakeFetcher
|
|||||||
@requests.last
|
@requests.last
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class FakeSocket < StringIO
|
||||||
|
def continue_timeout
|
||||||
|
1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def request(uri, request_class, last_modified = nil)
|
def request(uri, request_class, last_modified = nil)
|
||||||
@requests << request_class.new(uri.request_uri)
|
@requests << request_class.new(uri.request_uri)
|
||||||
yield last_request if block_given?
|
yield last_request if block_given?
|
||||||
|
|
||||||
|
# Ensure multipart request bodies are generated
|
||||||
|
socket = FakeSocket.new
|
||||||
|
last_request.exec socket.binmode, "1.1", last_request.path
|
||||||
|
_, last_request.body = socket.string.split("\r\n\r\n", 2)
|
||||||
|
|
||||||
create_response(uri)
|
create_response(uri)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user