conjur-api 6.3.1 → 6.4.0.pre.849

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+ require 'cgi'
7
+
8
+ module Conjur
9
+ class API
10
+ # Authenticator that uses an Azure Managed Identity JWT to obtain Conjur access tokens.
11
+ # Fetches a JWT from the Azure Instance Metadata Service (IMDS), then POSTs it
12
+ # to the Conjur authn-azure endpoint.
13
+ class AzureAuthenticator
14
+ include TokenExpiration
15
+
16
+ IMDS_TOKEN_PATH = '/metadata/identity/oauth2/token'.freeze
17
+ DEFAULT_RESOURCE = 'https://management.azure.com/'.freeze
18
+ DEFAULT_IMDS_BASE = 'http://169.254.169.254'.freeze
19
+ DEFAULT_IMDS_VER = '2018-02-01'.freeze
20
+
21
+ attr_reader :account, :service_id, :identity, :resource_uri,
22
+ :client_id, :imds_base_url, :imds_api_version
23
+
24
+ def initialize(account, service_id, identity,
25
+ resource_uri: DEFAULT_RESOURCE,
26
+ client_id: nil,
27
+ imds_base_url: DEFAULT_IMDS_BASE,
28
+ imds_api_version: DEFAULT_IMDS_VER)
29
+ @account = account
30
+ @service_id = service_id
31
+ @identity = identity
32
+ @resource_uri = resource_uri
33
+ @client_id = client_id
34
+ @imds_base_url = imds_base_url
35
+ @imds_api_version = imds_api_version
36
+ update_token_born
37
+ end
38
+
39
+ def refresh_token
40
+ Conjur::API.authenticate_azure(service_id, identity,
41
+ account: account,
42
+ resource_uri: resource_uri,
43
+ client_id: client_id,
44
+ imds_base_url: imds_base_url,
45
+ imds_api_version: imds_api_version).tap do
46
+ update_token_born
47
+ end
48
+ end
49
+ end
50
+
51
+ class << self
52
+ # Create a {Conjur::API} instance authenticated via Azure Managed Identity (authn-azure).
53
+ #
54
+ # @param [String] service_id the authn-azure service ID configured in Conjur
55
+ # @param [String] identity Conjur host identity (e.g. "host/my-app/azure-host")
56
+ # @param [String] account the Conjur organization account
57
+ # @param [String] resource_uri Azure resource URI for the IMDS token request
58
+ # @param [String, nil] client_id optional client ID for a user-assigned managed identity
59
+ # @param [String] imds_base_url override for the IMDS base URL (useful in tests)
60
+ # @param [String] imds_api_version override for the IMDS API version
61
+ # @param [String, nil] remote_ip optional IP address recorded in the audit log
62
+ # @return [Conjur::API]
63
+ def new_from_azure(service_id, identity,
64
+ account: Conjur.configuration.account,
65
+ resource_uri: AzureAuthenticator::DEFAULT_RESOURCE,
66
+ client_id: nil,
67
+ imds_base_url: AzureAuthenticator::DEFAULT_IMDS_BASE,
68
+ imds_api_version: AzureAuthenticator::DEFAULT_IMDS_VER,
69
+ remote_ip: nil)
70
+ self.new.init_from_azure(service_id, identity,
71
+ account: account,
72
+ resource_uri: resource_uri,
73
+ client_id: client_id,
74
+ imds_base_url: imds_base_url,
75
+ imds_api_version: imds_api_version,
76
+ remote_ip: remote_ip)
77
+ end
78
+
79
+ # Authenticate via authn-azure and return a parsed Conjur access token.
80
+ #
81
+ # @param [String] service_id the authn-azure service ID
82
+ # @param [String] identity Conjur host identity
83
+ # @param [String] account the Conjur organization account
84
+ # @return [Hash] parsed access token
85
+ def authenticate_azure(service_id, identity,
86
+ account: Conjur.configuration.account,
87
+ resource_uri: AzureAuthenticator::DEFAULT_RESOURCE,
88
+ client_id: nil,
89
+ imds_base_url: AzureAuthenticator::DEFAULT_IMDS_BASE,
90
+ imds_api_version: AzureAuthenticator::DEFAULT_IMDS_VER)
91
+ raise ArgumentError, "service_id is required" if service_id.nil? || service_id.empty?
92
+ raise ArgumentError, "identity is required" if identity.nil? || identity.empty?
93
+
94
+ if Conjur.log
95
+ Conjur.log << "Authenticating #{identity} to account #{account} via authn-azure/#{service_id}\n"
96
+ end
97
+
98
+ jwt = fetch_azure_jwt(resource_uri: resource_uri, client_id: client_id,
99
+ imds_base_url: imds_base_url, imds_api_version: imds_api_version)
100
+ JSON.parse(url_for(:authn_azure_authenticate, account, service_id, identity)
101
+ .post("jwt=#{CGI.escape(jwt)}", content_type: 'application/x-www-form-urlencoded'))
102
+ end
103
+
104
+ private
105
+
106
+ # Fetches an Azure Managed Identity JWT from IMDS.
107
+ # Uses a direct Net::HTTP connection (proxy addr = nil) because the IMDS
108
+ # link-local address 169.254.169.254 must never be routed through a proxy.
109
+ def fetch_azure_jwt(resource_uri:, client_id:, imds_base_url:, imds_api_version:)
110
+ query = "api-version=#{CGI.escape(imds_api_version)}&resource=#{CGI.escape(resource_uri)}"
111
+ query += "&client_id=#{CGI.escape(client_id)}" if client_id
112
+
113
+ uri = URI("#{imds_base_url}#{AzureAuthenticator::IMDS_TOKEN_PATH}?#{query}")
114
+ http = Net::HTTP.new(uri.host, uri.port, nil) # nil = no proxy
115
+ response = http.get(uri.request_uri, 'Metadata' => 'true')
116
+
117
+ unless response.is_a?(Net::HTTPSuccess)
118
+ raise "Azure IMDS token request failed: #{response.code} #{response.message}. Body: #{response.body}"
119
+ end
120
+
121
+ body = JSON.parse(response.body)
122
+ token = body['access_token']
123
+ raise "Azure IMDS returned an empty access token" if token.nil? || token.empty?
124
+
125
+ token
126
+ end
127
+ end
128
+
129
+ def init_from_azure(service_id, identity,
130
+ account: Conjur.configuration.account,
131
+ resource_uri: AzureAuthenticator::DEFAULT_RESOURCE,
132
+ client_id: nil,
133
+ imds_base_url: AzureAuthenticator::DEFAULT_IMDS_BASE,
134
+ imds_api_version: AzureAuthenticator::DEFAULT_IMDS_VER,
135
+ remote_ip: nil)
136
+ @remote_ip = remote_ip
137
+ @authenticator = AzureAuthenticator.new(account, service_id, identity,
138
+ resource_uri: resource_uri,
139
+ client_id: client_id,
140
+ imds_base_url: imds_base_url,
141
+ imds_api_version: imds_api_version)
142
+ self
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'cgi'
6
+ require 'json'
7
+
8
+ module Conjur
9
+ class API
10
+ # Authenticator that uses a GCP identity token to obtain Conjur access tokens.
11
+ # Fetches an identity JWT from the GCP Instance Metadata Service, then POSTs it
12
+ # to the Conjur authn-gcp endpoint.
13
+ #
14
+ # authn-gcp is a serviceless authenticator: there is no service ID, and Conjur
15
+ # identifies the calling host from the token's claims (matched against the
16
+ # host's authn-gcp/project-id annotation), so the host id is not sent in the
17
+ # authenticate URL.
18
+ #
19
+ # An explicit JWT may be supplied to bypass the metadata service fetch —
20
+ # useful in CI environments where the token is pre-fetched on a GCP agent.
21
+ class GCPAuthenticator
22
+ include TokenExpiration
23
+
24
+ DEFAULT_METADATA_URL = 'http://metadata.google.internal/computeMetadata/v1' \
25
+ '/instance/service-accounts/default/identity'.freeze
26
+
27
+ attr_reader :account, :identity, :jwt, :gcp_identity_url
28
+
29
+ def initialize(account, identity, jwt: nil,
30
+ gcp_identity_url: DEFAULT_METADATA_URL)
31
+ @account = account
32
+ @identity = identity
33
+ @jwt = jwt
34
+ @gcp_identity_url = gcp_identity_url
35
+ update_token_born
36
+ end
37
+
38
+ def refresh_token
39
+ Conjur::API.authenticate_gcp(identity, account: account,
40
+ jwt: jwt,
41
+ gcp_identity_url: gcp_identity_url).tap do
42
+ update_token_born
43
+ end
44
+ end
45
+ end
46
+
47
+ class << self
48
+ # Create a {Conjur::API} instance authenticated via GCP workload identity (authn-gcp).
49
+ #
50
+ # @param [String] identity Conjur host identity (e.g. "host/data/test/gcp-apps/test-app").
51
+ # Used to build the metadata-service audience; Conjur itself derives the
52
+ # host from the token, so it is not sent in the authenticate request.
53
+ # @param [String] account the Conjur organization account
54
+ # @param [String, nil] jwt pre-obtained GCP identity token; when supplied the metadata
55
+ # service is not called. Useful in tests and CI where the token is pre-fetched.
56
+ # @param [String] gcp_identity_url override for the GCP metadata identity URL (for tests)
57
+ # @param [String, nil] remote_ip optional IP address recorded in the audit log
58
+ # @return [Conjur::API]
59
+ def new_from_gcp(identity,
60
+ account: Conjur.configuration.account,
61
+ jwt: nil,
62
+ gcp_identity_url: GCPAuthenticator::DEFAULT_METADATA_URL,
63
+ remote_ip: nil)
64
+ self.new.init_from_gcp(identity, account: account,
65
+ jwt: jwt, gcp_identity_url: gcp_identity_url,
66
+ remote_ip: remote_ip)
67
+ end
68
+
69
+ # Authenticate via authn-gcp and return a parsed Conjur access token.
70
+ #
71
+ # @param [String] identity Conjur host identity (used to build the audience)
72
+ # @param [String] account the Conjur organization account
73
+ # @param [String, nil] jwt pre-obtained GCP identity token
74
+ # @param [String] gcp_identity_url override for the GCP metadata identity URL
75
+ # @return [Hash] parsed access token
76
+ def authenticate_gcp(identity,
77
+ account: Conjur.configuration.account,
78
+ jwt: nil,
79
+ gcp_identity_url: GCPAuthenticator::DEFAULT_METADATA_URL)
80
+ raise ArgumentError, "identity is required" if identity.nil? || identity.empty?
81
+
82
+ if Conjur.log
83
+ Conjur.log << "Authenticating #{identity} to account #{account} via authn-gcp\n"
84
+ end
85
+
86
+ token = jwt || fetch_gcp_jwt(account, identity, gcp_identity_url: gcp_identity_url)
87
+ JSON.parse(url_for(:authn_gcp_authenticate, account)
88
+ .post("jwt=#{CGI.escape(token)}", content_type: 'application/x-www-form-urlencoded'))
89
+ end
90
+
91
+ private
92
+
93
+ # Fetches a GCP identity token from the instance metadata service.
94
+ # Uses a direct Net::HTTP connection (proxy addr = nil) because the metadata
95
+ # link-local address must never be routed through a proxy.
96
+ #
97
+ # The requested audience is "conjur/{account}/host/{identity}", matching what
98
+ # the authn-gcp authenticator expects.
99
+ def fetch_gcp_jwt(account, identity, gcp_identity_url:)
100
+ aud = gcp_audience(account, identity)
101
+ url = "#{gcp_identity_url}?#{URI.encode_www_form(audience: aud, format: 'full')}"
102
+
103
+ uri = URI(url)
104
+ http = Net::HTTP.new(uri.host, uri.port, nil) # nil = no proxy
105
+ response = http.get(uri.request_uri, 'Metadata-Flavor' => 'Google')
106
+
107
+ unless response.is_a?(Net::HTTPSuccess)
108
+ raise "GCP metadata token request failed: #{response.code} #{response.message}. Body: #{response.body}"
109
+ end
110
+
111
+ token = response.body&.strip
112
+ raise "GCP metadata service returned an empty token" if token.nil? || token.empty?
113
+
114
+ token
115
+ end
116
+
117
+ # Builds the authn-gcp audience for a Conjur host identity, e.g.
118
+ # "conjur/myorg/host/data/app". The "host/" prefix is optional in the
119
+ # supplied identity and is normalized away before being re-applied.
120
+ def gcp_audience(account, identity)
121
+ normalized = identity.start_with?('host/') ? identity['host/'.length..] : identity
122
+ "conjur/#{account}/host/#{normalized}"
123
+ end
124
+ end
125
+
126
+ def init_from_gcp(identity,
127
+ account: Conjur.configuration.account,
128
+ jwt: nil,
129
+ gcp_identity_url: GCPAuthenticator::DEFAULT_METADATA_URL,
130
+ remote_ip: nil)
131
+ @remote_ip = remote_ip
132
+ @authenticator = GCPAuthenticator.new(account, identity,
133
+ jwt: jwt, gcp_identity_url: gcp_identity_url)
134
+ self
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'uri'
5
+
6
+ module Conjur
7
+ class API
8
+ # Authenticator that uses AWS IAM credentials to obtain Conjur access tokens.
9
+ # Signs an AWS STS GetCallerIdentity request with SigV4, then POSTs the
10
+ # resulting headers as JSON to the Conjur authn-iam endpoint.
11
+ #
12
+ # Requires the +aws-sdk-core+ gem (not bundled with conjur-api).
13
+ # Add `gem 'aws-sdk-core'` to your Gemfile to use this authenticator.
14
+ class IAMAuthenticator
15
+ include TokenExpiration
16
+
17
+ DEFAULT_SERVICE_ID = 'prod'.freeze
18
+ DEFAULT_REGION = 'us-east-1'.freeze
19
+
20
+ STS_GLOBAL_URL = 'https://sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15'.freeze
21
+ STS_REGIONAL_URL = 'https://sts.%<region>s.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15'.freeze
22
+ STS_GLOBAL_SIGNING_REGION = 'us-east-1'.freeze
23
+
24
+ attr_reader :account, :service_id, :identity, :aws_region
25
+
26
+ def initialize(account, service_id, identity, aws_region = DEFAULT_REGION)
27
+ @account = account
28
+ @service_id = service_id
29
+ @identity = identity
30
+ @aws_region = aws_region
31
+ update_token_born
32
+ end
33
+
34
+ def refresh_token
35
+ Conjur::API.authenticate_iam(identity, account: account,
36
+ service_id: service_id,
37
+ aws_region: aws_region).tap do
38
+ update_token_born
39
+ end
40
+ end
41
+ end
42
+
43
+ class << self
44
+ # Create a {Conjur::API} instance authenticated via AWS IAM (authn-iam).
45
+ #
46
+ # AWS credentials are resolved from the default chain: environment variables
47
+ # (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN), then
48
+ # ~/.aws/credentials, then the EC2 instance profile.
49
+ #
50
+ # Requires the +aws-sdk-core+ gem.
51
+ #
52
+ # @param [String] identity Conjur host identity (e.g. "host/my-app/my-role")
53
+ # @param [String] service_id the authn-iam service ID (default: "prod")
54
+ # @param [String] account the Conjur organization account
55
+ # @param [String] aws_region AWS region for STS signing. Use "global" for the
56
+ # global STS endpoint (signed as us-east-1).
57
+ # @param [String, nil] remote_ip optional IP address recorded in the audit log
58
+ # @return [Conjur::API]
59
+ def new_from_iam(identity,
60
+ account: Conjur.configuration.account,
61
+ service_id: IAMAuthenticator::DEFAULT_SERVICE_ID,
62
+ aws_region: IAMAuthenticator::DEFAULT_REGION,
63
+ remote_ip: nil)
64
+ self.new.init_from_iam(identity, account: account, service_id: service_id,
65
+ aws_region: aws_region, remote_ip: remote_ip)
66
+ end
67
+
68
+ # Authenticate via authn-iam and return a parsed Conjur access token.
69
+ #
70
+ # @param [String] identity Conjur host identity
71
+ # @param [String] service_id the authn-iam service ID
72
+ # @param [String] account the Conjur organization account
73
+ # @param [String] aws_region AWS region for STS signing
74
+ # @return [Hash] parsed access token
75
+ def authenticate_iam(identity,
76
+ account: Conjur.configuration.account,
77
+ service_id: IAMAuthenticator::DEFAULT_SERVICE_ID,
78
+ aws_region: IAMAuthenticator::DEFAULT_REGION)
79
+ raise ArgumentError, "identity is required" if identity.nil? || identity.empty?
80
+ raise ArgumentError, "service_id is required" if service_id.nil? || service_id.empty?
81
+
82
+ begin
83
+ require 'aws-sdk-core'
84
+ rescue LoadError
85
+ raise LoadError,
86
+ "aws-sdk-core is required for IAM authentication. " \
87
+ "Add `gem 'aws-sdk-core'` to your Gemfile."
88
+ end
89
+
90
+ if Conjur.log
91
+ Conjur.log << "Authenticating #{identity} to account #{account} via authn-iam/#{service_id}\n"
92
+ end
93
+
94
+ signed_headers = build_iam_signed_headers(aws_region)
95
+ JSON.parse(url_for(:authn_iam_authenticate, account, service_id, identity)
96
+ .post(signed_headers.to_json, content_type: 'application/json'))
97
+ end
98
+
99
+ private
100
+
101
+ def build_iam_signed_headers(aws_region)
102
+ signing_region = aws_region == 'global' ? IAMAuthenticator::STS_GLOBAL_SIGNING_REGION : aws_region
103
+ sts_url = aws_region == 'global' || aws_region == IAMAuthenticator::STS_GLOBAL_SIGNING_REGION \
104
+ ? IAMAuthenticator::STS_GLOBAL_URL
105
+ : format(IAMAuthenticator::STS_REGIONAL_URL, region: aws_region)
106
+
107
+ provider = Aws::CredentialProviderChain.new.resolve
108
+ raise ArgumentError,
109
+ "No AWS credentials found. Configure an instance profile, " \
110
+ "environment variables (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY), " \
111
+ "or ~/.aws/credentials." unless provider
112
+
113
+ creds = provider.credentials
114
+ signer = Aws::Sigv4::Signer.new(
115
+ service: 'sts',
116
+ region: signing_region,
117
+ access_key_id: creds.access_key_id,
118
+ secret_access_key: creds.secret_access_key,
119
+ session_token: creds.session_token
120
+ )
121
+
122
+ signed = signer.sign_request(http_method: 'GET', url: sts_url, headers: {}, body: '')
123
+ headers = signed.headers.dup
124
+ # Conjur expects these specific keys
125
+ headers['host'] = URI.parse(sts_url).host
126
+ headers.compact
127
+ end
128
+ end
129
+
130
+ def init_from_iam(identity,
131
+ account: Conjur.configuration.account,
132
+ service_id: IAMAuthenticator::DEFAULT_SERVICE_ID,
133
+ aws_region: IAMAuthenticator::DEFAULT_REGION,
134
+ remote_ip: nil)
135
+ @remote_ip = remote_ip
136
+ @authenticator = IAMAuthenticator.new(account, service_id, identity, aws_region)
137
+ self
138
+ end
139
+ end
140
+ end
@@ -42,6 +42,33 @@ module Conjur
42
42
  )[fully_escape account][fully_escape username]['authenticate']
