faraday-middlewares-build_service 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 92109b8060adcc56c966e338975c922d8949fe84441fad5966b402f15d50eeeb
4
+ data.tar.gz: 8694e818cf0ac7ea9e3883c20bb924599c029bd34784c4347a3471272059c6e4
5
+ SHA512:
6
+ metadata.gz: 385406c99d67586cf0d0f5ee44c393df80c3bef79e1dbfdd984b13f6747887cd6a566ed735841a9561e90ea4b8f2f1e08f8746fa4e970fb76a0b44f7798a1d72
7
+ data.tar.gz: f551de118fa165f3a4dd74aa0d016807bfa9faa0da05660ed5f43c3e6e388248b8ec911385c2a0b9a5b916794dfc72024c47b6933e25a46ecdaa57a48a7c97af
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/testdouble/standard
3
+ ruby_version: 2.6
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2023-11-27
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "rake", "~> 13.0"
8
+
9
+ group :development do
10
+ gem "standard", "~> 1.3"
11
+ gem "byebug"
12
+ end
13
+
14
+ group :test do
15
+ gem "webmock"
16
+ gem "rspec", "~> 3.0"
17
+ gem "rspec-github", require: false
18
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,123 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ faraday-middlewares-build_service (0.1.0)
5
+ faraday (~> 1.10)
6
+ http-cookie (~> 1.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ addressable (2.8.5)
12
+ public_suffix (>= 2.0.2, < 6.0)
13
+ ast (2.4.2)
14
+ byebug (11.1.3)
15
+ crack (0.4.5)
16
+ rexml
17
+ diff-lcs (1.5.0)
18
+ domain_name (0.6.20231109)
19
+ faraday (1.10.3)
20
+ faraday-em_http (~> 1.0)
21
+ faraday-em_synchrony (~> 1.0)
22
+ faraday-excon (~> 1.1)
23
+ faraday-httpclient (~> 1.0)
24
+ faraday-multipart (~> 1.0)
25
+ faraday-net_http (~> 1.0)
26
+ faraday-net_http_persistent (~> 1.0)
27
+ faraday-patron (~> 1.0)
28
+ faraday-rack (~> 1.0)
29
+ faraday-retry (~> 1.0)
30
+ ruby2_keywords (>= 0.0.4)
31
+ faraday-em_http (1.0.0)
32
+ faraday-em_synchrony (1.0.0)
33
+ faraday-excon (1.1.0)
34
+ faraday-httpclient (1.0.1)
35
+ faraday-multipart (1.0.4)
36
+ multipart-post (~> 2)
37
+ faraday-net_http (1.0.1)
38
+ faraday-net_http_persistent (1.2.0)
39
+ faraday-patron (1.0.0)
40
+ faraday-rack (1.0.0)
41
+ faraday-retry (1.0.3)
42
+ hashdiff (1.0.1)
43
+ http-cookie (1.0.5)
44
+ domain_name (~> 0.5)
45
+ json (2.6.3)
46
+ language_server-protocol (3.17.0.3)
47
+ lint_roller (1.1.0)
48
+ multipart-post (2.3.0)
49
+ parallel (1.23.0)
50
+ parser (3.2.2.4)
51
+ ast (~> 2.4.1)
52
+ racc
53
+ public_suffix (5.0.4)
54
+ racc (1.7.3)
55
+ rainbow (3.1.1)
56
+ rake (13.1.0)
57
+ regexp_parser (2.8.2)
58
+ rexml (3.2.6)
59
+ rspec (3.12.0)
60
+ rspec-core (~> 3.12.0)
61
+ rspec-expectations (~> 3.12.0)
62
+ rspec-mocks (~> 3.12.0)
63
+ rspec-core (3.12.2)
64
+ rspec-support (~> 3.12.0)
65
+ rspec-expectations (3.12.3)
66
+ diff-lcs (>= 1.2.0, < 2.0)
67
+ rspec-support (~> 3.12.0)
68
+ rspec-github (2.4.0)
69
+ rspec-core (~> 3.0)
70
+ rspec-mocks (3.12.6)
71
+ diff-lcs (>= 1.2.0, < 2.0)
72
+ rspec-support (~> 3.12.0)
73
+ rspec-support (3.12.1)
74
+ rubocop (1.57.2)
75
+ json (~> 2.3)
76
+ language_server-protocol (>= 3.17.0)
77
+ parallel (~> 1.10)
78
+ parser (>= 3.2.2.4)
79
+ rainbow (>= 2.2.2, < 4.0)
80
+ regexp_parser (>= 1.8, < 3.0)
81
+ rexml (>= 3.2.5, < 4.0)
82
+ rubocop-ast (>= 1.28.1, < 2.0)
83
+ ruby-progressbar (~> 1.7)
84
+ unicode-display_width (>= 2.4.0, < 3.0)
85
+ rubocop-ast (1.30.0)
86
+ parser (>= 3.2.1.0)
87
+ rubocop-performance (1.19.1)
88
+ rubocop (>= 1.7.0, < 2.0)
89
+ rubocop-ast (>= 0.4.0)
90
+ ruby-progressbar (1.13.0)
91
+ ruby2_keywords (0.0.5)
92
+ standard (1.32.0)
93
+ language_server-protocol (~> 3.17.0.2)
94
+ lint_roller (~> 1.0)
95
+ rubocop (~> 1.57.2)
96
+ standard-custom (~> 1.0.0)
97
+ standard-performance (~> 1.2)
98
+ standard-custom (1.0.2)
99
+ lint_roller (~> 1.0)
100
+ rubocop (~> 1.50)
101
+ standard-performance (1.2.1)
102
+ lint_roller (~> 1.1)
103
+ rubocop-performance (~> 1.19.1)
104
+ unicode-display_width (2.5.0)
105
+ webmock (3.19.1)
106
+ addressable (>= 2.8.0)
107
+ crack (>= 0.3.2)
108
+ hashdiff (>= 0.4.0, < 2.0.0)
109
+
110
+ PLATFORMS
111
+ x86_64-linux
112
+
113
+ DEPENDENCIES
114
+ byebug
115
+ faraday-middlewares-build_service!
116
+ rake (~> 13.0)
117
+ rspec (~> 3.0)
118
+ rspec-github
119
+ standard (~> 1.3)
120
+ webmock
121
+
122
+ BUNDLED WITH
123
+ 2.4.10
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 SUSE
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # Faraday::Middlewares::BuildService
2
+
3
+ Provides a Faraday Middleware to interact with the [Open Build Service][obs].
4
+
5
+ It'll automatically retry a request with the authentication mechanism requested by the Build Service.
6
+
7
+ ## Installation
8
+
9
+ Install the gem and add to the application's Gemfile by executing:
10
+
11
+ ```bash
12
+ $ bundle add faraday-middlewares-bs-auth
13
+ ```
14
+
15
+ If bundler is not being used to manage dependencies, install the gem by executing:
16
+
17
+ ```bash
18
+ $ gem install faraday-middlewares-bs-auth
19
+ ```
20
+
21
+ ## Requirements
22
+
23
+ - `openssh` (for the `ssh-keygen` command).
24
+
25
+ ## Usage
26
+
27
+ ```ruby
28
+ # For Basic Authentication
29
+ credentials = {
30
+ username: 'my-user',
31
+ password: 'my-password'
32
+ }
33
+
34
+ # For SSH Signature authentication
35
+ credentials = {
36
+ username: 'my-user',
37
+ ssh_key: '--- BEGIN ... KEY --- ...'
38
+ }
39
+
40
+ base_url = "https://build.opensuse.org/"
41
+
42
+ client = Faraday.new(url: base_url) do |faraday|
43
+ faraday.use FaradayMiddleware::FollowRedirects
44
+ faraday.use Faraday::BuildService::Authentication, credentials: credentials
45
+ end
46
+
47
+ # then use faraday as usual:
48
+ response = client.get('/about')
49
+ puts response.headers
50
+ puts response.body
51
+ ```
52
+
53
+ ## Development
54
+
55
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
56
+
57
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
58
+
59
+ ## Contributing
60
+
61
+ Bug reports and pull requests are welcome on GitHub at https://github.com/SUSE/faraday-middlewares-build_service. This project is intended to be a safe, welcoming space for collaboration.
62
+
63
+ ## License
64
+
65
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
66
+
67
+ [obs]: https://openbuildservice.org/
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[spec standard]
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/faraday/middlewares/build_service/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "faraday-middlewares-build_service"
7
+ spec.version = Faraday::Middlewares::BuildService::VERSION
8
+ spec.authors = ["Jose D. Gomez R."]
9
+ spec.email = ["jose.gomez@suse.com"]
10
+
11
+ spec.summary = "Plug-and-play authentication for requests against Build Service"
12
+ spec.description = <<~DESC
13
+ Faraday middleware to automatically negotiate authentication with Build Service instances.
14
+
15
+ It can handle: No, Basic, and, Signature authentication.
16
+ DESC
17
+ spec.homepage = "https://github.com/SUSE/faraday-middlewares-bs-auth"
18
+ spec.license = "MIT"
19
+ spec.required_ruby_version = ">= 2.6.0"
20
+
21
+ spec.metadata["homepage_uri"] = spec.homepage
22
+ spec.metadata["source_code_uri"] = spec.homepage
23
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"
24
+
25
+ # Specify which files should be added to the gem when it is released.
26
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
27
+ spec.files = Dir.chdir(__dir__) do
28
+ `git ls-files -z`.split("\x0").reject do |f|
29
+ (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
30
+ end
31
+ end
32
+ spec.bindir = "exe"
33
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
34
+ spec.require_paths = ["lib"]
35
+
36
+ spec.add_dependency "faraday", "~> 1.10"
37
+ spec.add_dependency "http-cookie", "~> 1.0"
38
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "http-cookie"
5
+
6
+ module Faraday
7
+ module Middlewares
8
+ module BuildService
9
+ class Authentication < Faraday::Middleware
10
+ include BuildService::HttpHelpers
11
+
12
+ # @param env [Faraday::Env]
13
+ # @param credentials [Hash]
14
+ def initialize(app, credentials: {})
15
+ super(app)
16
+
17
+ @username = credentials[:username]
18
+ @password = credentials[:password]
19
+ @ssh_key = credentials[:ssh_key]
20
+
21
+ reset!
22
+ end
23
+
24
+ def reset!
25
+ @cookie_jar = ::HTTP::CookieJar.new
26
+ @authorization = nil
27
+ end
28
+
29
+ # @param env [Faraday::Env]
30
+ def call(env)
31
+ request_body = env[:body]
32
+ super(env)
33
+ rescue BuildService::RetryWithAuthError
34
+ # after failure env[:body] is set to the response body
35
+ # see https://github.com/lostisland/faraday-retry/blob/main/lib/faraday/retry/middleware.rb
36
+ env[:body] = request_body
37
+ # retry only once
38
+ super(env)
39
+ end
40
+
41
+ # @param env [Faraday::Env]
42
+ def on_request(env)
43
+ # If we have a cookie, use it and skip setting any other header.
44
+ if (cookie = @cookie_jar.cookies(env.url) && cookie&.any?)
45
+ env.request_headers["cookie"] = ::HTTP::Cookie.cookie_value(cookie)
46
+ return
47
+ end
48
+
49
+ env.request_headers["Authorization"] = @authorization if @authorization
50
+ end
51
+
52
+ # @param env [Faraday::Env]
53
+ def on_complete(env)
54
+ # If we have get cookie, we just save it, nothing else to do.
55
+ if (cookie = env[:response_headers]["set-cookie"])
56
+ @cookie_jar.parse(cookie, env[:url]) and return
57
+ end
58
+
59
+ # if there's no challenge response, means we're good
60
+ return unless env[:response_headers]["www-authenticate"]
61
+
62
+ challenges = parse_authorization_header(env[:response_headers]["www-authenticate"])
63
+ # If there's a challenge response AND we did authorization, then we failed.
64
+ # Stop here and let the failure bubble up.
65
+ return if @authorization
66
+
67
+ # Do the authorization challenges
68
+ challenges.each do |type, details|
69
+ next do_basic_auth!(env, details) if type.to_s.casecmp("basic").zero?
70
+ next do_signature_auth!(env, details) if type.to_s.casecmp("signature").zero?
71
+
72
+ raise UnknownChallengeError.new("Unknown challenge #{type}: #{details.inspect}")
73
+ end
74
+
75
+ # Signal to #call that we need to retry the request.
76
+ raise BuildService::RetryWithAuthError.new("Retry!")
77
+ end
78
+
79
+ # @param env [Faraday::Env]
80
+ # @param _details [Hash]
81
+ def do_basic_auth!(_env, _details)
82
+ raise MissingCredentialsError.new("Missing Username / Password") unless @username || @password
83
+
84
+ # Build an HTTP Basic Auth header
85
+ value = Base64.encode64([@username, @password].join(":"))
86
+ value.delete!("\n")
87
+ @authorization = "Basic #{value}"
88
+ end
89
+
90
+ # @param env [Faraday::Env]
91
+ # @param _details [Hash]
92
+ def do_signature_auth!(env, details)
93
+ raise MissingCredentialsError.new("Missing Username / SSH Key") unless @username || @ssh_key
94
+
95
+ # Build an HTTP Signature Auth header
96
+ payload = SshSigner.new(details, env[:response_headers], credentials: {
97
+ username: @username,
98
+ password: @password,
99
+ ssh_key: @ssh_key
100
+ }).generate
101
+ @authorization = "Signature #{payload}"
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Faraday
4
+ module Middlewares
5
+ module BuildService
6
+ class BaseError < StandardError; end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Faraday
4
+ module Middlewares
5
+ module BuildService
6
+ # Collection of low level HTTP Helpers
7
+ module HttpHelpers
8
+ # Convert a list of pairs into signature string format
9
+ #
10
+ # @param pairs [Array of [String, String]] String pairs to format
11
+ #
12
+ # As per the last part of
13
+ # https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.3
14
+ # the format is:
15
+ #
16
+ # header: value -- for static http headers
17
+ def pairs_to_payload(pairs)
18
+ pairs.map { |k, v| "#{k}: #{v}" }.join("\n")
19
+ end
20
+
21
+ # Convert a list of pairs into signature string format
22
+ #
23
+ # @param pairs [Array of [String, String]] String pairs to format
24
+ #
25
+ # As per the last part of
26
+ # https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.3
27
+ # the format is:
28
+ #
29
+ # header: value -- for static http headers
30
+ def pairs_to_quoted_string(pairs)
31
+ pairs.map do |key, value|
32
+ value = value.to_s.gsub('"', '\\"') # escape inner quotes
33
+ %(#{key}="#{value}")
34
+ end.join(",")
35
+ end
36
+
37
+ # Parse an http header list into a ruby list. This is not a regexable (at
38
+ # least not a simple regex can do it so far) since the quoted values can
39
+ # have commas. Is written as a tokenizer for this reason.
40
+ #
41
+ # @param list_string [String] HTTP List string
42
+ #
43
+ # example input: 'Signature realm="Use your developer account",headers="(created)", Basic realm="somerealm"'
44
+ #
45
+ # example output: ['Signature realm="Use your developer account"', 'headers="(created)"', 'Basic realm="somerealm"']
46
+ def parse_list_header(list_string)
47
+ res = []
48
+ part = ""
49
+ escape = quote = false
50
+
51
+ list_string.each_char do |char|
52
+ # if in escape mode, add the escaped character and reset
53
+ # the escape flag.
54
+ if escape
55
+ part += char
56
+ escape = false
57
+ next
58
+ end
59
+
60
+ # if in quote mode
61
+ if quote
62
+ case char
63
+ # check for a escape sequence and skip the escape character
64
+ when "\\"
65
+ escape = true
66
+ next
67
+ # check a quote and disable quote mode
68
+ when '"'
69
+ quote = false
70
+ end
71
+
72
+ # add the character to the part but don't process anything further
73
+ # quoted commas are meant to be kept as values
74
+ part += char
75
+ next
76
+ end
77
+
78
+ # in normal case
79
+ case char
80
+ # comma is a signal for a new item, save the current part & reset.
81
+ when ","
82
+ res << part
83
+ part = ""
84
+ next
85
+ # quote enables quote mode
86
+ when '"'
87
+ quote = true
88
+ end
89
+
90
+ # add the character to the part
91
+ part += char
92
+ end
93
+
94
+ # if there's any pending part, add it to the final result
95
+ if part
96
+ res << part
97
+ end
98
+
99
+ # strip the surrounding spaces & remove the empty items
100
+ res.map(&:strip).reject(&:empty?)
101
+ end
102
+
103
+ # Parse an WWW-Authenticate Header a hash of challenges
104
+ #
105
+ # @param header [String] WWW-Authenticate Header
106
+ #
107
+ # The WWW-Authenticate grammar is defined in
108
+ # https://datatracker.ietf.org/doc/html/rfc7235#section-4.1
109
+ #
110
+ # given this input: 'Signature realm="yourealm",headers="abcdef"'
111
+ # it returns
112
+ # { Signature: {realm: "yourealm", headers: "abcdef"} }
113
+ #
114
+ # This implementation is generic enough to get information about multiple
115
+ # challenges as long as they're not the same.
116
+ def parse_authorization_header(header)
117
+ result = {}
118
+ current_section = nil
119
+
120
+ parse_list_header(header).each do |item|
121
+ result[item] = nil and next unless item.include?("=")
122
+
123
+ name, value = item.split("=", 2)
124
+
125
+ if name.include?(" ")
126
+ current_section, name = name.split(" ", 2)
127
+ end
128
+
129
+ if value[0] == value[-1] && value[0] == '"'
130
+ value = unquote(value[1..])
131
+ end
132
+
133
+ (result[current_section.to_sym] ||= {})[name.to_sym] = value
134
+ end
135
+
136
+ result
137
+ end
138
+
139
+ # Remove quotes from string
140
+ #
141
+ # @param string [String] String to be unquoted
142
+ def unquote(string)
143
+ s = string.dup
144
+
145
+ case string[0, 1]
146
+ when "'", '"', "`"
147
+ s[0] = ""
148
+ end
149
+
150
+ case string[-1, 1]
151
+ when "'", '"', "`"
152
+ s[-1] = ""
153
+ end
154
+
155
+ s
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Faraday
4
+ module Middlewares
5
+ module BuildService
6
+ class MissingCredentialsError < BaseError; end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Faraday
4
+ module Middlewares
5
+ module BuildService
6
+ class RetryWithAuthError < BaseError; end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ module Faraday
5
+ module Middlewares
6
+ module BuildService
7
+ class SshSigner
8
+ include HttpHelpers
9
+ # Initalizes the signer
10
+ #
11
+ # @param challenge_params [Hash] WWW-Authenticate header parameters.
12
+ # @param headers [Hash] Request headers
13
+ # @param credentials [Hash] Credentials to sign the request
14
+ # Is expected to have :username & :ssh_key.
15
+ # @param challenge_context [String] Additional context headers for the challenge.
16
+ #
17
+ # challenge_params keys are:
18
+ # :realm => The realm to perform authentication on.
19
+ # :headers => a string list of headers to be used.
20
+ def initialize(challenge_params, headers, credentials: {}, challenge_context: {})
21
+ @headers = headers
22
+ @credentials = credentials
23
+ # The challenge is formatted according to RFC7235 section 4.1
24
+ # https://datatracker.ietf.org/doc/html/rfc7235#section-4.1
25
+ # www-authenticate: 'Signature realm="Use your developer account",headers="(created)"'
26
+ #
27
+ # This signer does only process Signature challenge
28
+ @challenge_params = challenge_params
29
+
30
+ created = Time.now.to_i
31
+ # funny tip: you can bucket requests into 5 min blocks and the signature
32
+ # will still be constant & valid.
33
+ # created = created - (created % 300)
34
+ @challenge_context = {
35
+ created: created
36
+ }.update(challenge_context)
37
+
38
+ # As per RFC Signing HTTP Messages draft-cavage-http-signatures-12 section
39
+ # 2.3. At the very least `(created)` header (and `(request-target)` but Build
40
+ # Service implementation doesn't include it) should be provided to the
41
+ # signature string.
42
+
43
+ # (created) represents a unix timestamp that must be provided into the
44
+ # final signature for the server to validate.
45
+ end
46
+
47
+ # Generate Signature
48
+ #
49
+ # as per https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.1
50
+ #
51
+ # a `signing_string` is created based on the headers that the challenge demands
52
+ # the format for such signing_string (also referred as payload) is:
53
+ #
54
+ # (calculated header): %value%\n
55
+ # host: your.host
56
+ #
57
+ # the \n is to show that a newline is needed there. Then this full string is
58
+ # passed to the signing algorithm, in the build service case is:
59
+ #
60
+ # ssh-keygen -Y sign -f "%path-to-privk%" -n "%realm%" -q <<< "signing string"
61
+ #
62
+ # the result of that signature is in OpenSSH Signature format. Delimiters are
63
+ # removed & newlines stripped.
64
+ #
65
+ # This final result along with the parameters used to generate the signature
66
+ # are concatenated in an HTTP List format and returned to the caller to use.
67
+ def generate
68
+ # first listify the headers in the challenge. As the spec
69
+ @challenge_params[:headers] ||= "(created)"
70
+
71
+ headers = @challenge_params[:headers].split
72
+
73
+ # Iterate through it to build the singing string
74
+ payload_pairs = headers.map do |header|
75
+ value = if header.include?("(")
76
+ # if it's a calculated header (surrounded by parenthesis), look for it
77
+ # in the challenge_context
78
+ @challenge_context[header.tr("()", "").to_sym]
79
+ else
80
+ # else look for it in the given headers
81
+ @headers[header]
82
+ end
83
+
84
+ [header, value]
85
+ end
86
+
87
+ # Build the signing string
88
+ signing_string = pairs_to_payload(payload_pairs)
89
+
90
+ # remove parenthesis from the headers to append to the signature parameters
91
+ payload_pairs = payload_pairs.map do |key, value|
92
+ key = key.tr("()", "")
93
+ [key, value]
94
+ end
95
+
96
+ # sign with the provided realm (or empty if not present)
97
+ signature = ssh_sign(signing_string, @challenge_params[:realm] || "", @credentials[:ssh_key])
98
+
99
+ # Build all signature parameters
100
+ signature_parameters = [
101
+ ["keyId", @credentials[:username]],
102
+ ["algorithm", "ssh"],
103
+ ["signature", signature],
104
+ ["headers", @challenge_params[:headers]],
105
+ *payload_pairs
106
+ ]
107
+
108
+ # build the HTTP quoted list & return
109
+ pairs_to_quoted_string(signature_parameters)
110
+ end
111
+
112
+ # Sign a given payload pertaining to a specific realm
113
+ #
114
+ # @param payload [String] Payload to sign
115
+ # @param realm [String] Realm or Purpose (see man ssh-keygen section -Y sign)
116
+ # @param ssh_key [Hash] SSH Private key to use in the singing
117
+ #
118
+ # The SSH Privatekey will briefly be materialized as a file in the filesystem
119
+ # for SSH to be able to pick it up and sign the payload.
120
+ #
121
+ # Then it'll be removed
122
+ def ssh_sign(payload, realm, ssh_key)
123
+ Tempfile.create("ssh-key", Rails.root.join("tmp/")) do |file|
124
+ file.write(ssh_key)
125
+ file.flush # force IO flush
126
+
127
+ cmd = ["ssh-keygen", "-Y", "sign", "-f", file.path.to_s, "-q", "-n", realm]
128
+ stdout, stderr, process_status = Open3.capture3({}, *cmd, stdin_data: payload)
129
+ raise "cannot sign: #{stderr}" unless process_status.to_i.zero?
130
+
131
+ # remove the surrounding --- blocks & join in one single line
132
+ signature = stdout.split("\n").slice(1..-2).join.presence
133
+
134
+ # this should never happen, ssh-keygen ALWAYS returns an armored format or fail
135
+ unless signature
136
+ raise "cannot sign: bad output"
137
+ end
138
+
139
+ signature
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Faraday
4
+ module Middlewares
5
+ module BuildService
6
+ class UnknownChallengeError < BaseError; end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Faraday
4
+ module Middlewares
5
+ module BuildService
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "build_service/version"
4
+
5
+ require_relative "build_service/base_error"
6
+ require_relative "build_service/missing_credentials_error"
7
+ require_relative "build_service/retry_with_auth_error"
8
+ require_relative "build_service/unknown_challenge_error"
9
+
10
+ require_relative "build_service/http_helpers"
11
+ require_relative "build_service/ssh_signer"
12
+
13
+ require_relative "build_service/authentication"
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: faraday-middlewares-build_service
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jose D. Gomez R.
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-11-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: http-cookie
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ description: |
42
+ Faraday middleware to automatically negotiate authentication with Build Service instances.
43
+
44
+ It can handle: No, Basic, and, Signature authentication.
45
+ email:
46
+ - jose.gomez@suse.com
47
+ executables: []
48
+ extensions: []
49
+ extra_rdoc_files: []
50
+ files:
51
+ - ".rspec"
52
+ - ".standard.yml"
53
+ - CHANGELOG.md
54
+ - Gemfile
55
+ - Gemfile.lock
56
+ - LICENSE
57
+ - README.md
58
+ - Rakefile
59
+ - faraday-middlewares-build_service.gemspec
60
+ - lib/faraday/middlewares/build_service.rb
61
+ - lib/faraday/middlewares/build_service/authentication.rb
62
+ - lib/faraday/middlewares/build_service/base_error.rb
63
+ - lib/faraday/middlewares/build_service/http_helpers.rb
64
+ - lib/faraday/middlewares/build_service/missing_credentials_error.rb
65
+ - lib/faraday/middlewares/build_service/retry_with_auth_error.rb
66
+ - lib/faraday/middlewares/build_service/ssh_signer.rb
67
+ - lib/faraday/middlewares/build_service/unknown_challenge_error.rb
68
+ - lib/faraday/middlewares/build_service/version.rb
69
+ homepage: https://github.com/SUSE/faraday-middlewares-bs-auth
70
+ licenses:
71
+ - MIT
72
+ metadata:
73
+ homepage_uri: https://github.com/SUSE/faraday-middlewares-bs-auth
74
+ source_code_uri: https://github.com/SUSE/faraday-middlewares-bs-auth
75
+ changelog_uri: https://github.com/SUSE/faraday-middlewares-bs-auth/blob/master/CHANGELOG.md
76
+ post_install_message:
77
+ rdoc_options: []
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: 2.6.0
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubygems_version: 3.4.10
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: Plug-and-play authentication for requests against Build Service
95
+ test_files: []