diff --git a/Gemfile b/Gemfile index 49afa3b282..4a8f67b20e 100644 --- a/Gemfile +++ b/Gemfile @@ -46,7 +46,6 @@ gem "loofah", ">= 2.2.3" # the branch doesn't immediately break this link gem 'conjur-api', '~> 5.pre' gem 'conjur-policy-parser', path: 'gems/policy-parser' -gem 'conjur-rack', path: 'gems/conjur-rack' gem 'conjur-rack-heartbeat' gem 'rack-rewrite' diff --git a/Gemfile.lock b/Gemfile.lock index b7dafe5d06..815d9669a6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,11 +1,3 @@ -PATH - remote: gems/conjur-rack - specs: - conjur-rack (5.0.0) - conjur-api (< 6) - rack (~> 2) - slosilo (~> 3.0) - PATH remote: gems/policy-parser specs: @@ -511,7 +503,6 @@ DEPENDENCIES conjur-cli (~> 6.2) conjur-debify conjur-policy-parser! - conjur-rack! conjur-rack-heartbeat csr cucumber (~> 7.1) diff --git a/app/controllers/concerns/current_user.rb b/app/controllers/concerns/current_user.rb index 6e12b1e34e..b099b651c9 100644 --- a/app/controllers/concerns/current_user.rb +++ b/app/controllers/concerns/current_user.rb @@ -2,11 +2,11 @@ module CurrentUser extend ActiveSupport::Concern - + included do include TokenUser end - + def current_user? begin current_user @@ -14,14 +14,14 @@ def current_user? nil end end - + def current_user @current_user ||= find_current_user end - + private - + def find_current_user - Role[token_user.roleid] || raise(ApplicationController::Forbidden) + Role[token_user.try(:role_id)] || raise(ApplicationController::Forbidden) end -end \ No newline at end of file +end diff --git a/app/controllers/concerns/token_user.rb b/app/controllers/concerns/token_user.rb index 01e1e9d6ae..1717cbe039 100644 --- a/app/controllers/concerns/token_user.rb +++ b/app/controllers/concerns/token_user.rb @@ -4,10 +4,10 @@ module TokenUser extend ActiveSupport::Concern def token_user? - Conjur::Rack.identity? + request.env['conjur-token-authentication.token_details'].present? end - + def token_user - Conjur::Rack.user + request.env['conjur-token-authentication.token_details'] end end diff --git a/app/controllers/credentials_controller.rb b/app/controllers/credentials_controller.rb index 5a3545ed4d..20feeec201 100644 --- a/app/controllers/credentials_controller.rb +++ b/app/controllers/credentials_controller.rb @@ -63,7 +63,7 @@ def rotate_api_key protected def authenticate_client - authentication.authenticated_role = Role[token_user.roleid] if token_user? + authentication.authenticated_role = Role[token_user.role_id] if token_user? perform_basic_authn raise Unauthorized, "Client not authenticated" unless authentication.authenticated? rescue => e diff --git a/app/controllers/status_controller.rb b/app/controllers/status_controller.rb index d9e9a2159c..bd0687e4a1 100644 --- a/app/controllers/status_controller.rb +++ b/app/controllers/status_controller.rb @@ -21,16 +21,17 @@ def whoami client_ip: request.ip, user_agent: request.user_agent, account: token_user.account, - username: token_user.login, - token_issued_at: Time.at(token_user.token.claims["iat"]) + username: token_user.role_id, + token_issued_at: Time.at(token_user.claims['iat']), + token_expires_at: Time.at(token_user.claims['exp']) }) end def audit_success Audit.logger.log( Audit::Event::Whoami.new( - client_ip: token_user.remote_ip, - role: ::Role.by_login(token_user.login, account: token_user.account), + client_ip: token_user.request_ip, + role: ::Role[token_user.role_id], success: true ) ) diff --git a/app/domain/token_factory.rb b/app/domain/token_factory.rb index 14d09ce5a3..6f0e84f56a 100644 --- a/app/domain/token_factory.rb +++ b/app/domain/token_factory.rb @@ -20,12 +20,35 @@ def signed_token(account:, username:, host_ttl: Rails.application.config.conjur_config.host_authorization_token_ttl, user_ttl: Rails.application.config.conjur_config.user_authorization_token_ttl) - signing_key(account).issue_jwt( - sub: username, - exp: Time.now + offset( - ttl: username.starts_with?('host/') ? host_ttl : user_ttl - ) - ) + + if username.starts_with?('host/') + offset = offset(ttl: host_ttl) + hostname = username.split('/')[1..-1].join('/') + role = Role["#{account}:host:#{hostname}"] + else + offset = offset(ttl: user_ttl) + role = Role["#{account}:user:#{username}"] + end + + raise 'Only hosts and users can use authorization tokens' unless role.present? + + issue_jwt(role: role, expires_in: offset) + end + + def issue_jwt(role:, expires_in:) + now = Time.now.to_i + # binding.pry + signing_key = signing_key(role.account) + claims = { + sub: role.role_id, + exp: now + expires_in, + nbf: now, + iat: now, + iss: 'cyberark/conjur' + } + claims[:restricted_to] = role.restricted_to.split(',').map(&:strip) unless role.restricted_to.blank? + # Add signing key to headers so we can descern which account was used to sign the token + JWT.encode(claims, signing_key.key, 'RS256', x5t: signing_key.fingerprint) end def offset(ttl:) @@ -39,6 +62,7 @@ def offset(ttl:) def parse_ttl(ttl:) # If TTL is an integer, return it return ttl.to_i if ttl.to_i.to_s == ttl.to_s + # Attempt to coerce a string into integer ttl.to_s.to_i end diff --git a/config/environments/development.rb b/config/environments/development.rb index d3b9a48acb..e6ba5ad073 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -15,7 +15,7 @@ # https://guides.rubyonrails.org/configuring.html#actiondispatch-hostauthorization # Accept multiple hosts for parallel tests - config.hosts << /^conjur[0-9]*$/ + config.hosts << /conjur[0-9]*/ # eager_load needed to make authentication work without the hacky # loading code... diff --git a/config/initializers/rack_middleware.rb b/config/initializers/rack_middleware.rb index e3c1e64eed..8ec339bcdc 100644 --- a/config/initializers/rack_middleware.rb +++ b/config/initializers/rack_middleware.rb @@ -3,24 +3,6 @@ # This is where we introduce custom middleware that interacts with Rack # and Rails to change how requests are handled. Rails.application.configure do - # This configures which paths do and do not require token authentication. - # Token authentication is optional for authn routes, and it's not applied at - # all to authentication, host factories, or static assets (e.g. images, CSS) - config.middleware.use(Conjur::Rack::Authenticator, - optional: [ - %r{^/authn-[^/]+/}, - %r{^/authn/}, - %r{^/public_keys/} - ], - except: [ - %r{^/authn-oidc/.*/providers}, - %r{^/authn-[^/]+/.*/authenticate$}, - %r{^/authn/.*/authenticate$}, - %r{^/host_factories/hosts$}, - %r{^/assets/.*}, - %r{^/authenticators$}, - %r{^/$} - ]) # We want to ensure requests have an expected content type # before other middleware runs to make sure any body parsing diff --git a/config/initializers/token_authentication.rb b/config/initializers/token_authentication.rb new file mode 100644 index 0000000000..99ed0d0523 --- /dev/null +++ b/config/initializers/token_authentication.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'rack/token_authentication' + +Rails.application.configure do + # This configures which paths do and do not require token authentication. + # Token authentication is optional for authn routes, and it's not applied at + # all to authentication, host factories, or static assets (e.g. images, CSS) + config.middleware.use(Rack::TokenAuthentication, { + optional: [ + %r{^/authn-[^/]+/}, + %r{^/authn/}, + %r{^/public_keys/} + ], + except: [ + %r{^/authn-oidc/.*/providers}, + %r{^/authn-[^/]+/.*/authenticate$}, + %r{^/authn/.*/authenticate$}, + %r{^/host_factories/hosts$}, + %r{^/assets/.*}, + %r{^/authenticators$}, + %r{^/$} + ] + }) +end diff --git a/dev/start b/dev/start index 43433099d0..1a3fea262e 100755 --- a/dev/start +++ b/dev/start @@ -6,7 +6,7 @@ set -o pipefail # CC servers can't find it for some reason. Local shellcheck is fine. # shellcheck disable=SC1091 -source "../ci/oauth/keycloak/keycloak_functions.sh" +# source "../ci/oauth/keycloak/keycloak_functions.sh" # SCRIPT GLOBAL STATE @@ -18,7 +18,7 @@ fi # Minimal set of services. We add to this list based on cmd line flags. services=(pg conjur client) -# Authenticators to enable. +# Authenticators to enable. default_authenticators="authn,authn-k8s/test" enabled_authenticators="$default_authenticators" @@ -98,7 +98,7 @@ Usage: start [options] --authn-gcp Starts with authn-gcp as authenticator --authn-iam Starts with authn-iam/prod as authenticator --authn-jwt Starts with authn-jwt as authenticator - --authn-ldap Starts OpenLDAP server and loads a demo policy to enable + --authn-ldap Starts OpenLDAP server and loads a demo policy to enable authentication via: 'curl -X POST -d "alice" http://localhost:3000/authn-ldap/test/cucumber/alice/authenticate' -h, --help Shows this help message. diff --git a/gems/conjur-rack/CHANGELOG.md b/gems/conjur-rack/CHANGELOG.md deleted file mode 100644 index 2289ed3172..0000000000 --- a/gems/conjur-rack/CHANGELOG.md +++ /dev/null @@ -1,54 +0,0 @@ -**This Gem has been moved into Conjur. All Conjur Rack Changelog entries should -appear in the main Changelog.** - -# unreleased version - -# v5.0.0 - -* Support Ruby 3. -* Bump `slosilo` to v3.0 with ruby 3. -* Remove pinned `bundler` version, use default system bundler. - -# v4.2.0 - -* Bump `slosilo` to v2.2 in order to be FIPS compliant - -# v4.0.0 - -* Bump `rack` to v2, `bundler` to v1.16 in gemspec -* Add Jenkinsfile to project -* Ignore headers such as Conjur-Privilege or Conjur-Audit if they're not -supported by the API (instead of erroring out). - -# v3.1.0 - -* Support for JWT Slosilo tokens. - -# v3.0.0.pre - -* Initial support for Conjur 5. - -# v2.3.0 - -* Add TRUSTED_PROXIES support - -# v2.2.0 - -* resolve 'own' token to CONJUR_ACCOUNT env var -* add #optional paths to Conjur::Rack authenticator - -# v2.1.0 - -* Add handling for `Conjur-Audit-Roles` and `Conjur-Audit-Resources` - -# v2.0.0 - -* Change `global_sudo?` to `global_elevate?` - -# v1.4.0 - -* 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. - -# v1.3.0 - -* Add handling for `X-Forwarded-For` and `X-Conjur-Privilege` diff --git a/gems/conjur-rack/CONTRIBUTING.md b/gems/conjur-rack/CONTRIBUTING.md deleted file mode 100644 index 7c0a67db14..0000000000 --- a/gems/conjur-rack/CONTRIBUTING.md +++ /dev/null @@ -1,16 +0,0 @@ -# Contributing - -For general contribution and community guidelines, please see the [community repo](https://github.com/cyberark/community). - -## Contributing Workflow - -1. [Fork the project](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) -2. [Clone your fork](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository) -3. Make local changes to your fork by editing files -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) -4. [Push your local changes to the remote server](https://help.github.com/en/github/using-git/pushing-commits-to-a-remote-repository) -5. [Create new Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork) - -From here your pull request will be reviewed and once you've responded to all -feedback it will be merged into the project. Congratulations, you're a -contributor! diff --git a/gems/conjur-rack/Gemfile b/gems/conjur-rack/Gemfile deleted file mode 100644 index ba44e6d927..0000000000 --- a/gems/conjur-rack/Gemfile +++ /dev/null @@ -1,12 +0,0 @@ -source 'https://rubygems.org' - -# make sure github uses TLS -git_source(:github) { |name| "https://github.com/#{name}.git" } - -#ruby=ruby-3.0 -#ruby-gemset=conjur-rack - -# Specify your gem's dependencies in conjur-rack.gemspec -gemspec - -# gem 'conjur-api', github: 'cyberark/conjur-api-ruby', branch: 'master' diff --git a/gems/conjur-rack/LICENSE.txt b/gems/conjur-rack/LICENSE.txt deleted file mode 100644 index 069db73dc1..0000000000 --- a/gems/conjur-rack/LICENSE.txt +++ /dev/null @@ -1,22 +0,0 @@ -Copyright (c) 2020 CyberArk Software Ltd. All rights reserved. - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/gems/conjur-rack/README.md b/gems/conjur-rack/README.md deleted file mode 100644 index 98b50e1a96..0000000000 --- a/gems/conjur-rack/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Conjur::Rack - -TODO: Write a gem description - -## Installation - -Add this line to your application's Gemfile: - - gem 'conjur-rack' - -And then execute: - - $ bundle - -Or install it yourself as: - - $ gem install conjur-rack - -## Usage - -TODO: Write usage instructions here - -## Contributing - -We welcome contributions of all kinds to this repository. For instructions on -how to get started and descriptions of our development workflows, please see our -[contributing guide](CONTRIBUTING.md). diff --git a/gems/conjur-rack/Rakefile b/gems/conjur-rack/Rakefile deleted file mode 100644 index d0c3eb3141..0000000000 --- a/gems/conjur-rack/Rakefile +++ /dev/null @@ -1,14 +0,0 @@ -require "bundler/gem_tasks" -require 'rspec/core/rake_task' -require 'ci/reporter/rake/rspec' - -RSpec::Core::RakeTask.new(:spec) do |t| - t.rspec_opts = "--format doc" - unless ENV["CONJUR_ENV"] == "ci" - t.rspec_opts << " --color" - else - Rake::Task["ci:setup:rspec"].invoke - end -end - -task :default => :spec diff --git a/gems/conjur-rack/conjur-rack.gemspec b/gems/conjur-rack/conjur-rack.gemspec deleted file mode 100644 index a4627220a4..0000000000 --- a/gems/conjur-rack/conjur-rack.gemspec +++ /dev/null @@ -1,28 +0,0 @@ -lib = File.expand_path('../lib', __FILE__) -$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require 'conjur/rack/version' - -Gem::Specification.new do |spec| - spec.name = 'conjur-rack' - spec.version = Conjur::Rack::VERSION - spec.authors = ['Cyberark R&D'] - spec.summary = 'Rack authenticator and basic User struct' - spec.homepage = 'http://github.com/conjurinc/conjur-rack' - - spec.files = Dir.glob("lib/**/*") + %w[README.md] - spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } - spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) - spec.require_paths = ['lib'] - spec.required_ruby_version = '>= 2.5' - - spec.add_dependency('conjur-api', '< 6') - spec.add_dependency('rack', '~> 2') - spec.add_dependency('slosilo', '~> 3.0') - - spec.add_development_dependency('activesupport', '< 7') - spec.add_development_dependency('ci_reporter_rspec') - spec.add_development_dependency('pry-byebug') - spec.add_development_dependency('rake') - spec.add_development_dependency('rspec') - spec.add_development_dependency('rspec-its') -end diff --git a/gems/conjur-rack/lib/conjur/rack.rb b/gems/conjur-rack/lib/conjur/rack.rb deleted file mode 100644 index 1a9c353baf..0000000000 --- a/gems/conjur-rack/lib/conjur/rack.rb +++ /dev/null @@ -1,26 +0,0 @@ -require "conjur/rack/version" -require "conjur/rack/authenticator" -require "conjur/rack/path_prefix" -require 'ipaddr' -require 'set' - -module TrustedProxies - - def trusted_proxy?(ip) - trusted_proxies ? trusted_proxies.any? { |cidr| cidr.include?(ip) } : super - end - - def trusted_proxies - @trusted_proxies || ENV['TRUSTED_PROXIES'].try do |proxies| - cidrs = Set.new(proxies.split(',') + ['127.0.0.1']) - @trusted_proxies = cidrs.collect {|cidr| IPAddr.new(cidr) } - end - end - -end - -module Rack - class Request - prepend TrustedProxies - end -end diff --git a/gems/conjur-rack/lib/conjur/rack/authenticator.rb b/gems/conjur-rack/lib/conjur/rack/authenticator.rb deleted file mode 100644 index 902240983b..0000000000 --- a/gems/conjur-rack/lib/conjur/rack/authenticator.rb +++ /dev/null @@ -1,196 +0,0 @@ -require "conjur/rack/user" - -module Conjur - module Rack - - class << self - def conjur_rack - Thread.current[:conjur_rack] ||= {} - end - - def identity? - !conjur_rack[:identity].nil? - end - - def user - User.new(identity[0], identity[1], - :privilege => privilege, - :remote_ip => remote_ip, - :audit_roles => audit_roles, - :audit_resources => audit_resources - ) - end - - def identity - conjur_rack[:identity] or raise "No Conjur identity for current request" - end - - # class attributes - [:privilege, :remote_ip, :audit_roles, :audit_resources].each do |a| - define_method(a) do - conjur_rack[a] - end - end - end - - - class Authenticator - class AuthorizationError < SecurityError - end - class SignatureError < SecurityError - end - class Forbidden < SecurityError - end - - attr_reader :app, :options - - # +options+: - # :except :: a list of request path patterns for which to skip authentication. - # :optional :: request path patterns for which authentication is optional. - def initialize app, options = {} - @app = app - @options = options - end - - # threadsafe accessors, values are established explicitly below - def env; Thread.current[:rack_env] ; end - - # instance attributes - [:token, :account, :privilege, :remote_ip, :audit_roles, :audit_resources].each do |a| - define_method(a) do - conjur_rack[a] - end - end - - def call rackenv - # never store request-specific variables as application attributes - Thread.current[:rack_env] = rackenv - if authenticate? - begin - identity = verify_authorization_and_get_identity # [token, account] - - if identity - conjur_rack[:token] = identity[0] - conjur_rack[:account] = identity[1] - conjur_rack[:identity] = identity - conjur_rack[:privilege] = http_privilege - conjur_rack[:remote_ip] = http_remote_ip - conjur_rack[:audit_roles] = http_audit_roles - conjur_rack[:audit_resources] = http_audit_resources - end - - rescue Forbidden - return error 403, $!.message - rescue SecurityError, RestClient::Exception - return error 401, $!.message - end - end - begin - @app.call rackenv - ensure - Thread.current[:rack_env] = nil - Thread.current[:conjur_rack] = {} - end - end - - protected - - def conjur_rack - Conjur::Rack.conjur_rack - end - - def validate_token_and_get_account token - failure = SignatureError.new("Unauthorized: Invalid token") - raise failure unless (signer = Slosilo.token_signer token) - if signer == 'own' - ENV['CONJUR_ACCOUNT'] or raise failure - else - raise failure unless signer =~ /\Aauthn:(.+)\z/ - $1 - end - end - - def error status, message - [status, { 'Content-Type' => 'text/plain', 'Content-Length' => message.length.to_s }, [message] ] - end - - def parsed_token - token = http_authorization.to_s[/^Token token="(.*)"/, 1] - token = token && JSON.parse(Base64.decode64(token)) - token = Slosilo::JWT token rescue token - rescue JSON::ParserError - raise AuthorizationError.new("Malformed authorization token") - end - - RECOGNIZED_CLAIMS = [ - 'iat', 'exp', # recognized by Slosilo - 'cidr', 'sub', - 'iss', 'aud', 'jti' # RFC 7519, not handled but recognized - ].freeze - - def verify_authorization_and_get_identity - if token = parsed_token - begin - account = validate_token_and_get_account token - if token.respond_to?(:claims) - claims = token.claims - raise AuthorizationError, "token contains unrecognized claims" unless \ - (claims.keys.map(&:to_s) - RECOGNIZED_CLAIMS).empty? - if (cidr = claims['cidr']) - raise Forbidden, "IP address rejected" unless \ - cidr.map(&IPAddr.method(:new)).any? { |c| c.include? http_remote_ip } - end - end - return [token, account] - end - else - path = http_path - if optional_paths.find{|p| p.match(path)}.nil? - raise AuthorizationError.new("Authorization missing") - else - nil - end - end - end - - def authenticate? - path = http_path - if options[:except] - options[:except].find{|p| p.match(path)}.nil? - else - true - end - end - - def optional_paths - options[:optional] || [] - end - - def http_authorization - env['HTTP_AUTHORIZATION'] - end - - def http_privilege - env['HTTP_X_CONJUR_PRIVILEGE'] - end - - def http_remote_ip - require 'rack/request' - ::Rack::Request.new(env).ip - end - - def http_audit_roles - env['HTTP_CONJUR_AUDIT_ROLES'] - end - - def http_audit_resources - env['HTTP_CONJUR_AUDIT_RESOURCES'] - end - - def http_path - [ env['SCRIPT_NAME'], env['PATH_INFO'] ].join - end - - end - end -end diff --git a/gems/conjur-rack/lib/conjur/rack/path_prefix.rb b/gems/conjur-rack/lib/conjur/rack/path_prefix.rb deleted file mode 100644 index 511db284d3..0000000000 --- a/gems/conjur-rack/lib/conjur/rack/path_prefix.rb +++ /dev/null @@ -1,31 +0,0 @@ -# https://raw.github.com/merb/merb/master/merb-core/lib/merb-core/rack/middleware/path_prefix.rb -module Conjur - module Rack - class PathPrefix - EMPTY_STRING = "" - SLASH = "/" - - # @api private - def initialize(app, path_prefix = nil) - @app = app - @path_prefix = /^#{Regexp.escape(path_prefix)}/ - end - - # @api plugin - def call(env) - strip_path_prefix(env) - @app.call(env) - end - - # @api private - def strip_path_prefix(env) - ['PATH_INFO', 'REQUEST_URI'].each do |path_key| - if env[path_key] =~ @path_prefix - env[path_key].sub!(@path_prefix, EMPTY_STRING) - env[path_key] = SLASH if env[path_key].empty? - end - end - end - end - end -end \ No newline at end of file diff --git a/gems/conjur-rack/lib/conjur/rack/user.rb b/gems/conjur-rack/lib/conjur/rack/user.rb deleted file mode 100644 index 9680d2ee5a..0000000000 --- a/gems/conjur-rack/lib/conjur/rack/user.rb +++ /dev/null @@ -1,141 +0,0 @@ -require 'conjur/api' - -module Conjur - module Rack - # Token data can be a string (which is the user login), or a Hash. - # If it's a hash, it should contain the user login keyed by the string 'login'. - # The rest of the payload is available as +attributes+. - class User - attr_reader :token, :account, :privilege, :remote_ip, :audit_roles, :audit_resources - - def initialize(token, account, options = {}) - @token = token - @account = account - # Third argument used to be the name of privilege, be - # backwards compatible: - if options.respond_to?(:to_str) - @privilege = options - else - @privilege = options[:privilege] - @remote_ip = options[:remote_ip] - @audit_roles = options[:audit_roles] - @audit_resources = options[:audit_resources] - end - end - - # This file was accidently calling account conjur_account, - # I'm adding an alias in case that's going on anywhere else. - # -- Jon - alias :conjur_account :account - # alias :conjur_account= :account= - - # Returns the global privilege which was present on the request, if and only - # if the user actually has that privilege. - # - # Returns nil if no global privilege was present in the request headers, - # or if a global privilege was present in the request headers, but the user doesn't - # actually have that privilege according to the Conjur server. - def validated_global_privilege - unless @validated_global_privilege - @privilege = nil unless @privilege && - api.respond_to?(:global_privilege_permitted?) && - api.global_privilege_permitted?(@privilege) - @validated_global_privilege = true - end - @privilege - end - - # True if and only if the user has valid global 'reveal' privilege. - def global_reveal? - validated_global_privilege == "reveal" - end - - # True if and only if the user has valid global 'elevate' privilege. - def global_elevate? - validated_global_privilege == "elevate" - end - - def login - parse_token - - @login - end - - def attributes - parse_token - - @attributes || {} - end - - def roleid - tokens = login.split('/') - role_kind, roleid = if tokens.length == 1 - [ 'user', login ] - else - [ tokens[0], tokens[1..-1].join('/') ] - end - [ account, role_kind, roleid ].join(':') - end - - def role - api.role(roleid) - end - - def audit_resources - Conjur::API.decode_audit_ids(@audit_resources) if @audit_resources - end - - def audit_roles - Conjur::API.decode_audit_ids(@audit_roles) if @audit_roles - end - - def api(cls = Conjur::API) - args = [ token ] - args.push remote_ip if remote_ip - api = cls.new_from_token(*args) - - # These are features not present in some API versions. - # Test for them and only apply if it makes sense. Ignore otherwise. - %i(privilege audit_resources audit_roles).each do |feature| - meth = "with_#{feature}".intern - if api.respond_to?(meth) && (value = send(feature)) - api = api.send meth, value - end - end - - api - end - - protected - - def parse_token - return if @login - - @token = Slosilo::JWT token - load_jwt token - rescue ArgumentError - if data = token['data'] - return load_legacy data - else - raise "malformed token" - end - end - - def load_legacy data - if data.is_a?(String) - @login = token['data'] - elsif data.is_a?(Hash) - @attributes = token['data'].clone - @login = @attributes.delete('login') or raise "No 'login' field in token data" - else - raise "Expecting String or Hash token data, got #{data.class.name}" - end - end - - def load_jwt jwt - @attributes = jwt.claims.merge (jwt.header || {}) # just pass all the info - @login = jwt.claims['sub'] or raise "No 'sub' field in claims" - end - end - end -end diff --git a/gems/conjur-rack/lib/conjur/rack/version.rb b/gems/conjur-rack/lib/conjur/rack/version.rb deleted file mode 100644 index ad0a8d043c..0000000000 --- a/gems/conjur-rack/lib/conjur/rack/version.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Conjur - module Rack - VERSION = "5.0.0" - end -end diff --git a/gems/conjur-rack/spec/rack/authenticator_spec.rb b/gems/conjur-rack/spec/rack/authenticator_spec.rb deleted file mode 100644 index 6c0a1907bb..0000000000 --- a/gems/conjur-rack/spec/rack/authenticator_spec.rb +++ /dev/null @@ -1,181 +0,0 @@ -require 'spec_helper' - -require 'conjur/rack/authenticator' - -describe Conjur::Rack::Authenticator do - include_context "with authenticator" - - describe "#call" do - context "to an unprotected path" do - let(:except) { [ /^\/foo/ ] } - let(:env) { { 'SCRIPT_NAME' => '', 'PATH_INFO' => '/foo/bar' } } - before { - options[:except] = except - expect(app).to receive(:call).with(env).and_return app - } - context "without authorization" do - it "proceeds" do - expect(call).to eq(app) - expect(Conjur::Rack.identity?).to be(false) - end - end - context "with authorization" do - include_context "with authorization" - it "ignores the authorization" do - expect(call).to eq(app) - expect(Conjur::Rack.identity?).to be(false) - end - end - end - - context "to a protected path" do - let(:env) { { 'SCRIPT_NAME' => '/pathname' } } - context "without authorization" do - it "returns a 401 error" do - expect(call).to return_http 401, "Authorization missing" - end - end - context "with Conjur authorization" do - include_context "with authorization" - - context "with CIDR restriction" do - let(:claims) { { 'sub' => 'test-user', 'cidr' => %w(192.168.2.0/24 2001:db8::/32) } } - let(:token) { Slosilo::JWT.new(claims) } - before do - allow(subject).to receive_messages \ - parsed_token: token, - http_remote_ip: remote_ip - end - - %w(10.0.0.2 fdda:5cc1:23:4::1f).each do |addr| - context "with address #{addr} out of range" do - let(:remote_ip) { addr } - it "returns 403" do - expect(call).to return_http 403, "IP address rejected" - end - end - end - - %w(192.168.2.3 2001:db8::22).each do |addr| - context "with address #{addr} in range" do - let(:remote_ip) { addr } - it "passes the request" do - expect(call.login).to eq 'test-user' - end - end - end - end - - context "of a valid token" do - it 'launches app' do - expect(app).to receive(:call).with(env).and_return app - expect(call).to eq(app) - end - end - context "of an invalid token" do - it "returns a 401 error" do - allow(Slosilo).to receive(:token_signer).and_return(nil) - expect(call).to return_http 401, "Unauthorized: Invalid token" - end - end - context "of a token invalid for authn" do - it "returns a 401 error" do - allow(Slosilo).to receive(:token_signer).and_return('a-totally-different-key') - expect(call).to return_http 401, "Unauthorized: Invalid token" - end - end - context "of 'own' token" do - it "returns ENV['CONJUR_ACCOUNT']" do - expect(ENV).to receive(:[]).with("CONJUR_ACCOUNT").and_return("test-account") - expect(app).to receive(:call) do |*args| - expect(Conjur::Rack.identity?).to be(true) - expect(Conjur::Rack.user.account).to eq('test-account') - :done - end - allow(Slosilo).to receive(:token_signer).and_return('own') - expect(call).to eq(:done) - end - it "requires ENV['CONJUR_ACCOUNT']" do - expect(ENV).to receive(:[]).with("CONJUR_ACCOUNT").and_return(nil) - allow(Slosilo).to receive(:token_signer).and_return('own') - expect(call).to return_http 401, "Unauthorized: Invalid token" - end - end - end - - context "with junk in token" do - let(:env) { { 'HTTP_AUTHORIZATION' => 'Token token="open sesame"' } } - it "returns 401" do - expect(call).to return_http 401, "Malformed authorization token" - end - end - - context "with JSON junk in token" do - let(:env) { { 'HTTP_AUTHORIZATION' => 'Token token="eyJmb28iOiAiYmFyIn0="' } } - before do - allow(Slosilo).to receive(:token_signer).and_return(nil) - end - - it "returns 401" do - expect(call).to return_http 401, "Unauthorized: Invalid token" - end - end - end - context "to an optional path" do - let(:optional) { [ /^\/foo/ ] } - let(:env) { { 'SCRIPT_NAME' => '', 'PATH_INFO' => '/foo/bar' } } - before { - options[:optional] = optional - } - context "without authorization" do - it "proceeds" do - expect(app).to receive(:call) do |*args| - expect(Conjur::Rack.identity?).to be(false) - :done - end - expect(call).to eq(:done) - end - end - context "with authorization" do - include_context "with authorization" - it "processes the authorization" do - expect(app).to receive(:call) do |*args| - expect(Conjur::Rack.identity?).to be(true) - :done - end - expect(call).to eq(:done) - end - end - end - - RSpec::Matchers.define :return_http do |status, message| - match do |actual| - status, headers, body = actual - expect(status).to eq status - expect(headers).to eq "Content-Type" => "text/plain", "Content-Length" => message.length.to_s - expect(body.join).to eq message - end - end - end - - # protected internal methods - - describe '#verify_authorization_and_get_identity' do - it "accepts JWT tokens without CIDR restrictions" do - mock_jwt sub: 'user' - expect { subject.send :verify_authorization_and_get_identity }.to_not raise_error - end - - it "rejects JWT tokens with unrecognized claims" do - mock_jwt extra: 'field' - expect { subject.send :verify_authorization_and_get_identity }.to raise_error \ - Conjur::Rack::Authenticator::AuthorizationError - end - - def mock_jwt claims - token = Slosilo::JWT.new(claims).add_signature(alg: 'none') {} - allow(subject).to receive(:parsed_token) { token } - allow(Slosilo).to receive(:token_signer).with(token).and_return 'authn:test' - end - end -end diff --git a/gems/conjur-rack/spec/rack/path_prefix_spec.rb b/gems/conjur-rack/spec/rack/path_prefix_spec.rb deleted file mode 100644 index 41abf9d153..0000000000 --- a/gems/conjur-rack/spec/rack/path_prefix_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'spec_helper' - -require 'conjur/rack/path_prefix' - -describe Conjur::Rack::PathPrefix do - let(:app) { double(:app) } - let(:prefix) { "/api" } - let(:path_prefix) { Conjur::Rack::PathPrefix.new(app, prefix) } - let(:call) { path_prefix.call env } - let(:env) { - { - 'PATH_INFO' => path - } - } - - context "#call" do - context "/api/hosts" do - let(:path) { "/api/hosts" } - it "matches" do - expect(app).to receive(:call).with({ 'PATH_INFO' => '/hosts' }).and_return app - call - end - end - context "/api" do - let(:path) { "/api" } - it "doesn't erase the path completely" do - expect(app).to receive(:call).with({ 'PATH_INFO' => '/' }).and_return app - call - end - end - context "with non-matching prefix" do - let(:path) { "/hosts" } - it "doesn't match" do - expect(app).to receive(:call).with({ 'PATH_INFO' => '/hosts' }).and_return app - call - end - end - end - -end \ No newline at end of file diff --git a/gems/conjur-rack/spec/rack/user_spec.rb b/gems/conjur-rack/spec/rack/user_spec.rb deleted file mode 100644 index e1f138a429..0000000000 --- a/gems/conjur-rack/spec/rack/user_spec.rb +++ /dev/null @@ -1,221 +0,0 @@ -require 'spec_helper' -require 'conjur/rack/user' - -describe Conjur::Rack::User do - let(:login){ 'admin' } - let(:token){ {'data' => login} } - let(:account){ 'acct' } - let(:privilege) { nil } - let(:remote_ip) { nil } - let(:audit_roles) { nil } - let(:audit_resources) { nil } - - subject(:user) { - described_class.new(token, account, - :privilege => privilege, - :remote_ip => remote_ip, - :audit_roles => audit_roles, - :audit_resources => audit_resources - ) - } - - it 'provides field accessors' do - expect(user.token).to eq token - expect(user.account).to eq account - expect(user.conjur_account).to eq account - expect(user.login).to eq login - end - - describe '#roleid' do - let(:login){ tokens.join('/') } - - context "when login contains one token" do - let(:tokens) { %w(foobar) } - - it "is expanded to account:user:token" do - expect(subject.roleid).to eq "#{account}:user:foobar" - end - end - - context "when login contains two tokens" do - let(:tokens) { %w(foo bar) } - - it "is expanded to account:first:second" do - expect(subject.roleid).to eq "#{account}:foo:bar" - end - end - - context "when login contains three tokens" do - let(:tokens) { %w(foo bar baz) } - - it "is expanded to account:first:second/third" do - expect(subject.roleid).to eq "#{account}:foo:bar/baz" - end - end - end - - describe '#role' do - let(:roleid){ 'the role id' } - let(:api){ double('conjur api') } - before do - allow(subject).to receive(:roleid).and_return roleid - allow(subject).to receive(:api).and_return api - end - - it 'passes roleid to api.role' do - expect(api).to receive(:role).with(roleid).and_return 'the role' - expect(subject.role).to eq('the role') - end - end - - describe "#global_reveal?" do - let(:api){ double "conjur-api" } - before { allow(subject).to receive(:api).and_return(api) } - - context "with global privilege" do - let(:privilege) { "reveal" } - - context "when not supported" do - before { expect(api).not_to respond_to :global_privilege_permitted? } - it "simply returns false" do - expect(subject.global_reveal?).to be false - end - end - - context "when supported" do - before do - allow(api).to receive(:global_privilege_permitted?).with('reveal') { true } - end - it "checks the API function global_privilege_permitted?" do - expect(subject.global_reveal?).to be true - # The result is cached - expect(api).not_to receive :global_privilege_permitted? - subject.global_reveal? - end - end - end - - context "without a global privilege" do - it "simply returns false" do - expect(subject.global_reveal?).to be false - end - end - end - - describe '#api' do - context "when given a class" do - let(:cls){ double('API class') } - it "calls cls.new_from_token with its token" do - expect(cls).to receive(:new_from_token).with(token).and_return 'the api' - expect(subject.api(cls)).to eq('the api') - end - end - - context 'when not given args' do - let(:api) { double :api } - before do - allow(Conjur::API).to receive(:new_from_token).with(token).and_return(api) - end - - it "builds the api from token" do - expect(subject.api).to eq api - end - - context "with remote_ip" do - let(:remote_ip) { "the-ip" } - it "passes the IP to the API constructor" do - expect(Conjur::API).to receive(:new_from_token).with(token, 'the-ip').and_return(api) - expect(subject.api).to eq api - end - end - - context "with privilege" do - let(:privilege) { "elevate" } - it "applies the privilege on the API object" do - expect(api).to receive(:with_privilege).with("elevate").and_return "privileged api" - expect(subject.api).to eq "privileged api" - end - end - - context "when audit supported" do - before do - # If we're testing on an API version that doesn't - # support audit this method will be missing, so stub. - unless Conjur::API.respond_to? :decode_audit_ids - # not exactly a faithful reimplementation, but good enough for here - allow(Conjur::API).to receive(:decode_audit_ids) {|x|[x]} - end - end - - context "with audit resource" do - let (:audit_resources) { 'food:bacon' } - it "applies the audit resource on the API object" do - expect(api).to receive(:with_audit_resources).with(['food:bacon']).and_return('the api') - expect(subject.api).to eq 'the api' - end - end - - context "with audit roles" do - let (:audit_roles) { 'user:cook' } - it "applies the audit role on the API object" do - expect(api).to receive(:with_audit_roles).with(['user:cook']).and_return('the api') - expect(subject.api).to eq 'the api' - end - end - end - - context "when audit not supported" do - before do - expect(api).not_to respond_to :with_audit_resources - expect(api).not_to respond_to :with_audit_roles - end - let (:audit_resources) { 'food:bacon' } - let (:audit_roles) { 'user:cook' } - it "ignores audit roles and resources" do - expect(subject.api).to eq api - end - end - end - end - - context "with invalid type payload" do - let(:token){ { "data" => :alice } } - it "raises an error on trying to access the content" do - expect{ subject.login }.to raise_error("Expecting String or Hash token data, got Symbol") - end - end - - context "with hash payload" do - let(:token){ { "data" => { "login" => "alice", "capabilities" => { "fry" => "bacon" } } } } - - it "processes the login and attributes" do - original_token = token.deep_dup - - expect(subject.login).to eq('alice') - expect(subject.attributes).to eq({"capabilities" => { "fry" => "bacon" }}) - - expect(token).to eq original_token - end - end - - context "with JWT token" do - let(:token) { {"protected"=>"eyJhbGciOiJ0ZXN0IiwidHlwIjoiSldUIn0=", - "payload"=>"eyJzdWIiOiJhbGljZSIsImlhdCI6MTUwNDU1NDI2NX0=", - "signature"=>"dGVzdHNpZw=="} } - - it "processes the login and attributes" do - original_token = token.deep_dup - - expect(subject.login).to eq('alice') - - # TODO: should we only pass unrecognized attrs here? - expect(subject.attributes).to eq \ - 'alg' => 'test', - 'iat' => 1504554265, - 'sub' => 'alice', - 'typ' => 'JWT' - - expect(token).to eq original_token - end - end -end diff --git a/gems/conjur-rack/spec/rack_spec.rb b/gems/conjur-rack/spec/rack_spec.rb deleted file mode 100644 index 02464cb21f..0000000000 --- a/gems/conjur-rack/spec/rack_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'spec_helper' -require 'conjur/rack' - -describe Conjur::Rack do - describe '.user' do - include_context "with authorization" - let(:stubuser) { double :stubuser } - before do - allow(Conjur::Rack::User).to receive(:new) - .with(token, 'someacc', {:privilege => privilege, :remote_ip => remote_ip, :audit_roles => audit_roles, :audit_resources => audit_resources}) - .and_return(stubuser) - end - - context 'when called in app context' do - shared_examples_for :returns_user do - it "returns user built from token" do - expect(call).to eq stubuser - end - end - - include_examples :returns_user - - context 'with X-Conjur-Privilege' do - let(:privilege) { "elevate" } - include_examples :returns_user - end - - context 'with X-Forwarded-For' do - let(:remote_ip) { "66.0.0.1" } - include_examples :returns_user - end - - context 'with Conjur-Audit-Roles' do - let (:audit_roles) { 'user%3Acook' } - include_examples :returns_user - end - - context 'with Conjur-Audit-Resources' do - let (:audit_resources) { 'food%3Abacon' } - include_examples :returns_user - end - - end - - it "raises error if called out of app context" do - expect { Conjur::Rack.user }.to raise_error('No Conjur identity for current request') - end - end -end diff --git a/gems/conjur-rack/spec/spec_helper.rb b/gems/conjur-rack/spec/spec_helper.rb deleted file mode 100644 index e35d2880d5..0000000000 --- a/gems/conjur-rack/spec/spec_helper.rb +++ /dev/null @@ -1,47 +0,0 @@ -require 'rubygems' -$:.unshift File.join(File.dirname(__FILE__), "..", "lib") -$:.unshift File.join(File.dirname(__FILE__), "lib") - -# Allows loading of an environment config based on the environment -require 'rspec' -require 'rspec/its' -require 'securerandom' -require 'slosilo' - -RSpec.configure do |config| -end - -RSpec.shared_context "with authenticator" do - let(:options) { {} } - let(:app) { double(:app) } - subject(:authenticator) { Conjur::Rack::Authenticator.new(app, options) } - let(:call) { authenticator.call env } -end - -RSpec.shared_context "with authorization" do - include_context "with authenticator" - let(:token_signer) { "authn:someacc" } - let(:audit_resources) { nil } - let(:privilege) { nil } - let(:remote_ip) { nil } - let(:audit_roles) { nil } - - before do - allow(app).to receive(:call) { Conjur::Rack.user } - allow(Slosilo).to receive(:token_signer).and_return(token_signer) - end - - let(:env) do - { - 'HTTP_AUTHORIZATION' => "Token token=\"#{basic_64}\"" - }.tap do |e| - e['HTTP_X_CONJUR_PRIVILEGE'] = privilege if privilege - e['HTTP_X_FORWARDED_FOR'] = remote_ip if remote_ip - e['HTTP_CONJUR_AUDIT_ROLES'] = audit_roles if audit_roles - e['HTTP_CONJUR_AUDIT_RESOURCES'] = audit_resources if audit_resources - end - end - - let(:basic_64) { Base64.strict_encode64(token.to_json) } - let(:token) { { "data" => "foobar" } } -end diff --git a/gems/conjur-rack/test.sh b/gems/conjur-rack/test.sh deleted file mode 100755 index ad12ad8cf4..0000000000 --- a/gems/conjur-rack/test.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -e - -TEST_IMAGE='ruby:3.0' - -rm -f Gemfile.lock - -docker run --rm \ - -v "$PWD:/usr/src/app" \ - -w /usr/src/app \ - -e CONJUR_ENV=ci \ - $TEST_IMAGE \ - bash -c "gem update --system && bundle update && bundle exec rake spec" diff --git a/lib/rack/token_authentication.rb b/lib/rack/token_authentication.rb new file mode 100644 index 0000000000..74048590a4 --- /dev/null +++ b/lib/rack/token_authentication.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require 'jwt' + +module Rack + class TokenAuthentication + + class AuthorizationError < SecurityError + end + + class SignatureError < SecurityError + end + + class Forbidden < SecurityError + end + + AuthTokenDetails = Struct.new(:role_id, :claims, :request_ip, keyword_init: true) do + def account + role_id.split(':').first + end + end + + # +options+: + # :except :: a list of request path patterns for which to skip authentication. + # :optional :: request path patterns for which authentication is optional. + def initialize(app, options, slosilo: Slosilo) + @app = app + @options = options + @slosilo = slosilo + end + + def call(env) + # Duplicate for thread safety + dup._call(env) + end + + def _call(env) + request_path = [ env['SCRIPT_NAME'], env['PATH_INFO'] ].join + + if authentication_required?(request_path) + begin + request_ip = ::Rack::Request.new(env).ip + raw_token = env['HTTP_AUTHORIZATION'].to_s[/^Token token="(.*)"/, 1] + + if raw_token.present? + claims = validate_authentication_token(raw_token) + + unless ip_address_permissible?(claims: claims, ip_address: request_ip) + raise(Forbidden, 'IP address rejected') + end + + env['conjur-token-authentication.token_details'] = AuthTokenDetails.new( + role_id: claims['sub'], + claims: claims.slice('sub', 'iat', 'exp'), + request_ip: request_ip + ) + else + unless authentication_optional?(request_path) + raise(AuthorizationError, 'Authorization missing') + end + end + + rescue Forbidden => e + return error_response(status: 403, message: e.message) + rescue SecurityError => e + return error_response(status: 401, message: e.message) + end + end + + # call the next rack application + @app.call(env) + end + + protected + + def get_slosilo_key_by_fingerprint(fingerprint) + @slosilo.each do |id, key| + return id, key if key.fingerprint == fingerprint + end + + raise(SignatureError, 'Valid signing key not found') + end + + def validate_authentication_token(token) + # If the auth token comes in Base64 encoded, decode it. + # Note: The decoded string includes double quotes which need to be removed. + token = Base64.decode64(token).tr('""', '') unless token.include?('.') + + fingerprint = JSON.parse(Base64.decode64(token.split('.').first))['x5t'] + key_id, signing_key = get_slosilo_key_by_fingerprint(fingerprint) + + unless key_id.match?(/\Aauthn:(\w+):?\z/) + raise(SignatureError, 'Invalid signing key identifier') + end + + begin + claims, = JWT.decode( + token, + signing_key.key.public_key, + true, + { + algorithm: 'RS256', + iss: 'cyberark/conjur', + verify_iss: true, + verify_iat: true + } + ) + rescue JWT::DecodeError => e + raise(SignatureError, "Token verification failed: '#{e.message}'") + end + claims + end + + def ip_address_permissible?(ip_address:, claims:) + return true unless claims.key?('restricted_to') + + claims['restricted_to'] + .map(&IPAddr.method(:new)) + .any? { |c| c.include?(ip_address) } + end + + def authentication_required?(path) + [*@options[:except]].find{ |p| p.match(path) }.nil? + end + + def authentication_optional?(path) + [*@options[:optional]].find { |p| p.match(path) } + end + + def error_response(status:, message:) + [status, { 'Content-Type' => 'text/plain', 'Content-Length' => message.length.to_s }, [message] ] + end + end +end