43
43
  end
44
44
 
45
+ # POST /authn-azure/{service_id}/{account}/{login}/authenticate
46
+ def authn_azure_authenticate account, service_id, identity
47
+ RestClient::Resource.new(
48
+ Conjur.configuration.core_url,
49
+ Conjur.configuration.rest_client_options
50
+ )['authn-azure'][fully_escape service_id][fully_escape account][fully_escape identity]['authenticate']
51
+ end
52
+
53
+ # POST /authn-iam/{service_id}/{account}/{login}/authenticate
54
+ def authn_iam_authenticate account, service_id, identity
55
+ RestClient::Resource.new(
56
+ Conjur.configuration.core_url,
57
+ Conjur.configuration.rest_client_options
58
+ )['authn-iam'][fully_escape service_id][fully_escape account][fully_escape identity]['authenticate']
59
+ end
60
+
61
+ # POST /authn-gcp/{account}/authenticate
62
+ # authn-gcp is a serviceless authenticator: Conjur derives the host from
63
+ # the GCP identity token's claims, so neither a service id nor the login
64
+ # appears in the path.
65
+ def authn_gcp_authenticate account
66
+ RestClient::Resource.new(
67
+ Conjur.configuration.core_url,
68
+ Conjur.configuration.rest_client_options
69
+ )['authn-gcp'][fully_escape account]['authenticate']
70
+ end
71
+
45
72
  # Builds the RestClient::Resource for an authn-cert authentication request.
