conjur-rack 1.4.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
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