conjur-rack 1.4.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 90340dd3a8dbc2f410a43b66ef10d010e76a9ffd
4
- data.tar.gz: 9cc5f13b006ac102df5a28b65da08e7f3a7ef6dd
2
+ SHA256:
3
+ metadata.gz: bc0ff03755a64c04c14e15a5648c01484872315ac6e04904815ed93a115a569e
4
+ data.tar.gz: e7fd818c8efff7d6037c632117d8f73e021c185ebf5bebc08feeae75e88d72e7
5
5
  SHA512:
6
- metadata.gz: 9223bfe5621080f8b9dba63e2de5593e2f6e44d2ad34d3beb60c8447a0ff8340eae3d1530420975052cfcbe6d26a66e2bbc5c8a1737f0019a2ee437c0fe458a3
7
- data.tar.gz: 58042ba2ee61e535da72b1395ab7f48e1a873d8fba78fdc0ac1ab1a01a15b3dda63e7ba78fe83a550876c180238fbe5d38917d50f5c0485cf8dba1f42bfdbb67
6
+ metadata.gz: 55a29ba4ee7e25e26514298c780c77a779252168419b516efc238af619a58a809faea7ba2664130876a43ad3d74929394f473532054c6b83dc16651f7141291b
7
+ data.tar.gz: e27d9e5cb3e26bbf5bef707dfb51e1509b1957f9a26a54b12467e046160f3097e6a776247f308364a8f5a7a8dd904e02f8a00d112c24c81c7ef2772cc0c3402c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,47 @@
1
+ # unreleased version
2
+
3
+ # v5.0.0
4
+
5
+ * Support Ruby 3.
6
+ * Bump `slosilo` to v3.0 with ruby 3.
7
+ * Remove pinned `bundler` version, use default system bundler.
8
+
9
+ # v4.2.0
10
+
11
+ * Bump `slosilo` to v2.2 in order to be FIPS compliant
12
+
13
+ # v4.0.0
14
+
15
+ * Bump `rack` to v2, `bundler` to v1.16 in gemspec
16
+ * Add Jenkinsfile to project
17
+ * Ignore headers such as Conjur-Privilege or Conjur-Audit if they're not
18
+ supported by the API (instead of erroring out).
19
+
20
+ # v3.1.0
21
+
22
+ * Support for JWT Slosilo tokens.
23
+
24
+ # v3.0.0.pre
25
+
26
+ * Initial support for Conjur 5.
27
+
28
+ # v2.3.0
29
+
30
+ * Add TRUSTED_PROXIES support
31
+
32
+ # v2.2.0
33
+
34
+ * resolve 'own' token to CONJUR_ACCOUNT env var
35
+ * add #optional paths to Conjur::Rack authenticator
36
+
37
+ # v2.1.0
38
+
39
+ * Add handling for `Conjur-Audit-Roles` and `Conjur-Audit-Resources`
40
+
41
+ # v2.0.0
42
+
43
+ * Change `global_sudo?` to `global_elevate?`
44
+
1
45
  # v1.4.0
2
46
 