46
73
  # cert_options must include :ssl_client_cert and :ssl_client_key so that
47
74
  # the client certificate is presented during the TLS handshake.
@@ -62,10 +89,14 @@ module Conjur
62
89
  end
63
90
 
64
91
  def authenticator account, authenticator, service_id, credentials
65
- RestClient::Resource.new(
92
+ resource = RestClient::Resource.new(
66
93
  Conjur.configuration.core_url,
67
94
  Conjur.configuration.create_rest_client_options(credentials)
68
- )[fully_escape authenticator][fully_escape service_id][fully_escape account]
95
+ )[fully_escape authenticator]
96
+ # Serviceless authenticators (e.g. authn-gcp) have no service id segment:
97
+ # the path is authn-<type>/<account>.
98
+ resource = resource[fully_escape service_id] unless service_id.nil? || service_id.empty?
99
+ resource[fully_escape account]
69
100
  end
70
101
 
71
102
  def authenticators
data/lib/conjur/api.rb CHANGED
@@ -36,6 +36,9 @@ require 'conjur/log_source'
36
36
  require 'conjur/has_attributes'
37
37
  require 'conjur/api/authenticators'
38
38
  require 'conjur/api/authn'
39
+ require 'conjur/api/authn_azure'
40
+ require 'conjur/api/authn_iam'
41
+ require 'conjur/api/authn_gcp'
39
42
  require 'conjur/api/authn_cert'
