diff --git a/CHANGELOG.md b/CHANGELOG.md index 575c7eda49..b7f9738198 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Nothing should go in this section, please add to the latest unreleased version (and update the corresponding date), or add a new version. -## [1.18.0] - 2022-07-12 +## [1.18.0] - 2022-08-01 + +### Added +- Adds support for namespace label based identity scope for the Kubernetes Authenticator + [cyberark/conjur#2613](https://github.com/cyberark/conjur/pull/2613) + ### Changed - Adds support for authentication using OIDC's code authorization flow [cyberark/conjur#2595](https://github.com/cyberark/conjur/pull/2595) diff --git a/Gemfile b/Gemfile index 607ab39985..1f06d52394 100644 --- a/Gemfile +++ b/Gemfile @@ -86,7 +86,6 @@ group :development, :test do gem 'cucumber', '~> 7.1' gem 'database_cleaner', '~> 1.8' gem 'debase', '~> 0.2.5.beta2' - gem 'faye-websocket' gem 'json_spec', '~> 1.1' gem 'faye-websocket' gem 'net-ssh' diff --git a/app/domain/authentication/authn_k8s/authentication_request.rb b/app/domain/authentication/authn_k8s/authentication_request.rb index 53b1becc3e..63a3f343d9 100644 --- a/app/domain/authentication/authn_k8s/authentication_request.rb +++ b/app/domain/authentication/authn_k8s/authentication_request.rb @@ -16,8 +16,10 @@ def valid_restriction?(restriction) case restriction.name when Restrictions::NAMESPACE if restriction.value != @namespace - raise Errors::Authentication::AuthnK8s::NamespaceMismatch(@namespace, restriction.value) + raise Errors::Authentication::AuthnK8s::NamespaceMismatch.new(@namespace, restriction.value) end + when Restrictions::NAMESPACE_LABEL_SELECTOR + @k8s_resource_validator.valid_namespace?(label_selector: restriction.value) else # Restrictions defined using '-', but the k8s client expects type with '_' instead. # e.g. 'restriction=stateful-set' converted to 'k8s_type=stateful_set' diff --git a/app/domain/authentication/authn_k8s/consts.rb b/app/domain/authentication/authn_k8s/consts.rb index 7d6d307e82..0a8f1800b5 100644 --- a/app/domain/authentication/authn_k8s/consts.rb +++ b/app/domain/authentication/authn_k8s/consts.rb @@ -8,6 +8,7 @@ module AuthnK8s module Restrictions NAMESPACE = "namespace" + NAMESPACE_LABEL_SELECTOR = "namespace-label-selector" SERVICE_ACCOUNT = "service-account" POD = "pod" DEPLOYMENT = "deployment" @@ -17,13 +18,13 @@ module Restrictions # This is not exactly a restriction, because it only validates container existence and not requesting container name. AUTHENTICATION_CONTAINER_NAME = "authentication-container-name" - REQUIRED = [NAMESPACE].freeze + REQUIRED_EXCLUSIVE = [NAMESPACE, NAMESPACE_LABEL_SELECTOR].freeze RESOURCE_TYPE_EXCLUSIVE = [DEPLOYMENT, DEPLOYMENT_CONFIG, STATEFUL_SET].freeze OPTIONAL = [SERVICE_ACCOUNT, POD, AUTHENTICATION_CONTAINER_NAME].freeze - PERMITTED = REQUIRED + RESOURCE_TYPE_EXCLUSIVE + OPTIONAL + PERMITTED = REQUIRED_EXCLUSIVE + RESOURCE_TYPE_EXCLUSIVE + OPTIONAL CONSTRAINTS = Constraints::MultipleConstraint.new( - Constraints::RequiredConstraint.new(required: REQUIRED), + Constraints::RequiredExclusiveConstraint.new(required_exclusive: REQUIRED_EXCLUSIVE), Constraints::PermittedConstraint.new(permitted: PERMITTED), Constraints::ExclusiveConstraint.new(exclusive: RESOURCE_TYPE_EXCLUSIVE) ) diff --git a/app/domain/authentication/authn_k8s/k8s_object_lookup.rb b/app/domain/authentication/authn_k8s/k8s_object_lookup.rb index a4dfe3fbe0..178c315de1 100644 --- a/app/domain/authentication/authn_k8s/k8s_object_lookup.rb +++ b/app/domain/authentication/authn_k8s/k8s_object_lookup.rb @@ -104,6 +104,15 @@ def pod_by_name(podname, namespace) k8s_client_for_method("get_pod").get_pod(podname, namespace) end + # Returns the labels hash for a Namespace with a given name. + # + # @return nil if no such Namespace exists. + def namespace_labels_hash(namespace) + namespace_object = k8s_client_for_method("get_namespace").get_namespace(namespace) + + return namespace_object.metadata.labels.to_h unless namespace_object.nil? + end + # Locates pods matching label selector in a namespace. # def pods_by_label(label_selector, namespace) diff --git a/app/domain/authentication/authn_k8s/k8s_resource_validator.rb b/app/domain/authentication/authn_k8s/k8s_resource_validator.rb index 7b191fdc61..31b5105924 100644 --- a/app/domain/authentication/authn_k8s/k8s_resource_validator.rb +++ b/app/domain/authentication/authn_k8s/k8s_resource_validator.rb @@ -24,6 +24,44 @@ def valid_resource?(type:, name:) @logger.debug(LogMessages::Authentication::AuthnK8s::ValidatedK8sResource.new(type, name)) end + # Validates label selector and creates a hash + # In the spirit of https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector.go + def valid_namespace?(label_selector:) + @logger.debug(LogMessages::Authentication::AuthnK8s::ValidatingK8sResourceLabel.new('namespace', namespace, label_selector)) + + if label_selector.length == 0 + raise Errors::Authentication::AuthnK8s::InvalidLabelSelector.new(label_selector) + end + label_selector_hash = label_selector + .split(",") + .map{ |kv_pair| + kv_pair = kv_pair.split(/={1,2}/, 2) + + invalid ||= kv_pair.length != 2 + invalid ||= kv_pair[0].include?("!") + + if (invalid) + raise Errors::Authentication::AuthnK8s::InvalidLabelSelector.new(label_selector) + end + + kv_pair[0] = kv_pair[0].to_sym + kv_pair + } + .to_h + + # Fetch namespace labels + # TODO: refactor this to have a generic label fetching method in @k8s_object_lookup + labels_hash = @k8s_object_lookup.namespace_labels_hash(namespace) + + # Validates label selector hash against labels hash + unless label_selector_hash.all? { |k, v| labels_hash[k] == v } + raise Errors::Authentication::AuthnK8s::LabelSelectorMismatch.new('namespace', namespace, label_selector) + end + + @logger.debug(LogMessages::Authentication::AuthnK8s::ValidatedK8sResourceLabel.new('namespace', namespace, label_selector)) + return true + end + private def retrieve_k8s_resource(type, name) diff --git a/app/domain/authentication/constraints/required_exclusive_constraint.rb b/app/domain/authentication/constraints/required_exclusive_constraint.rb new file mode 100644 index 0000000000..aaf0a2ef83 --- /dev/null +++ b/app/domain/authentication/constraints/required_exclusive_constraint.rb @@ -0,0 +1,19 @@ +module Authentication + module Constraints + + # This constraint is initialized with an array of strings. + # They represent resource restrictions where exactly one is required. + class RequiredExclusiveConstraint + + def initialize(required_exclusive:) + @required_exclusive = required_exclusive + end + + def validate(resource_restrictions:) + restrictions_found = resource_restrictions & @required_exclusive + raise Errors::Authentication::Constraints::IllegalRequiredExclusiveCombination.new(@required_exclusive, restrictions_found) unless restrictions_found.length == 1 + end + + end + end +end diff --git a/app/domain/errors.rb b/app/domain/errors.rb index 484ad2e03a..4df16ab04f 100644 --- a/app/domain/errors.rb +++ b/app/domain/errors.rb @@ -289,6 +289,16 @@ module AuthnK8s code: "CONJ00026E" ) + LabelSelectorMismatch = ::Util::TrackableErrorClass.new( + msg: "Kubernetes {0-resource-type} '{1-resource-id}' does not match label-selector: '{2-label-selector}'", + code: "CONJ00083E" + ) + + InvalidLabelSelector = ::Util::TrackableErrorClass.new( + msg: "Invalid label-selector '{0-label-selector}': must adhere to format '=,=,...', supports '=' and '=='", + code: "CONJ00094E" + ) + ContainerNotFound = ::Util::TrackableErrorClass.new( msg: "Container '{0}' was not found in the pod. Host id: {1}", code: "CONJ00028E" @@ -699,6 +709,12 @@ module Constraints msg: "Role must have at least one relevant annotation", code: "CONJ00099E" ) + + IllegalRequiredExclusiveCombination = ::Util::TrackableErrorClass.new( + msg: "Role must have exactly one of the following required constraints: " \ + "{0-constraints}. Role configured with {1-provided}", + code: "CONJ00131E" + ) end end diff --git a/app/domain/logs.rb b/app/domain/logs.rb index 3f0337b443..d06e88c81d 100644 --- a/app/domain/logs.rb +++ b/app/domain/logs.rb @@ -234,14 +234,24 @@ module AuthnK8s ) ValidatingK8sResource = ::Util::TrackableLogMessageClass.new( - msg: "Validating K8s resource. Type:'{0}', Name: {1}", + msg: "Validating K8s resource. Type:'{0}', Name:'{1}'", code: "CONJ00050D" ) ValidatedK8sResource = ::Util::TrackableLogMessageClass.new( - msg: "Validated K8s resource. Type:'{0}', Name: {1}", + msg: "Validated K8s resource. Type:'{0}', Name:'{1}'", code: "CONJ00051D" ) + + ValidatingK8sResourceLabel = ::Util::TrackableLogMessageClass.new( + msg: "Validating K8s resource using label selector. Type:'{0}', Name:'{1}', Label:'{2}'", + code: "CONJ00145D" + ) + + ValidatedK8sResourceLabel = ::Util::TrackableLogMessageClass.new( + msg: "Validated K8s resource using label selector. Type:'{0}', Name:'{1}', Label:'{2}'", + code: "CONJ00146D" + ) end module AuthnIam diff --git a/build-and-publish-internal-appliance.sh b/build-and-publish-internal-appliance.sh new file mode 100755 index 0000000000..883d623234 --- /dev/null +++ b/build-and-publish-internal-appliance.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + + +IMAGE="registry.tld/conjur-appliance:eval-authn-k8s-label-selector" + +echo "Building on top of stable appliance image and pushing to ${IMAGE}" + +echo " + +# --- +FROM registry.tld/conjur-appliance:5.0-stable + +# Copy new source files +$( + echo " +app/domain/authentication/authn_k8s/authentication_request.rb +app/domain/authentication/authn_k8s/consts.rb +app/domain/authentication/authn_k8s/k8s_object_lookup.rb +app/domain/authentication/authn_k8s/k8s_resource_validator.rb +app/domain/authentication/constraints/required_exclusive_constraint.rb +app/domain/errors.rb +" | docker run --rm -i --entrypoint="" ruby:2-alpine ruby -e ' +files = STDIN.read.split("\n").reject(&:empty?) +puts files.map {|file| "COPY #{file} /opt/conjur/possum/#{file}"}.join("\n") +' +) + +RUN chown -R conjur:conjur /opt/conjur/possum/app + +# --- + +" | \ + tee /dev/stderr | \ + docker build -f - -t "${IMAGE}" . + +docker push "${IMAGE}" diff --git a/spec/app/domain/authentication/authn_k8s/k8s_resource_validator_spec.rb b/spec/app/domain/authentication/authn_k8s/k8s_resource_validator_spec.rb new file mode 100644 index 0000000000..65c60e0503 --- /dev/null +++ b/spec/app/domain/authentication/authn_k8s/k8s_resource_validator_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe(Authentication::AuthnK8s::K8sResourceValidator) do + let(:log_output) { StringIO.new } + let(:logger) { + Logger.new( + log_output, + formatter: proc do | severity, time, progname, msg | + "#{severity},#{msg}\n" + end) + } + + subject { + described_class.new(k8s_object_lookup: k8s_object_lookup, pod: pod, logger: logger) + } + + let(:k8s_object_lookup) { + double("k8s_object_lookup").tap { |d| + allow(d).to receive(:namespace_labels_hash) + .with("namespace_name") + .and_return({ :key1 => "value1", :key2 => "value2" }) + } + } + + let(:pod) { + double("pod").tap { |d| + d.stub_chain("metadata.namespace").and_return("namespace_name") + } + } + + context "#valid_namespace?" do + it 'raises error on empty label selector' do + expect { subject.valid_namespace?(label_selector: "") }.to( + raise_error( + ::Errors::Authentication::AuthnK8s::InvalidLabelSelector + ) + ) + end + + it 'raises error on invalid label selector' do + # No key-value pair + expect { subject.valid_namespace?(label_selector: "key,") }.to( + raise_error( + ::Errors::Authentication::AuthnK8s::InvalidLabelSelector + ) + ) + + # Unsupported operator + expect { subject.valid_namespace?(label_selector: "key!=value") }.to( + raise_error( + ::Errors::Authentication::AuthnK8s::InvalidLabelSelector + ) + ) + end + + it 'returns true for labels matching label-selector' do + # Single key, single equals format + expect( + subject.valid_namespace?(label_selector: "key1=value1") + ).to be true + # Single key, double equals format + expect( + subject.valid_namespace?(label_selector: "key2==value2") + ).to be true + # Multiple keys + expect( + subject.valid_namespace?(label_selector: "key1=value1,key2=value2") + ).to be true + end + + it 'throws an error for labels not matching label-selector' do + # Value mismatch + expect { subject.valid_namespace?(label_selector: "key1=notvalue") }.to( + raise_error( + ::Errors::Authentication::AuthnK8s::LabelSelectorMismatch + ) + ) + # Key not found + expect { subject.valid_namespace?(label_selector: "notfoundkey=value") }.to( + raise_error( + ::Errors::Authentication::AuthnK8s::LabelSelectorMismatch + ) + ) + # One of multiple keys does not match + expect { subject.valid_namespace?(label_selector: "key1=value1,notfoundkey=value") }.to( + raise_error( + ::Errors::Authentication::AuthnK8s::LabelSelectorMismatch + ) + ) + end + + it 'logs before label-selector validation begins, and after success' do + subject.valid_namespace?(label_selector: "key1=value1") + + expect(log_output.string.split("\n")).to include( + "DEBUG,CONJ00145D Validating K8s resource using label selector. Type:'namespace', Name:'namespace_name', Label:'key1=value1'", + "DEBUG,CONJ00146D Validated K8s resource using label selector. Type:'namespace', Name:'namespace_name', Label:'key1=value1'" + ) + end + + it 'logs before label-selector validation begins, but not after failure' do + expect { subject.valid_namespace?(label_selector: "key1=notvalue") }.to raise_error + + expect(log_output.string.split("\n")).to include( + "DEBUG,CONJ00145D Validating K8s resource using label selector. Type:'namespace', Name:'namespace_name', Label:'key1=notvalue'", + ) + expect(log_output.string.split("\n")).not_to include( + "DEBUG,CONJ00146D Validated K8s resource using label selector. Type:'namespace', Name:'namespace_name', Label:'key1=notvalue'" + ) + end + end +end diff --git a/spec/app/domain/authentication/constraints/required_exclusive_constraint_spec.rb b/spec/app/domain/authentication/constraints/required_exclusive_constraint_spec.rb new file mode 100644 index 0000000000..cec2c6ed6b --- /dev/null +++ b/spec/app/domain/authentication/constraints/required_exclusive_constraint_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe(Authentication::Constraints::RequiredExclusiveConstraint) do + context "Given RequiredExclusiveConstraint initialized with 3 restrictions" do + let(:reqx_restrictions) { %w[reqx_one reqx_two reqx_three] } + let(:additional_restriction) { "additional" } + let(:raised_error) { ::Errors::Authentication::Constraints::IllegalRequiredExclusiveCombination } + + subject(:constraint) do + Authentication::Constraints::RequiredExclusiveConstraint.new(required_exclusive: reqx_restrictions) + end + + context "when validating with no ReqX restrictions" do + let(:expected_error_message) { /#{Regexp.escape(reqx_restrictions.to_s)}/ } + + subject do + constraint.validate(resource_restrictions: [additional_restriction]) + end + + it "raises an error" do + expect { subject }.to raise_error(raised_error, expected_error_message) + end + end + + context "when validating with one ReqX restriction" do + subject do + constraint.validate(resource_restrictions: [reqx_restrictions.first, additional_restriction]) + end + + it "does not raise an error" do + expect { subject }.to_not raise_error + end + end + + context "when validating with many ReqX restrictions" do + let(:resource_restrictions) { reqx_restrictions[1, 2] } + let(:expected_error_message) { /#{Regexp.escape(resource_restrictions.to_s)}/ } + + subject do + constraint.validate(resource_restrictions: resource_restrictions + [additional_restriction]) + end + + it "raises an error" do + expect { subject }.to raise_error(raised_error, expected_error_message) + end + end + + context "when validating with all ReqX restrictions" do + let(:expected_error_message) { /#{Regexp.escape(reqx_restrictions.to_s)}/ } + + subject do + constraint.validate(resource_restrictions: reqx_restrictions + [additional_restriction]) + end + + it "raises an error" do + expect { subject }.to raise_error(raised_error, expected_error_message) + end + end + + end +end diff --git a/spec/controllers/authenticate_controller_authn_k8s_spec.rb b/spec/controllers/authenticate_controller_authn_k8s_spec.rb index 5fd25e3b97..aece8616ea 100644 --- a/spec/controllers/authenticate_controller_authn_k8s_spec.rb +++ b/spec/controllers/authenticate_controller_authn_k8s_spec.rb @@ -24,6 +24,24 @@ def apply_root_policy(account, policy_content:, expect_success: false) end end +def define_and_grant_host(account:, host_id:, annotations:, service_id:) + host_policy = %Q( +# Define test app host +- !host + id: #{host_id} + annotations: +#{annotations.map{ |k,v| "#{k}: #{v}" }.join("\n").indent(4)} + +# Grant app host authentication privileges +- !permit + role: !host #{host_id} + privilege: [ read, authenticate ] + resource: !webservice #{service_id} + ) + + apply_root_policy(account, policy_content: host_policy, expect_success: true) +end + def define_authenticator(account:, service_id:, host_id:) # Create authenticator instance by applying policy authenticator_policy = %Q( @@ -31,18 +49,6 @@ def define_authenticator(account:, service_id:, host_id:) # In the spirit of # https://docs.cyberark.com/Product-Doc/OnlineHelp/AAM-DAP/Latest/en/Content/Integrations/k8s-ocp/k8s-app-identity.htm?tocpath=Integrations%7COpenShift%252FKubernetes%7CSet%20up%20applications%7C_____4 -# Define test app host -- !host - id: #{host_id} - annotations: - authn-k8s/namespace: default - # authn-k8s/namespace-label-selector: "field.cattle.io/projectId=p-q7s7z" - authn-k8s/authentication-container-name: bash - # authn-k8s/service-account: - # authn-k8s/deployment: - # authn-k8s/deployment-config: - # authn-k8s/stateful-set: - # Enroll a Kubernetes authentication service - !policy id: #{service_id} @@ -63,12 +69,6 @@ def define_authenticator(account:, service_id:, host_id:) - !webservice annotations: description: Authenticator service for K8s cluster - - # Grant app host authentication privileges - - !permit - role: !host /#{host_id} - privilege: [ read, authenticate ] - resource: !webservice ) apply_root_policy(account, policy_content: authenticator_policy, expect_success: true) @@ -106,7 +106,6 @@ def fake_authn_k8s_login(account, service_id, host_id:) ) end - def authn_k8s_login(authenticator_id:, host_id:) # Fake login hostpkey = OpenSSL::PKey::RSA.new(2048) @@ -132,20 +131,41 @@ def authn_k8s_authenticate(authenticator_id:, account:, host_id:, signed_cert_pe post("/#{authenticator_id}/#{account}/#{escaped_host_id}/authenticate", env: payload) end +def capture_args(obj, method) + args = [] + original_method = obj.method(method) + allow(obj).to receive(method) { |arg| + args.push(arg) + original_method.call(arg) + } + args +end + describe AuthenticateController, :type => :request do + # Test server is defined in the appropritate "around" hook for the test example + let(:test_server) { @test_server } + let(:account) { "rspec" } let(:authenticator_id) { "authn-k8s/meow" } + let(:service_id) { "conjur/#{authenticator_id}" } let(:test_app_host) { "h-#{random_hex}" } + # Allows API calls to be made as the admin user let(:admin_request_env) do { 'HTTP_AUTHORIZATION' => "Token token=\"#{Base64.strict_encode64(Slosilo["authn:rspec"].signed_token("admin").to_json)}\"" } end - # Test server is defined in the appropritate "around" hook for the test example - let(:test_server) { @test_server } + before(:all) do + # Start fresh + DatabaseCleaner.clean_with(:truncation) + + # Init Slosilo key + Slosilo["authn:rspec"] ||= Slosilo::Key.new + Role.create(role_id: 'rspec:user:admin') + end describe "#authenticate" do - context "k8s api access contains subpath" do + context "k8s mock server" do around(:each) do |example| WebMock.disable_net_connect!(allow: 'http://localhost:1234') AuthnK8sTestServer.run_async( @@ -157,9 +177,7 @@ def authn_k8s_authenticate(authenticator_id:, account:, host_id:, signed_cert_pe end end - it "client successfully authenticates" do - service_id = "conjur/#{authenticator_id}" - + before(:each) do # Setup authenticator define_authenticator( account: account, @@ -177,10 +195,20 @@ def authn_k8s_authenticate(authenticator_id:, account:, host_id:, signed_cert_pe ca_cert: "---", service_account_token: "bearer token" ) - - # Authenticate - # first, ensure authenticator is enabled. Unfortunately there's no nicer way to do this since configuration used is that which is evaluated at load time! + # ensure authenticator is enabled. Unfortunately there's no nicer way to do this since configuration used is that which is evaluated at load time! allow_any_instance_of(Authentication::Webservices).to receive(:include?).and_return(true) + end + + it "client successfully authenticates with namespace name restriction" do + define_and_grant_host( + account: account, + host_id: test_app_host, + annotations: { + "authn-k8s/authentication-container-name" => "bash", + "authn-k8s/namespace" => "default" + }, + service_id: service_id + ) # NOTE: option to do an in-memory fake login request # signed_cert = fake_authn_k8s_login(account, service_id, host_id: test_app_host) @@ -207,6 +235,70 @@ def authn_k8s_authenticate(authenticator_id:, account:, host_id:, signed_cert_pe expect(token.signature).to be expect(token.claims).to have_key('iat') end + + it "client successfully authenticates with namespace label restriction" do + define_and_grant_host( + account: account, + host_id: test_app_host, + annotations: { + "authn-k8s/authentication-container-name" => "bash", + "authn-k8s/namespace-label-selector" => "field.cattle.io/projectId=p-q7s7z" + }, + service_id: service_id + ) + + # NOTE: option to do an in-memory fake login request + # signed_cert = fake_authn_k8s_login(account, service_id, host_id: test_app_host) + + # Login request, grab the signed certificate from the fake server + authn_k8s_login( + authenticator_id: authenticator_id, + host_id: test_app_host + ) + signed_cert = test_server.copied_content + + # Authenticate request + authn_k8s_authenticate( + authenticator_id: authenticator_id, + account: account, + host_id: test_app_host, + signed_cert_pem: signed_cert.to_s + ) + + # Assertions + expect(response).to be_ok + token = Slosilo::JWT.parse_json(response.body) + expect(token.claims['sub']).to eq("host/#{test_app_host}") + expect(token.signature).to be + expect(token.claims).to have_key('iat') + end + + it "client fails when given both namespace name and label restriction" do + define_and_grant_host( + account: account, + host_id: test_app_host, + annotations: { + "authn-k8s/namespace" => "default", + "authn-k8s/namespace-label-selector" => "field.cattle.io/projectId=p-q7s7z" , + "authn-k8s/authentication-container-name" => "bash" + }, + service_id: service_id + ) + + info_log_args = capture_args(Rails.logger, :info) + + # Login request, grab the signed certificate from the fake server + authn_k8s_login( + authenticator_id: authenticator_id, + host_id: test_app_host + ) + + expect(info_log_args).to satisfy { |args| + args.any? { |arg| + arg.to_s.include?("CONJ00131E") + } + } + end end # TODO: Add more scenarios @@ -214,15 +306,6 @@ def authn_k8s_authenticate(authenticator_id:, account:, host_id:, signed_cert_pe # 2. Conjur Authentication errors # ... end - - before(:all) do - # Start fresh - DatabaseCleaner.clean_with(:truncation) - - # Init Slosilo key - Slosilo["authn:rspec"] ||= Slosilo::Key.new - Role.create(role_id: 'rspec:user:admin') - end end # bundle exec rspec --format documentation ./spec/controllers/authenticate_controller_authn_k8s_spec.rb diff --git a/spec/support/authn_k8s/authn_k8s_test_server.rb b/spec/support/authn_k8s/authn_k8s_test_server.rb index a0aa26a35d..d00f1638d5 100644 --- a/spec/support/authn_k8s/authn_k8s_test_server.rb +++ b/spec/support/authn_k8s/authn_k8s_test_server.rb @@ -141,10 +141,10 @@ def call(env) [ 200, {"Content-Type" => "application/json"}, [AuthnK8sTestServer.read_response_file("good:api.v1.getpod.json")] ] elsif req.path.start_with?("#{subpath}/api/v1/namespaces/default/pods/") [ 404, {"Content-Type" => "application/json"}, [AuthnK8sTestServer.read_response_file("bad:api.v1.getpod.json")] ] - elsif req.fullpath == "#{subpath}/api/v1/namespaces?labelSelector=field.cattle.io%2FprojectId%3Dp-q7s7z&fieldSelector=metadata.name%3Ddefault" - [ 200, {"Content-Type" => "application/json"}, [AuthnK8sTestServer.read_response_file("good:api.v1.getnamespaces.json")] ] + elsif req.fullpath == "#{subpath}/api/v1/namespaces/default" + [ 200, {"Content-Type" => "application/json"}, [AuthnK8sTestServer.read_response_file("good:api.v1.getnamespace.json")]] elsif req.path.start_with?("#{subpath}/api/v1/namespaces") - [ 200, {"Content-Type" => "application/json"}, [AuthnK8sTestServer.read_response_file("bad:api.v1.getnamespaces.json")] ] + [ 404, {"Content-Type" => "application/json"}, [AuthnK8sTestServer.read_response_file("bad:api.v1.getnamespace.json")] ] # NOTE: Kubenertes clients make requests to a whole set of endpoints at initialization time. The only way we could find to make the clients # happy was to have this else branch return 200 with an empty JSON object. Ideally, this branch should return something like a 404. # TODO: Find a better way to satisfy Kubernetes client initialization in relation to the above note. diff --git a/spec/support/authn_k8s/bad:api.v1.getnamespace.json b/spec/support/authn_k8s/bad:api.v1.getnamespace.json new file mode 100644 index 0000000000..9956ef0df7 --- /dev/null +++ b/spec/support/authn_k8s/bad:api.v1.getnamespace.json @@ -0,0 +1,13 @@ +{ + "kind": "Status", + "apiVersion": "v1", + "metadata": {}, + "status": "Failure", + "message": "namespace \"ok\" not found", + "reason": "NotFound", + "details": { + "name": "ok", + "kind": "namespace" + }, + "code": 404 +} diff --git a/spec/support/authn_k8s/bad:api.v1.getnamespaces.json b/spec/support/authn_k8s/bad:api.v1.getnamespaces.json deleted file mode 100644 index 16f405ca99..0000000000 --- a/spec/support/authn_k8s/bad:api.v1.getnamespaces.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "kind": "NamespaceList", - "apiVersion": "v1", - "metadata": { - "resourceVersion": "2445300" - }, - "items": [] -} diff --git a/spec/support/authn_k8s/good:api.v1.getnamespace.json b/spec/support/authn_k8s/good:api.v1.getnamespace.json new file mode 100644 index 0000000000..8cf0b04461 --- /dev/null +++ b/spec/support/authn_k8s/good:api.v1.getnamespace.json @@ -0,0 +1,83 @@ +{ + "kind": "Namespace", + "apiVersion": "v1", + "metadata": { + "resourceVersion": "2445300", + "name": "default", + "uid": "da93666e-c98d-44eb-9b04-eb59f2ccc000", + "resourceVersion": "1254", + "creationTimestamp": "2022-05-12T13:54:02Z", + "labels": { + "field.cattle.io/projectId": "p-q7s7z", + "kubernetes.io/metadata.name": "default" + }, + "annotations": { + "cattle.io/status": "{\"Conditions\":[{\"Type\":\"ResourceQuotaInit\",\"Status\":\"True\",\"Message\":\"\",\"LastUpdateTime\":\"2022-05-12T13:55:26Z\"},{\"Type\":\"InitialRolesPopulated\",\"Status\":\"True\",\"Message\":\"\",\"LastUpdateTime\":\"2022-05-12T13:55:31Z\"}]}", + "field.cattle.io/projectId": "c-4hbzx:p-q7s7z", + "lifecycle.cattle.io/create.namespace-auth": "true" + }, + "finalizers": [ + "controller.cattle.io/namespace-auth" + ], + "managedFields": [ + { + "manager": "kube-apiserver", + "operation": "Update", + "apiVersion": "v1", + "time": "2022-05-12T13:54:02Z", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + } + }, + { + "manager": "rancher", + "operation": "Update", + "apiVersion": "v1", + "time": "2022-05-12T13:55:25Z", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:cattle.io/status": {}, + "f:field.cattle.io/projectId": {}, + "f:lifecycle.cattle.io/create.namespace-auth": {} + }, + "f:finalizers": { + ".": {}, + "v:\"controller.cattle.io/namespace-auth\"": {} + } + } + } + }, + { + "manager": "agent", + "operation": "Update", + "apiVersion": "v1", + "time": "2022-05-12T13:55:49Z", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + "f:field.cattle.io/projectId": {} + } + } + } + } + ] + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} diff --git a/spec/support/authn_k8s/good:api.v1.getnamespaces.json b/spec/support/authn_k8s/good:api.v1.getnamespaces.json deleted file mode 100644 index 4be3a4a0e1..0000000000 --- a/spec/support/authn_k8s/good:api.v1.getnamespaces.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "kind": "NamespaceList", - "apiVersion": "v1", - "metadata": { - "resourceVersion": "2445300" - }, - "items": [ - { - "metadata": { - "name": "default", - "uid": "da93666e-c98d-44eb-9b04-eb59f2ccc000", - "resourceVersion": "1254", - "creationTimestamp": "2022-05-12T13:54:02Z", - "labels": { - "field.cattle.io/projectId": "p-q7s7z", - "kubernetes.io/metadata.name": "default" - }, - "annotations": { - "cattle.io/status": "{\"Conditions\":[{\"Type\":\"ResourceQuotaInit\",\"Status\":\"True\",\"Message\":\"\",\"LastUpdateTime\":\"2022-05-12T13:55:26Z\"},{\"Type\":\"InitialRolesPopulated\",\"Status\":\"True\",\"Message\":\"\",\"LastUpdateTime\":\"2022-05-12T13:55:31Z\"}]}", - "field.cattle.io/projectId": "c-4hbzx:p-q7s7z", - "lifecycle.cattle.io/create.namespace-auth": "true" - }, - "finalizers": [ - "controller.cattle.io/namespace-auth" - ], - "managedFields": [ - { - "manager": "kube-apiserver", - "operation": "Update", - "apiVersion": "v1", - "time": "2022-05-12T13:54:02Z", - "fieldsType": "FieldsV1", - "fieldsV1": { - "f:metadata": { - "f:labels": { - ".": {}, - "f:kubernetes.io/metadata.name": {} - } - } - } - }, - { - "manager": "rancher", - "operation": "Update", - "apiVersion": "v1", - "time": "2022-05-12T13:55:25Z", - "fieldsType": "FieldsV1", - "fieldsV1": { - "f:metadata": { - "f:annotations": { - ".": {}, - "f:cattle.io/status": {}, - "f:field.cattle.io/projectId": {}, - "f:lifecycle.cattle.io/create.namespace-auth": {} - }, - "f:finalizers": { - ".": {}, - "v:\"controller.cattle.io/namespace-auth\"": {} - } - } - } - }, - { - "manager": "agent", - "operation": "Update", - "apiVersion": "v1", - "time": "2022-05-12T13:55:49Z", - "fieldsType": "FieldsV1", - "fieldsV1": { - "f:metadata": { - "f:labels": { - "f:field.cattle.io/projectId": {} - } - } - } - } - ] - }, - "spec": { - "finalizers": [ - "kubernetes" - ] - }, - "status": { - "phase": "Active" - } - } - ] -}