3
47
  * Add `validated_global_privilege` helper function to get the global privilege, if any, which has been submitted with the request and verified by the Conjur server.
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,16 @@
1
+ # Contributing
2
+
3
+ For general contribution and community guidelines, please see the [community repo](https://github.com/cyberark/community).
4
+
5
+ ## Contributing Workflow
6
+
7
+ 1. [Fork the project](https://help.github.com/en/github/getting-started-with-github/fork-a-repo)
8
+ 2. [Clone your fork](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository)
9
+ 3. Make local changes to your fork by editing files
10
+ 3. [Commit your changes](https://help.github.com/en/github/managing-files-in-a-repository/adding-a-file-to-a-repository-using-the-command-line)
11
+ 4. [Push your local changes to the remote server](https://help.github.com/en/github/using-git/pushing-commits-to-a-remote-repository)
12
+ 5. [Create new Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork)
13
+
14
+ From here your pull request will be reviewed and once you've responded to all
15
+ feedback it will be merged into the project. Congratulations, you're a
16
+ contributor!
data/Gemfile CHANGED
@@ -1,6 +1,12 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
+ # make sure github uses TLS
4
+ git_source(:github) { |name| "https://github.com/#{name}.git" }
5
+
6
+ #ruby=ruby-3.0
7
+ #ruby-gemset=conjur-rack
8
+
3
9
  # Specify your gem's dependencies in conjur-rack.gemspec
4
10
  gemspec
5
11
 
6
- gem 'conjur-api', github: 'conjurinc/api-ruby', branch: 'master'
12
+ # gem 'conjur-api', github: 'cyberark/conjur-api-ruby', branch: 'master'
data/Jenkinsfile ADDED
@@ -0,0 +1,63 @@
1
+ pipeline {
2
+ agent { label 'executor-v2' }
3
+
4
+ options {
5
+ timestamps()
6
+ buildDiscarder(logRotator(daysToKeepStr: '30'))
7
+ }
8
+
9
+ stages {
10
+ stage('Run tests') {
11
+ steps {
12
+ sh './test.sh'
13
+
14
+ junit 'spec/reports/*.xml'
15
+ }
16
+ }
17
+
18
+ // Only publish to RubyGems if the HEAD is
19
+ // tagged with the same version as in version.rb
20
+ stage('Publish to RubyGems') {
21
+ agent { label 'executor-v2' }
22
+
23
+ when {
24
+ expression { currentBuild.resultIsBetterOrEqualTo('SUCCESS') }
25
+ branch "master"
26
+ expression {
27
+ def exitCode = sh returnStatus: true, script: ''' set +x
28
+ echo "Determining if publishing is requested..."
29
+
30
+ VERSION=`cat lib/conjur/rack/version.rb | grep VERSION | sed 's/.* "//;s/"//'`
31
+ echo Declared version: $VERSION
32
+
33
+ # Jenkins git plugin is broken and always fetches with `--no-tags`
34
+ # (or `--tags`, neither of which is what you want), so tags end up
35
+ # not being fetched. Try to fix that.
36
+ # (Unfortunately this fetches all remote heads, so we may have to find
37
+ # another solution for bigger repos.)
38
+ git fetch -q
39
+
40
+ # note when tag not found git rev-parse will just print its name
41
+ TAG=`git rev-list -n 1 "v$VERSION" 2>/dev/null || :`
42
+ echo Tag v$VERSION: $TAG
43
+
44
+ HEAD=`git rev-parse HEAD`
45
+ echo HEAD: $HEAD
46
+
47
+ test "$HEAD" = "$TAG"
48
+ '''
49
+ return exitCode == 0
50
+ }
51
+ }
52
+ steps {
53
+ sh './publish.sh'
54
+ }
55
+ }
56
+ }
57
+
58
+ post {
59
+ always {
60
+ cleanupAndNotify(currentBuild.currentResult)
61
+ }
62
+ }
63
+ }
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013 Kevin Gilpin
1
+ Copyright (c) 2020 CyberArk Software Ltd. All rights reserved.
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -22,8 +22,6 @@ TODO: Write usage instructions here
22
22
 
23
23
  ## Contributing
24
24
 
25
- 1. Fork it
26
- 2. Create your feature branch (`git checkout -b my-new-feature`)
27
- 3. Commit your changes (`git commit -am 'Add some feature'`)
28
- 4. Push to the branch (`git push origin my-new-feature`)
29
- 5. Create new Pull Request
25
+ We welcome contributions of all kinds to this repository. For instructions on
26
+ how to get started and descriptions of our development workflows, please see our
27
+ [contributing guide](CONTRIBUTING.md).
data/conjur-rack.gemspec CHANGED
@@ -18,12 +18,15 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "slosilo"
22
- spec.add_dependency "conjur-api", ">= 4.17"
23
- spec.add_dependency "rack"
24
-
25
- spec.add_development_dependency "bundler", "~> 1.3"
21
+ spec.add_dependency "slosilo", "~> 3.0"
22
+ spec.add_dependency "conjur-api", "< 6"
23
+ spec.add_dependency "rack", "~> 2"
24
+
26
25
  spec.add_development_dependency "rake"
27
- spec.add_development_dependency "rspec", ">=2.9", "<3.0"
26
+ spec.add_development_dependency "rspec"
27
+ spec.add_development_dependency "activesupport", "< 7"
28
28
  spec.add_development_dependency 'ci_reporter_rspec'
29
+ spec.add_development_dependency 'pry-byebug'
30
+ spec.add_development_dependency 'rspec-its'
31
+
29
32
  end
@@ -2,36 +2,51 @@ require "conjur/rack/user"
2
2
 
3
3
  module Conjur
4
4
  module Rack
5
- def self.identity?
6
- !Thread.current[:conjur_rack_identity].nil?
7
- end
8
-
9
- def self.user
10
- User.new(identity[0], identity[1], privilege, remote_ip)
11
- end
12
-
13
- def self.identity
14
- Thread.current[:conjur_rack_identity] or raise "No Conjur identity for current request"
15
- end
16
-
17
- def self.privilege
18
- Thread.current[:conjur_rack_privilege]
19
- end
20
-
21
- def self.remote_ip
22
- Thread.current[:conjur_rack_remote_ip]
5
+
6
+ class << self
7
+ def conjur_rack
8
+ Thread.current[:conjur_rack] ||= {}
9
+ end
10
+
11
+ def identity?
12
+ !conjur_rack[:identity].nil?
13
+ end
14
+
15
+ def user
16
+ User.new(identity[0], identity[1],
17
+ :privilege => privilege,
18
+ :remote_ip => remote_ip,
19
+ :audit_roles => audit_roles,
20
+ :audit_resources => audit_resources
21
+ )
22
+ end
23
+
24
+ def identity
25
+ conjur_rack[:identity] or raise "No Conjur identity for current request"
26
+ end
27
+
28
+ # class attributes
29
+ [:privilege, :remote_ip, :audit_roles, :audit_resources].each do |a|
30
+ define_method(a) do
31
+ conjur_rack[a]
32
+ end
33
+ end
23
34
  end
35
+
24
36
 
25
37
  class Authenticator
26
38
  class AuthorizationError < SecurityError
27
39
  end
28
40
  class SignatureError < SecurityError
29
41
  end
42
+ class Forbidden < SecurityError
43
+ end
30
44
 
31
45
  attr_reader :app, :options
32
46
 
33
47
  # +options+:
34
- # :except :: a list of request path patterns for which to skip authentication
48
+ # :except :: a list of request path patterns for which to skip authentication.
49
+ # :optional :: request path patterns for which authentication is optional.
35
50
  def initialize app, options = {}
36
51
  @app = app
37
52
  @options = options
@@ -39,10 +54,13 @@ module Conjur
39
54
 
40
55
  # threadsafe accessors, values are established explicitly below
41
56
  def env; Thread.current[:rack_env] ; end
42
- def token; Thread.current[:conjur_rack_token] ; end
43
- def account; Thread.current[:conjur_rack_account]; end
44
- def privilege; Thread.current[:conjur_rack_privilege]; end
45
- def remote_ip; Thread.current[:conjur_rack_remote_ip]; end
57
+
58
+ # instance attributes
59
+ [:token, :account, :privilege, :remote_ip, :audit_roles, :audit_resources].each do |a|
60
+ define_method(a) do
61
+ conjur_rack[a]
62
+ end
63
+ end
46
64
 
47
65
  def call rackenv
48
66
  # never store request-specific variables as application attributes
@@ -50,13 +68,19 @@ module Conjur
50
68
  if authenticate?
51
69
  begin
52
70
  identity = verify_authorization_and_get_identity # [token, account]
53
-
54
- Thread.current[:conjur_rack_token] = identity[0]
55
- Thread.current[:conjur_rack_account] = identity[1]
56
- Thread.current[:conjur_rack_identity] = identity
57
- Thread.current[:conjur_rack_privilege] = conjur_privilege
58
- Thread.current[:conjur_rack_remote_ip] = remote_ip
71
+
72
+ if identity
73
+ conjur_rack[:token] = identity[0]
74
+ conjur_rack[:account] = identity[1]
75
+ conjur_rack[:identity] = identity
76
+ conjur_rack[:privilege] = http_privilege
77
+ conjur_rack[:remote_ip] = http_remote_ip
78
+ conjur_rack[:audit_roles] = http_audit_roles
79
+ conjur_rack[:audit_resources] = http_audit_resources
80
+ end
59
81
 
82
+ rescue Forbidden
83
+ return error 403, $!.message
60
84
  rescue SecurityError, RestClient::Exception
61
85
  return error 401, $!.message
62
86
  end
@@ -65,61 +89,108 @@ module Conjur
65
89
  @app.call rackenv
66
90
  ensure
67
91
  Thread.current[:rack_env] = nil
68
- Thread.current[:conjur_rack_identity] = nil
69
- Thread.current[:conjur_rack_token] = nil
70
- Thread.current[:conjur_rack_account] = nil
71
- Thread.current[:conjur_rack_privilege] = nil
72
- Thread.current[:conjur_rack_remote_ip] = nil
92
+ Thread.current[:conjur_rack] = {}
73
93
  end
74
94
  end
75
95
 
76
96
  protected
77
97
 
98
+ def conjur_rack
99
+ Conjur::Rack.conjur_rack
100
+ end
101
+
78
102
  def validate_token_and_get_account token
79
- failure = SignatureError.new("Unathorized: Invalid token")
103
+ failure = SignatureError.new("Unauthorized: Invalid token")
80
104
  raise failure unless (signer = Slosilo.token_signer token)
81
- raise failure unless signer =~ /\Aauthn:(.+)\z/
82
- return $1
105
+ if signer == 'own'
106
+ ENV['CONJUR_ACCOUNT'] or raise failure
107
+ else
108
+ raise failure unless signer =~ /\Aauthn:(.+)\z/
109
+ $1
110
+ end
83
111
  end
84
112
 
85
113
  def error status, message
86
114
  [status, { 'Content-Type' => 'text/plain', 'Content-Length' => message.length.to_s }, [message] ]
87
115
  end
116
+
117
+ def parsed_token
118
+ token = http_authorization.to_s[/^Token token="(.*)"/, 1]
119
+ token = token && JSON.parse(Base64.decode64(token))
120
+ token = Slosilo::JWT token rescue token
121
+ rescue JSON::ParserError
122
+ raise AuthorizationError.new("Malformed authorization token")
123
+ end
88
124
 
125
+ RECOGNIZED_CLAIMS = [
126
+ 'iat', 'exp', # recognized by Slosilo
127
+ 'cidr', 'sub',
128
+ 'iss', 'aud', 'jti' # RFC 7519, not handled but recognized
129
+ ].freeze
130
+
89
131
  def verify_authorization_and_get_identity
90
- if authorization.to_s[/^Token token="(.*)"/]
91
- token = JSON.parse(Base64.decode64($1))
92
- account = validate_token_and_get_account(token)
93
- return [token, account]
132
+ if token = parsed_token
133
+ begin
134
+ account = validate_token_and_get_account token
135
+ if token.respond_to?(:claims)
136
+ claims = token.claims
137
+ raise AuthorizationError, "token contains unrecognized claims" unless \
138
+ (claims.keys.map(&:to_s) - RECOGNIZED_CLAIMS).empty?
139
+ if (cidr = claims['cidr'])
140
+ raise Forbidden, "IP address rejected" unless \
141
+ cidr.map(&IPAddr.method(:new)).any? { |c| c.include? http_remote_ip }
142
+ end
143
+ end
144
+ return [token, account]
145
+ end
94
146
  else
95
- raise AuthorizationError.new("Authorization missing")
147
+ path = http_path
148
+ if optional_paths.find{|p| p.match(path)}.nil?
149
+ raise AuthorizationError.new("Authorization missing")
150
+ else
151
+ nil
152
+ end
96
153
  end
97
154
  end
98
155
 
99
156
  def authenticate?
157
+ path = http_path
100
158
  if options[:except]
101
159
  options[:except].find{|p| p.match(path)}.nil?
102
160
  else
103
161
  true
104
162
  end
105
163
  end
106
-
107
- def conjur_privilege
108
- env['HTTP_X_CONJUR_PRIVILEGE']
109
- end
110
164
 
111
- def authorization
165
+ def optional_paths
166
+ options[:optional] || []
167
+ end
168
+
169
+ def http_authorization
112
170
  env['HTTP_AUTHORIZATION']
113
171
  end
114
-
115
- def remote_ip
172
+
173
+ def http_privilege
174
+ env['HTTP_X_CONJUR_PRIVILEGE']
175
+ end
176
+
177
+ def http_remote_ip
116
178
  require 'rack/request'
117
179
  ::Rack::Request.new(env).ip
118
180
  end
119
-
120
- def path
181
+
182
+ def http_audit_roles
183
+ env['HTTP_CONJUR_AUDIT_ROLES']
184
+ end
185
+
186
+ def http_audit_resources
187
+ env['HTTP_CONJUR_AUDIT_RESOURCES']
188
+ end
189
+
190
+ def http_path
121
191
  [ env['SCRIPT_NAME'], env['PATH_INFO'] ].join
122
192
  end
193
+
123
194
  end
124
195
  end
125
196
  end
@@ -2,25 +2,32 @@ require 'conjur/api'
2
2
 
3
3
  module Conjur
4
4
  module Rack
5
+ # Token data can be a string (which is the user login), or a Hash.
6
+ # If it's a hash, it should contain the user login keyed by the string 'login'.
7
+ # The rest of the payload is available as +attributes+.
5
8
  class User
6
- attr_accessor :token, :account, :privilege, :remote_ip
9
+ attr_reader :token, :account, :privilege, :remote_ip, :audit_roles, :audit_resources
7
10
 
8
- def initialize(token, account, privilege = nil, remote_ip = nil)
11
+ def initialize(token, account, options = {})
9
12
  @token = token
10
13
  @account = account
11
- @privilege = privilege
12
- @remote_ip = remote_ip
14
+ # Third argument used to be the name of privilege, be
15
+ # backwards compatible:
16
+ if options.respond_to?(:to_str)
17
+ @privilege = options
18
+ else
19
+ @privilege = options[:privilege]
20
+ @remote_ip = options[:remote_ip]
21
+ @audit_roles = options[:audit_roles]
22
+ @audit_resources = options[:audit_resources]
23
+ end
13
24
  end
14
25
 
15
26
  # This file was accidently calling account conjur_account,
16
27
  # I'm adding an alias in case that's going on anywhere else.
17
28
  # -- Jon
18
29
  alias :conjur_account :account
19
- alias :conjur_account= :account=
20
-
21
- def new_association(cls, params = {})
22
- cls.new params.merge({userid: login})
23
- end
30
+ # alias :conjur_account= :account=
24
31
 
25
32
  # Returns the global privilege which was present on the request, if and only
26
33
  # if the user actually has that privilege.
@@ -30,7 +37,9 @@ module Conjur
30
37
  # actually have that privilege according to the Conjur server.
31
38
  def validated_global_privilege
32
39
  unless @validated_global_privilege
33
- @privilege = nil if @privilege && !api.global_privilege_permitted?(@privilege)
40
+ @privilege = nil unless @privilege &&
41
+ api.respond_to?(:global_privilege_permitted?) &&
42
+ api.global_privilege_permitted?(@privilege)
34
43
  @validated_global_privilege = true
35
44
  end
36
45
  @privilege
@@ -41,13 +50,21 @@ module Conjur
41
50
  validated_global_privilege == "reveal"
42
51
  end
43
52
 
44
- # True if and only if the user has valid global 'sudo' privilege.
45
- def global_sudo?
46
- validated_global_privilege == "sudo"
53
+ # True if and only if the user has valid global 'elevate' privilege.
54
+ def global_elevate?
55
+ validated_global_privilege == "elevate"
47
56
  end
48
57
 
49
58
  def login
50
- token["data"] or raise "No data field in token"
59
+ parse_token
60
+
61
+ @login
62
+ end
63
+
64
+ def attributes
65
+ parse_token
66
+
67
+ @attributes || {}
51
68
  end
52
69
 
53
70
  def roleid
@@ -63,17 +80,62 @@ module Conjur
63
80
  def role
64
81
  api.role(roleid)
65
82
  end
66
-
83
+
84
+ def audit_resources
85
+ Conjur::API.decode_audit_ids(@audit_resources) if @audit_resources
86
+ end
87
+
88
+ def audit_roles
89
+ Conjur::API.decode_audit_ids(@audit_roles) if @audit_roles
90
+ end
91
+
67
92
  def api(cls = Conjur::API)
68
93
  args = [ token ]
69
94
  args.push remote_ip if remote_ip
70
95
  api = cls.new_from_token(*args)
71
- if privilege
72
- api.with_privilege(privilege)
96
+
97
+ # These are features not present in some API versions.
98
+ # Test for them and only apply if it makes sense. Ignore otherwise.
99
+ %i(privilege audit_resources audit_roles).each do |feature|
100
+ meth = "with_#{feature}".intern
101
+ if api.respond_to?(meth) && (value = send(feature))
102
+ api = api.send meth, value
103
+ end
104
+ end
105
+
106
+ api
107
+ end
108
+
109
+ protected
110
+
111
+ def parse_token
112
+ return if @login
113
+
114
+ @token = Slosilo::JWT token
115
+ load_jwt token
116
+ rescue ArgumentError
117
+ if data = token['data']
118
+ return load_legacy data
73
119
  else
74
- api
120
+ raise "malformed token"
75
121
  end
76
122
  end
123
+
124
+ def load_legacy data
125
+ if data.is_a?(String)
126
+ @login = token['data']
127
+ elsif data.is_a?(Hash)
128
+ @attributes = token['data'].clone
129
+ @login = @attributes.delete('login') or raise "No 'login' field in token data"
130
+ else
131
+ raise "Expecting String or Hash token data, got #{data.class.name}"
132
+ end
133
+ end
134
+
135
+ def load_jwt jwt
136
+ @attributes = jwt.claims.merge (jwt.header || {}) # just pass all the info
137
+ @login = jwt.claims['sub'] or raise "No 'sub' field in claims"
138
+ end
77
139
  end
78
140
  end
79
141
  end
@@ -1,5 +1,5 @@
1
1
  module Conjur
2
2
  module Rack
3
- VERSION = "1.4.0"
3
+ VERSION = "5.0.0"
4
4
  end
5
5
  end
data/lib/conjur/rack.rb CHANGED
@@ -1,3 +1,26 @@
1
1
  require "conjur/rack/version"
2
2
  require "conjur/rack/authenticator"
3
3
  require "conjur/rack/path_prefix"
4
+ require 'ipaddr'
5
+ require 'set'
6
+
7
+ module TrustedProxies
8
+
9
+ def trusted_proxy?(ip)
10
+ trusted_proxies ? trusted_proxies.any? { |cidr| cidr.include?(ip) } : super
11
+ end
12
+
13
+ def trusted_proxies
14
+ @trusted_proxies || ENV['TRUSTED_PROXIES'].try do |proxies|
15
+ cidrs = Set.new(proxies.split(',') + ['127.0.0.1'])
16
+ @trusted_proxies = cidrs.collect {|cidr| IPAddr.new(cidr) }
17
+ end
18
+ end
19
+
20
+ end
21
+
22
+ module Rack
23
+ class Request
24
+ prepend TrustedProxies
25
+ end
26
+ end
data/publish.sh ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash -ex
2
+
3
+ docker pull registry.tld/conjurinc/publish-rubygem
4
+
5
+ summon --yaml "RUBYGEMS_API_KEY: !var rubygems/api-key" \
6
+ docker run --rm --env-file @SUMMONENVFILE -v "$(pwd)":/opt/src \
7
+ registry.tld/conjurinc/publish-rubygem conjur-rack