40
43
  require 'conjur/api/roles'
41
44
  require 'conjur/api/resources'
data/secrets.yml ADDED
@@ -0,0 +1,13 @@
1
+ # Summon secrets for CI. `summon -e <env> ./test.sh` resolves the !var
2
+ # references below from the CI Conjur vault and exports them into the
3
+ # environment for the wrapped command.
4
+ #
5
+ # The azure environment supplies the values the authn-azure integration
6
+ # feature needs (see features/authn_azure.feature and features/README.md).
7
+ # Mirrors the layout used by conjur-api-java / conjur-api-dotnet.
8
+ azure:
9
+ RUN_AZURE_TESTS: "true"
10
+ AZURE_SUBSCRIPTION_ID: !var ci/azure/subscription-id
11
+ AZURE_RESOURCE_GROUP: !var ci/azure/authn-test/resource-group
12
+ USER_ASSIGNED_IDENTITY: !var ci/azure/authn-test/user-assigned-id
13
+ USER_ASSIGNED_IDENTITY_CLIENT_ID: !var ci/azure/authn-test/user-assigned-id-client-id
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'cgi'
5
+ require 'conjur/api/router'
6
+
7
+ describe Conjur::API, api: :dummy do
8
+ let(:service_id) { 'my-azure-service' }
9
+ let(:identity) { 'host/my-app/azure-host' }
10
+ let(:azure_jwt) { 'eyJhbGciOiJSUzI1NiJ9.azure-payload.sig' }
11
+ let(:raw_token) { { 'protected' => 'p', 'payload' => 'pl', 'signature' => 's' } }
12
+
13
+ describe '.new_from_azure' do
14
+ it 'returns an API instance with an AzureAuthenticator' do
15
+ allow(Conjur::API).to receive(:authenticate_azure).and_return(raw_token)
16
+ api_instance = Conjur::API.new_from_azure(service_id, identity, account: account)
17
+ expect(api_instance).to be_a(Conjur::API)
18
+ expect(api_instance.authenticator).to be_a(Conjur::API::AzureAuthenticator)
19
+ end
20
+ end
21
+
22
+ describe '.authenticate_azure' do
23
+ let(:resource) { double('resource') }
24
+
25
+ before do
26
+ allow(Conjur::API).to receive(:url_for)
27
+ .with(:authn_azure_authenticate, account, service_id, identity)
28
+ .and_return(resource)
29
+ allow(resource).to receive(:post)
30
+ .with("jwt=#{CGI.escape(azure_jwt)}", content_type: 'application/x-www-form-urlencoded')
31
+ .and_return(raw_token.to_json)
32
+ allow(Conjur::API).to receive(:fetch_azure_jwt).and_return(azure_jwt)
33
+ end
34
+
35
+ it 'returns a parsed token' do
36
+ token = Conjur::API.authenticate_azure(service_id, identity, account: account)
37
+ expect(token).to eq(raw_token)
38
+ end
39
+
40
+ it 'raises ArgumentError when service_id is empty' do
41
+ expect { Conjur::API.authenticate_azure('', identity, account: account) }
42
+ .to raise_error(ArgumentError, /service_id is required/)
43
+ end
44
+
45
+ it 'raises ArgumentError when identity is empty' do
46
+ expect { Conjur::API.authenticate_azure(service_id, '', account: account) }
47
+ .to raise_error(ArgumentError, /identity is required/)
48
+ end
49
+ end
50
+
51
+ describe 'AzureAuthenticator#refresh_token' do
52
+ subject(:authenticator) do
53
+ Conjur::API::AzureAuthenticator.new(account, service_id, identity)
54
+ end
55
+
56
+ it 'delegates to authenticate_azure with stored credentials' do
57
+ expect(Conjur::API).to receive(:authenticate_azure)
58
+ .with(service_id, identity,
59
+ account: account,
60
+ resource_uri: Conjur::API::AzureAuthenticator::DEFAULT_RESOURCE,
61
+ client_id: nil,
62
+ imds_base_url: Conjur::API::AzureAuthenticator::DEFAULT_IMDS_BASE,
63
+ imds_api_version: Conjur::API::AzureAuthenticator::DEFAULT_IMDS_VER)
64
+ .and_return(raw_token)
65
+ expect(authenticator.refresh_token).to eq(raw_token)
66
+ end
67
+ end
68
+ end
69
+
70
+ describe Conjur::API::Router do
71
+ let(:account) { 'myaccount' }
72
+ let(:service_id) { 'my-service' }
73
+ let(:identity) { 'host/my-app/azure-host' }
74
+
75
+ before do
76
+ allow(Conjur.configuration).to receive(:core_url).and_return('https://conjur.example.com')
77
+ allow(Conjur.configuration).to receive(:rest_client_options).and_return({})
78
+ end
79
+
80
+ describe '#authn_azure_authenticate' do
81
+ subject { described_class.authn_azure_authenticate(account, service_id, identity) }
82
+
83
+ it 'returns a RestClient::Resource' do
84
+ expect(subject).to be_a(RestClient::Resource)
85
+ end
86
+
87
+ it 'builds the correct URL' do
88
+ expect(subject.url).to end_with('/authn-azure/my-service/myaccount/host%2Fmy-app%2Fazure-host/authenticate')
89
+ end
90
+ end
91
+ end