pg-azure_workload_identity 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 76a51f189884a5080a32166397a3cde091c99c1db2b4fc6b3e25d14ae8a24c53
4
+ data.tar.gz: 82c6a01050444c0a9d4d99993098dc69aef71402b64774de6ebfe0a6c8f4c452
5
+ SHA512:
6
+ metadata.gz: 2c3e82ebca506d35b28d02a128e0a9a1ea6ffc21295afe28341d4291820f1a917babd3ceea8a295725328a20e39b1feddf50e4b07d8b8c890b58a90700a17c4b
7
+ data.tar.gz: c11bee40a5458703988050efd757f90a3e154aec5bda21604fc5d2dc3c26afe69540930fc97c2841185d71c7b226d44f8b3c0d7a0b9d470684f1f3dae1725e18
data/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ --markup markdown
2
+ --markup-provider redcarpet
3
+ --no-private
4
+ --exclude ^sig/
5
+ -
6
+ *.md
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.1.0] - 2026-05-19
8
+
9
+ ### Added
10
+ * A plugin for the [`pg` gem](https://rubygems.org/gems/pg) that adds support for [Entra ID Authentication](https://learn.microsoft.com/azure/postgresql/security/security-entra-concepts) when connecting to PostgreSQL flexible database servers in Microsoft Azure. ([#1](https://github.com/atpoint-cloud/pg-azure_workload_identity/pull/1))
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Simon Schmid
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # PG::AzureWorkloadIdentity
2
+
3
+ [![Gem](https://img.shields.io/gem/v/pg-azure_workload_identity?style=flat-square)](https://rubygems.org/gems/pg-azure_workload_identity)
4
+  
5
+ [![Docs](https://img.shields.io/badge/yard-docs-blue?style=flat-square)](https://www.rubydoc.info/gems/pg-azure_workload_identity)
6
+
7
+ `PG::AzureWorkloadIdentity` is a plugin for the [`pg` gem](https://rubygems.org/gems/pg) that adds support for [Microsoft Entra ID authentication](https://learn.microsoft.com/azure/postgresql/security/security-entra-concepts) when connecting to [Azure Database for PostgreSQL flexible server](https://learn.microsoft.com/azure/postgresql/flexible-server/overview), using [Microsoft Entra Workload Identity](https://learn.microsoft.com/entra/workload-id/workload-identity-federation) — typically as projected into pods by [AKS Workload Identity](https://learn.microsoft.com/azure/aks/workload-identity-overview).
8
+
9
+ Entra ID authentication lets your application connect to the database using short-lived access tokens instead of a fixed password. Combined with Workload Identity, your pods never carry a long-lived secret at all: the Kubernetes service account JWT is automatically exchanged for an Entra access token via [federated credentials](https://learn.microsoft.com/entra/workload-id/workload-identity-federation), and that token is used as the database password. No client secrets, no certificate rotation, no password storage.
10
+
11
+ ## Installation
12
+
13
+ Install manually:
14
+
15
+ ```console
16
+ $ gem install pg-azure_workload_identity
17
+ ```
18
+
19
+ or with Bundler:
20
+
21
+ ```console
22
+ $ bundle add pg-azure_workload_identity
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ To use Workload Identity authentication for your database connections, you need to
28
+
29
+ 1. enable Microsoft Entra authentication on your Azure Database for PostgreSQL flexible server,
30
+ 2. provide your application with a Workload Identity, and
31
+ 3. configure your application to use the gem's token generator.
32
+
33
+ ### 1. Enable Microsoft Entra authentication on your database
34
+
35
+ Configure your Azure Database for PostgreSQL flexible server to allow Microsoft Entra authentication, either during server provisioning or on an existing server through the **Security → Authentication** pane. See [Use Microsoft Entra ID for authentication with Azure Database for PostgreSQL](https://learn.microsoft.com/azure/postgresql/security/security-entra-configure) for the step-by-step instructions.
36
+
37
+ Choose between **Microsoft Entra authentication only** (no password-based logins) and **PostgreSQL and Microsoft Entra authentication** (both modes available), then add at least one Microsoft Entra administrator. Only an Entra administrator can subsequently create additional Entra-mapped database roles.
38
+
39
+ Connect to the database as an Entra administrator and create a role for your application's identity. For a user-assigned managed identity, use its object (principal) ID:
40
+
41
+ ```sql
42
+ select * from pgaadauth_create_principal_with_oid('my-app', '<managed-identity-object-id>', 'service', false, false);
43
+ ```
44
+
45
+ Grant the role whatever privileges your application needs.
46
+
47
+ ### 2. Provide your application with a Workload Identity
48
+
49
+ The recommended way to provide a credential to a pod in AKS is [Microsoft Entra Workload ID](https://learn.microsoft.com/azure/aks/workload-identity-overview). It works by federating a Kubernetes service account with an Entra ID identity, so a JWT projected into the pod can be exchanged for an Entra access token without ever sharing a long-lived secret.
50
+
51
+ The setup steps are (see [Deploy and configure an AKS cluster with Workload ID](https://learn.microsoft.com/azure/aks/workload-identity-deploy-cluster) for the full walkthrough):
52
+
53
+ 1. Enable the OIDC issuer and Workload Identity add-on on your AKS cluster.
54
+ 2. Create a user-assigned managed identity (or use an existing one).
55
+ 3. Create a [federated identity credential](https://learn.microsoft.com/entra/workload-id/workload-identity-federation) on the managed identity, with the cluster's OIDC issuer as the `issuer`, `api://AzureADTokenExchange` as the `audience`, and `system:serviceaccount:<namespace>:<service-account-name>` as the `subject`.
56
+ 4. Create a Kubernetes service account annotated with `azure.workload.identity/client-id: <managed-identity-client-id>` and use it for your application's pods.
57
+ 5. Add `azure.workload.identity/use: "true"` to your pod template's labels.
58
+
59
+ Once that's in place, the Workload Identity mutating webhook automatically injects the following environment variables into your pod, which the gem uses to acquire access tokens:
60
+
61
+ | Variable | Provided by |
62
+ | --- | --- |
63
+ | `AZURE_TENANT_ID` | Workload Identity webhook |
64
+ | `AZURE_CLIENT_ID` | Workload Identity webhook (from the service-account annotation) |
65
+ | `AZURE_FEDERATED_TOKEN_FILE` | Workload Identity webhook (path to the projected service-account JWT) |
66
+
67
+ ### 3. Configure your application to use the workload-identity token generator
68
+
69
+ Set the non-standard `azure_workload_identity` connection parameter to a truthy value (e.g. `true`). When the gem sees this parameter on a connection, it acquires an Entra access token using the environment variables above and injects it as the database password before handing the connection string to `libpq`.
70
+
71
+ You can set this parameter in
72
+
73
+ * the query string of a connection URI:
74
+
75
+ ```
76
+ postgresql://my-app@my-server.postgres.database.azure.com:5432/appdb?sslmode=require&azure_workload_identity=true
77
+ ```
78
+
79
+ * a `key=value` pair in a connection string:
80
+
81
+ ```
82
+ user=my-app host=my-server.postgres.database.azure.com port=5432 dbname=appdb sslmode=require azure_workload_identity=true
83
+ ```
84
+
85
+ * a `key: value` pair in a connection hash:
86
+
87
+ ```ruby
88
+ PG.connect(
89
+ user: "my-app",
90
+ host: "my-server.postgres.database.azure.com",
91
+ port: 5432,
92
+ dbname: "appdb",
93
+ sslmode: "require",
94
+ azure_workload_identity: "true"
95
+ )
96
+ ```
97
+
98
+ * `database.yml`, if you're using Rails:
99
+
100
+ ```yaml
101
+ production:
102
+ adapter: postgresql
103
+ username: my-app
104
+ host: my-server.postgres.database.azure.com
105
+ port: 5432
106
+ database: appdb
107
+ sslmode: require
108
+ azure_workload_identity: true
109
+ ```
110
+
111
+ The gem caches access tokens in memory for their reported lifetime (with a small refresh threshold), so concurrent and back-to-back connections share a single token exchange. Tokens are refreshed automatically on demand before they expire.
112
+
113
+ If the defaults don't match your environment (for example, you're using a sovereign cloud or a non-OSS-RDBMS scope), you can install a custom token generator at boot:
114
+
115
+ ```ruby
116
+ PG::AzureWorkloadIdentity.auth_token_generator = PG::AzureWorkloadIdentity::AuthTokenGenerator.new(
117
+ identity_endpoint: "https://login.microsoftonline.us/<tenant-id>/oauth2/v2.0/token",
118
+ client_id: "<client-id>",
119
+ scope: "https://ossrdbms-aad.database.usgovcloudapi.net/.default",
120
+ grant_type: "client_credentials",
121
+ client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
122
+ federated_token_file: "/var/run/secrets/azure/tokens/azure-identity-token"
123
+ )
124
+ ```
125
+
126
+ The object you assign just needs to respond to `#call` and return a string token, so you can swap in an entirely different implementation when needed.
127
+
128
+ ### 4. Set `sslmode` to `verify-full` (recommended)
129
+
130
+ Although Azure Database for PostgreSQL flexible server requires TLS by default (`sslmode=require`), the strongest setting is `sslmode=verify-full` — this also verifies the server's certificate chain and hostname, preventing man-in-the-middle attacks. You'll need to point `sslrootcert` at the Azure-issued certificate bundle; see [TLS certificates for Azure Database for PostgreSQL flexible server](https://learn.microsoft.com/azure/postgresql/flexible-server/concepts-networking-ssl-tls#downloading-root-ca-certificates-and-updating-application-clients-in-certificate-pinning-scenarios) for the current chain and download instructions. Note that this might no be possible when using private endpoints with entries in private DNS zones.
131
+
132
+ ## Development
133
+
134
+ After checking out the repo, run `bundle install` to install dependencies.
135
+ Then, run `bundle exec rake` to run the linter and unit tests.
136
+
137
+ The acceptance suite spins up a local PostgreSQL container via Docker Compose and exercises the full connection flow (with WebMock stubbing the Entra token endpoint) — run it with:
138
+
139
+ ```console
140
+ $ bundle exec rake test:acceptance
141
+ ```
142
+
143
+ To release a new version:
144
+
145
+ 1. Update the version number in [version.rb](lib/pg/azure_workload_identity/version.rb), and run `bundle install` to update [Gemfile.lock](Gemfile.lock).
146
+ 2. Update [CHANGELOG.md](CHANGELOG.md).
147
+ 3. Submit the changes as a pull request.
148
+ 4. Once merged, tag the release and push the tag to GitHub.
149
+
150
+ ## Contributing
151
+
152
+ Bug reports and pull requests are welcome [on GitHub](https://github.com/atpoint-cloud/pg-azure_workload_identity).
153
+
154
+ ## Credits
155
+
156
+ This gem is built on the design and structure of [`pg-aws_rds_iam`](https://github.com/haines/pg-aws_rds_iam) which provides the equivalent functionality for AWS RDS IAM authentication.
157
+
158
+ ## License
159
+
160
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "auth_token_injector"
4
+
5
+ module PG
6
+ module AzureWorkloadIdentity
7
+ # Patch prepended to
8
+ # `ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.singleton_class`
9
+ # that wires Azure Workload Identity authentication into the
10
+ # `rails dbconsole` / `rails db` flow.
11
+ #
12
+ # The adapter's `dbconsole` class method is responsible for launching
13
+ # `psql` against the configured database; without this patch, `psql`
14
+ # would prompt for (or fail without) a password when the configuration
15
+ # opts into workload identity. The override generates an auth token and
16
+ # exposes it via `PGPASSWORD` before delegating to the original
17
+ # implementation.
18
+ module ActiveRecordPostgreSQLAdapter
19
+ # Sets `PGPASSWORD` on the environment to a freshly generated Azure
20
+ # Workload Identity auth token when the configuration enables it, then
21
+ # delegates to the original `dbconsole` implementation.
22
+ #
23
+ # @param config [ActiveRecord::DatabaseConfigurations::DatabaseConfig]
24
+ # the configuration for the database `psql` is being launched
25
+ # against.
26
+ # @return [void]
27
+ def dbconsole(config, options = {})
28
+ AuthTokenInjector.new.inject_into_psql_env! config.configuration_hash, ENV
29
+ super
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "auth_token_injector"
4
+
5
+ module PG
6
+ module AzureWorkloadIdentity
7
+ # Patch prepended to
8
+ # `ActiveRecord::Tasks::PostgreSQLDatabaseTasks` that wires Azure
9
+ # Workload Identity authentication into rake tasks that shell out to
10
+ # `psql` (e.g. `db:structure:load`, `db:structure:dump`).
11
+ #
12
+ # ActiveRecord builds an environment hash for the `psql` subprocess via
13
+ # the private `#psql_env` method; this override generates an auth token
14
+ # and adds it to that hash as `PGPASSWORD` whenever the configuration
15
+ # opts into workload identity, so the `psql` invocation authenticates
16
+ # without prompting.
17
+ module ActiveRecordPostgreSQLDatabaseTasks
18
+ private
19
+
20
+ # Builds the environment hash for the `psql` subprocess, injecting a
21
+ # freshly generated Azure Workload Identity auth token as
22
+ # `PGPASSWORD` when the configuration enables it.
23
+ #
24
+ # @return [Hash{String => String}] the environment hash to pass to
25
+ # the `psql` subprocess.
26
+ def psql_env
27
+ super.tap do |psql_env|
28
+ AuthTokenInjector.new.inject_into_psql_env! configuration_hash, psql_env
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require_relative "error"
6
+
7
+ module PG
8
+ module AzureWorkloadIdentity
9
+ # Wraps a fetched OAuth access token together with the moment it was
10
+ # generated, and answers whether it is still safely usable. Validity
11
+ # is measured against the monotonic clock so the answer is not
12
+ # affected by wall-clock jumps (NTP slews, VM clock corrections).
13
+ class AuthToken
14
+ # Seconds before the reported expiry at which the token is
15
+ # considered stale, so callers proactively refresh before it
16
+ # actually expires in flight.
17
+ REFRESH_THRESHOLD_SECONDS = 60
18
+
19
+ # @return [String] the bearer access token.
20
+ attr_reader :access_token
21
+
22
+ # Parses a token response from the Azure AD token endpoint and builds
23
+ # an {AuthToken} instance.
24
+ #
25
+ # @param json [String] the raw JSON response body from the token endpoint.
26
+ # @return [AuthToken] the parsed token.
27
+ # @raise [Error] when the JSON is invalid or lacks the expected fields.
28
+ def self.from_json(json)
29
+ JSON.parse(json).then do |data|
30
+ new(
31
+ access_token: data.fetch("access_token").to_s,
32
+ expires_in: data.fetch("expires_in").to_i
33
+ )
34
+ end
35
+ rescue JSON::ParserError => e
36
+ raise Error, "Failed to parse token response from JSON: #{e.message}"
37
+ rescue KeyError => e
38
+ raise Error, "Token response is missing key #{e.key} in #{e.receiver}"
39
+ end
40
+
41
+ # @param access_token [String] the bearer access token returned by
42
+ # the token endpoint.
43
+ # @param expires_in [Integer, String] seconds-until-expiry as
44
+ # reported by the token endpoint.
45
+ # @param refresh_threshold [Integer] seconds before the reported
46
+ # expiry at which the token should be considered stale.
47
+ def initialize(
48
+ access_token:,
49
+ expires_in:,
50
+ refresh_threshold: REFRESH_THRESHOLD_SECONDS
51
+ )
52
+ @access_token = access_token
53
+ @expiry = expires_in
54
+ @generated_at = now
55
+ @refresh_threshold = refresh_threshold
56
+ end
57
+
58
+ # @return [Boolean] `true` if the token is still valid with the
59
+ # refresh threshold applied.
60
+ def valid?
61
+ (now - @generated_at) < (@expiry - @refresh_threshold)
62
+ end
63
+
64
+ private
65
+
66
+ def now
67
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "openssl"
5
+ require "uri"
6
+
7
+ require_relative "auth_token"
8
+ require_relative "error"
9
+
10
+ module PG
11
+ module AzureWorkloadIdentity
12
+ # Exchanges a Kubernetes-projected federated identity token (as mounted
13
+ # into pods by AKS Workload Identity) for an Azure AD access token via the
14
+ # OAuth 2.0 client-credentials flow with a JWT bearer client assertion.
15
+ #
16
+ # Instances are callable via {#call}, which returns a cached access token
17
+ # while it is still valid (with a refresh threshold applied) and otherwise
18
+ # fetches a new one. Token fetches are guarded by a mutex so concurrent
19
+ # callers share a single in-flight request.
20
+ #
21
+ # Configuration may be passed explicitly to {#initialize} or populated
22
+ # from the standard Azure Workload Identity environment variables via
23
+ # {.default}.
24
+ class AuthTokenGenerator
25
+ IDENTITY_ENDPOINT = "https://login.microsoftonline.com/%<tenant_id>s/oauth2/v2.0/token"
26
+ SCOPE = "https://ossrdbms-aad.database.windows.net/.default"
27
+ GRANT_TYPE = "client_credentials"
28
+ CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
29
+
30
+ # Builds an {AuthTokenGenerator} using the standard Azure Workload
31
+ # Identity environment variables (`AZURE_TENANT_ID`,
32
+ # `AZURE_CLIENT_ID`, `AZURE_FEDERATED_TOKEN_FILE`) and conventional
33
+ # OAuth defaults.
34
+ #
35
+ # @return [AuthTokenGenerator]
36
+ def self.default
37
+ new(
38
+ identity_endpoint: format(IDENTITY_ENDPOINT, tenant_id: ENV.fetch("AZURE_TENANT_ID")),
39
+ client_id: ENV.fetch("AZURE_CLIENT_ID"),
40
+ scope: SCOPE,
41
+ grant_type: GRANT_TYPE,
42
+ client_assertion_type: CLIENT_ASSERTION_TYPE,
43
+ federated_token_file: ENV.fetch("AZURE_FEDERATED_TOKEN_FILE")
44
+ )
45
+ end
46
+
47
+ # @return [String] the Azure AD token endpoint URL.
48
+ attr_reader :identity_endpoint
49
+
50
+ # @return [String] the AAD application/client id.
51
+ attr_reader :client_id
52
+
53
+ # @return [String] the requested OAuth scope.
54
+ attr_reader :scope
55
+
56
+ # @return [String] the OAuth grant type
57
+ # (typically `"client_credentials"`).
58
+ attr_reader :grant_type
59
+
60
+ # @return [String] the client-assertion type
61
+ # (typically `"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"`).
62
+ attr_reader :client_assertion_type
63
+
64
+ # @return [String] absolute path to the file containing the
65
+ # Kubernetes-projected federated identity JWT.
66
+ attr_reader :federated_token_file
67
+
68
+ # @param identity_endpoint [String] the Azure AD token endpoint.
69
+ # @param client_id [String] the AAD application id.
70
+ # @param scope [String] the requested OAuth scope.
71
+ # @param grant_type [String] the OAuth grant type.
72
+ # @param client_assertion_type [String] the client-assertion type.
73
+ # @param federated_token_file [String] path to the projected
74
+ # federated identity JWT.
75
+ def initialize( # rubocop:disable Metrics/ParameterLists
76
+ identity_endpoint:,
77
+ client_id:,
78
+ scope:,
79
+ grant_type:,
80
+ client_assertion_type:,
81
+ federated_token_file:
82
+ )
83
+ @identity_endpoint = URI.parse(identity_endpoint)
84
+ @client_id = client_id
85
+ @scope = scope
86
+ @grant_type = grant_type
87
+ @client_assertion_type = client_assertion_type
88
+ @federated_token_file = federated_token_file
89
+ @mutex = Mutex.new
90
+ @token = nil
91
+ end
92
+
93
+ # Returns a currently valid Azure AD access token, fetching a new one
94
+ # from the identity endpoint when the cached token is missing or about
95
+ # to expire. Thread-safe: concurrent callers share a single in-flight
96
+ # fetch.
97
+ #
98
+ # @return [String] the bearer access token.
99
+ # @raise [Error] when the federated token cannot be read, the HTTP
100
+ # request fails, the response is non-2xx, or the response body is
101
+ # not valid JSON / lacks the expected fields.
102
+ def call
103
+ @mutex.synchronize do
104
+ @token = refresh unless @token&.valid?
105
+ @token.access_token
106
+ end
107
+ end
108
+
109
+ private
110
+
111
+ def refresh
112
+ response = request_token
113
+ return AuthToken.from_json(response.body) if response.is_a?(Net::HTTPSuccess)
114
+
115
+ raise Error, "Azure AD token endpoint responded with #{response.code} #{response.message}: #{response.body}"
116
+ end
117
+
118
+ def request_token
119
+ Net::HTTP.start(
120
+ @identity_endpoint.hostname.to_s,
121
+ @identity_endpoint.port,
122
+ use_ssl: @identity_endpoint.scheme == "https"
123
+ ) do |http|
124
+ http.request(build_request)
125
+ end
126
+ rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout,
127
+ SocketError, IOError, SystemCallError, OpenSSL::SSL::SSLError => e
128
+ raise Error, "Failed to reach Azure AD token endpoint: #{e.class}: #{e.message}"
129
+ end
130
+
131
+ def build_request
132
+ Net::HTTP::Post.new(@identity_endpoint).tap do |request|
133
+ request["Content-Type"] = "application/x-www-form-urlencoded"
134
+ request.body = URI.encode_www_form(
135
+ client_id: client_id,
136
+ scope: scope,
137
+ client_assertion_type: client_assertion_type,
138
+ client_assertion: read_federated_token,
139
+ grant_type: grant_type
140
+ )
141
+ end
142
+ end
143
+
144
+ def read_federated_token
145
+ File.read(federated_token_file).strip
146
+ rescue SystemCallError, IOError => e
147
+ raise Error, "Failed to read federated token file #{federated_token_file.inspect}: #{e.class}: #{e.message}"
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pg"
4
+ require_relative "connection_info"
5
+
6
+ module PG
7
+ module AzureWorkloadIdentity
8
+ # AuthTokenInjector is responsible for injecting auth tokens into connection
9
+ # strings and PSQL environment variables when required.
10
+ class AuthTokenInjector
11
+ # Initializes a new AuthTokenInjector.
12
+ #
13
+ # The generator is resolved lazily — only on the code paths that
14
+ # actually need a token — so that constructing an injector is cheap
15
+ # and side-effect-free for connections that don't opt into workload
16
+ # identity.
17
+ #
18
+ # @param auth_token_generator [#call, nil] A callable that generates
19
+ # an auth token. Defaults to the module-level generator at the time
20
+ # a token is first needed.
21
+ def initialize(auth_token_generator: nil)
22
+ @auth_token_generator = auth_token_generator
23
+ end
24
+
25
+ # Injects an auth token into the given connection string as password if required.
26
+ #
27
+ # @param connection_string [String] The original connection string.
28
+ # @return [String] The modified connection string with the auth token injected if required.
29
+ def inject_into_connection_string(connection_string)
30
+ connection_info = ConnectionInfo.from_connection_string(connection_string)
31
+ return connection_string unless generate_auth_token?(connection_info)
32
+
33
+ connection_info.password = auth_token_generator.call
34
+ connection_info.to_s
35
+ end
36
+
37
+ # Injects an auth token into the given PSQL environment hash if required.
38
+ # This is used for the db:console rake task.
39
+ #
40
+ # @param configuration_hash [Hash] The ActiveRecord connection configuration hash from database.yml
41
+ # @param psql_env [Hash] The environment hash to be passed to the PSQL process.
42
+ def inject_into_psql_env!(configuration_hash, psql_env)
43
+ connection_info = ConnectionInfo.from_active_record_configuration_hash(configuration_hash)
44
+ return unless generate_auth_token?(connection_info)
45
+
46
+ psql_env["PGPASSWORD"] = auth_token_generator.call
47
+ end
48
+
49
+ private
50
+
51
+ # Resolves the auth token generator lazily, falling back to the
52
+ # module-level default only when a token is actually needed.
53
+ def auth_token_generator
54
+ @auth_token_generator ||= AzureWorkloadIdentity.auth_token_generator
55
+ end
56
+
57
+ # Determines whether an auth token should be generated based on the connection information.
58
+ #
59
+ # @param connection_info [ConnectionInfo] The parsed connection information.
60
+ # @return [Boolean] true if `azure_workload_identity` is set, false otherwise.
61
+ def generate_auth_token?(connection_info)
62
+ connection_info.azure_workload_identity
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "auth_token_injector"
4
+ require_relative "connection_info"
5
+
6
+ module PG
7
+ module AzureWorkloadIdentity
8
+ # Patch prepended to `PG::Connection.singleton_class` that teaches libpq's
9
+ # connection-parameter machinery about the non-standard
10
+ # `azure_workload_identity` option and injects a freshly generated auth
11
+ # token as the password when that option is set.
12
+ #
13
+ # The three overrides cooperate:
14
+ # - {#conndefaults} adds `azure_workload_identity` to the list of known
15
+ # parameters so it is recognized rather than rejected as unknown.
16
+ # - {#conninfo_parse} parses a connection string while preserving the
17
+ # value of `azure_workload_identity` in the returned parameter list
18
+ # (libpq would otherwise drop it).
19
+ # - {#parse_connect_args} runs the final connection string through
20
+ # {AuthTokenInjector} so the password is replaced with a generated
21
+ # token whenever `azure_workload_identity` is set.
22
+ module Connection
23
+ # Lists libpq's connection parameter defaults, with the
24
+ # `azure_workload_identity` option appended.
25
+ #
26
+ # @return [Array<Hash>] the connection parameter defaults.
27
+ def conndefaults
28
+ super + [conndefault_azure_workload_identity]
29
+ end
30
+
31
+ # Parses a connection string into the libpq parameter array, preserving
32
+ # the `azure_workload_identity` option.
33
+ #
34
+ # The input is first round-tripped through {ConnectionInfo} so the
35
+ # custom option is stripped before being handed to libpq's parser, and
36
+ # then re-appended to the parsed result with its original value.
37
+ #
38
+ # @param connection_string [String] either a URI-style or key=value
39
+ # connection string.
40
+ # @return [Array<Hash>] the parsed parameter array, including the
41
+ # `azure_workload_identity` entry when set.
42
+ def conninfo_parse(connection_string)
43
+ connection_info = ConnectionInfo.from_connection_string(connection_string)
44
+
45
+ super(connection_info.to_s).tap do |result|
46
+ if connection_info.azure_workload_identity
47
+ result << conndefault_azure_workload_identity.merge(val: connection_info.azure_workload_identity)
48
+ end
49
+ end
50
+ end
51
+
52
+ # Normalizes connection arguments and injects an Azure Workload Identity
53
+ # auth token as the password when `azure_workload_identity` is set.
54
+ #
55
+ # @param args [Array] the arguments passed to `PG::Connection.new` /
56
+ # `PG.connect` (forwarded to `super`).
57
+ # @return [String] the connection string with the auth token injected
58
+ # if applicable, otherwise unchanged.
59
+ def parse_connect_args(...)
60
+ AuthTokenInjector.new.inject_into_connection_string(super)
61
+ end
62
+
63
+ private
64
+
65
+ # Connection-parameter descriptor for the `azure_workload_identity`
66
+ # option, matching the shape of entries returned by libpq's
67
+ # `PQconndefaults`.
68
+ #
69
+ # @return [Hash] the descriptor hash.
70
+ def conndefault_azure_workload_identity
71
+ {
72
+ keyword: "azure_workload_identity",
73
+ envvar: nil,
74
+ compiled: nil,
75
+ val: nil,
76
+ label: "Azure-Workload-Identity",
77
+ dispchar: "",
78
+ dispsize: 64
79
+ }
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PG
4
+ module AzureWorkloadIdentity
5
+ module ConnectionInfo
6
+ # Wraps an ActiveRecord database configuration hash (as produced from
7
+ # `config/database.yml`) and exposes the connection parameters relevant
8
+ # to Azure Workload Identity authentication, including the non-standard
9
+ # `azure_workload_identity` flag.
10
+ #
11
+ # Accessor names follow this gem's connection-info contract (`user`,
12
+ # `host`, `port`) rather than ActiveRecord's keys (note the
13
+ # `:username` -> `#user` mapping).
14
+ class ActiveRecordConfigurationHash
15
+ # @param configuration_hash [Hash{Symbol => Object}] an ActiveRecord
16
+ # database configuration hash (symbol keys, as found under an
17
+ # environment in `database.yml`).
18
+ def initialize(configuration_hash)
19
+ @configuration_hash = configuration_hash
20
+ end
21
+
22
+ # @return [Object, nil] the value of the `azure_workload_identity` key,
23
+ # or `nil` if not set. Truthy values opt the connection into Azure
24
+ # Workload Identity authentication.
25
+ def azure_workload_identity
26
+ @configuration_hash[:azure_workload_identity]
27
+ end
28
+
29
+ # @return [String, nil] the database username (ActiveRecord's
30
+ # `:username` key).
31
+ def user
32
+ @configuration_hash[:username]
33
+ end
34
+
35
+ # @return [String, nil] the database host.
36
+ def host
37
+ @configuration_hash[:host]
38
+ end
39
+
40
+ # @return [Integer, String, nil] the database port.
41
+ def port
42
+ @configuration_hash[:port]
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+ require "pg"
5
+
6
+ require_relative "../error"
7
+
8
+ module PG
9
+ module AzureWorkloadIdentity
10
+ module ConnectionInfo
11
+ # Parses and manipulates a PostgreSQL connection string in libpq's
12
+ # key=value format (e.g. `"host=localhost user=admin port=5432"`).
13
+ #
14
+ # The parser preserves functional parity with libpq's `conninfo_parse`,
15
+ # supporting whitespace around the `=`, single-quoted values, and
16
+ # backslash escapes (`\\X` -> `X`) in both quoted and unquoted values.
17
+ #
18
+ # The non-standard `azure_workload_identity` parameter is recognized and
19
+ # extracted on construction so it can drive Azure Workload Identity
20
+ # authentication without being forwarded to libpq.
21
+ #
22
+ # @see https://github.com/postgres/postgres/blob/REL_18_4/src/interfaces/libpq/fe-connect.c#L6289-L6444
23
+ class KeyValueString
24
+ # @return [String, nil] the value of the `azure_workload_identity`
25
+ # parameter that was present in the original connection string, or
26
+ # `nil` if the parameter was not set.
27
+ attr_reader :azure_workload_identity
28
+
29
+ # Parses the given connection string and extracts the
30
+ # `azure_workload_identity` parameter (if any) so it is not forwarded to
31
+ # libpq.
32
+ #
33
+ # @param connection_string [String] a PostgreSQL key=value connection
34
+ # string.
35
+ # @raise [Error] if the connection string is malformed.
36
+ def initialize(connection_string)
37
+ @params = Parser.new(connection_string).parse
38
+ @azure_workload_identity = @params.delete(:azure_workload_identity)
39
+ end
40
+
41
+ # @return [String, nil] the value of the `user` parameter.
42
+ def user
43
+ @params[:user]
44
+ end
45
+
46
+ # @return [String, nil] the value of the `host` parameter.
47
+ def host
48
+ @params[:host]
49
+ end
50
+
51
+ # @return [String, nil] the value of the `port` parameter.
52
+ def port
53
+ @params[:port]
54
+ end
55
+
56
+ # Sets the `password` parameter.
57
+ #
58
+ # @param value [String] the password to set.
59
+ # @return [String] the value that was set.
60
+ def password=(value)
61
+ @params[:password] = value
62
+ end
63
+
64
+ # Serializes the parameters back to a libpq key=value connection
65
+ # string. Values are quoted/escaped via `PG::Connection.quote_connstr`
66
+ # so they remain valid when whitespace or special characters are
67
+ # present. The `azure_workload_identity` parameter extracted at
68
+ # construction is not included.
69
+ #
70
+ # @return [String] the connection string.
71
+ def to_s
72
+ @params.map { |key, value| "#{key}=#{PG::Connection.quote_connstr(value)}" }.join(" ")
73
+ end
74
+
75
+ # @return [Hash{Symbol => String}] a copy of the parsed parameters
76
+ # (excluding `azure_workload_identity`, which was extracted at
77
+ # construction).
78
+ def to_h
79
+ @params.dup
80
+ end
81
+
82
+ # Tokenizes a libpq key=value connection string into a hash of
83
+ # symbol-keyed parameters.
84
+ #
85
+ # The grammar mirrors `conninfo_parse`: whitespace separates pairs,
86
+ # whitespace is allowed around `=`, values may be single-quoted, and
87
+ # `\\X` unescapes to `X` in both quoted and unquoted values. A
88
+ # trailing backslash at EOF inside an unquoted value is silently
89
+ # dropped (matching libpq).
90
+ #
91
+ # @see https://github.com/postgres/postgres/blob/REL_18_4/src/interfaces/libpq/fe-connect.c#L6289-L6444
92
+ class Parser
93
+ # @param connection_string [String] the connection string to parse.
94
+ def initialize(connection_string)
95
+ @buffer = StringScanner.new(connection_string)
96
+ end
97
+
98
+ # Parses the connection string passed to the constructor.
99
+ #
100
+ # @return [Hash{Symbol => String}] the parsed parameters, in the
101
+ # order they appeared in the input. Later occurrences of the same
102
+ # key overwrite earlier ones.
103
+ # @raise [Error] when a keyword is missing, `=` is missing
104
+ # after a keyword, or a quoted value is not terminated.
105
+ def parse # rubocop:disable Metrics/MethodLength
106
+ result = {} # : Hash[Symbol, String]
107
+
108
+ skip_whitespace
109
+ until @buffer.eos?
110
+ key = parse_key
111
+ skip_whitespace
112
+ assert_followed_by_equals_sign key
113
+ skip_whitespace
114
+ value = parse_value
115
+ skip_whitespace
116
+
117
+ result[key] = value
118
+ end
119
+
120
+ @buffer.reset
121
+ result
122
+ end
123
+
124
+ private
125
+
126
+ # Advances the scanner past any run of whitespace characters.
127
+ #
128
+ # @return [Integer, nil] the number of characters consumed, or
129
+ # `nil` if none matched.
130
+ def skip_whitespace
131
+ @buffer.skip(/\s+/)
132
+ end
133
+
134
+ # Reads the next keyword (any run of non-whitespace, non-`=`
135
+ # characters) and returns it as a symbol.
136
+ #
137
+ # @return [Symbol] the keyword.
138
+ # @raise [Error] if no keyword characters are present at the
139
+ # current position.
140
+ def parse_key
141
+ key = @buffer.scan(/[^=\s]+/)
142
+ raise Error, %(Missing parameter name before "#{@buffer.rest}") unless key
143
+
144
+ key.to_sym
145
+ end
146
+
147
+ # Consumes a single `=` character at the current position.
148
+ #
149
+ # @param key [Symbol] the keyword that should be followed by `=`
150
+ # (used in the error message).
151
+ # @raise [Error] if the next character is not `=`.
152
+ # @return [void]
153
+ def assert_followed_by_equals_sign(key)
154
+ raise Error, %(Missing "=" after "#{key}") unless @buffer.getch == "="
155
+ end
156
+
157
+ # Reads a value, dispatching to the quoted or unquoted form based
158
+ # on whether the next character is a single quote.
159
+ #
160
+ # @return [String] the parsed value (may be empty).
161
+ # @raise [Error] if a quoted value is not terminated.
162
+ def parse_value
163
+ if @buffer.peek(1) == "'"
164
+ parse_quoted_value
165
+ else
166
+ parse_unquoted_value
167
+ end
168
+ end
169
+
170
+ # Reads a single-quoted value, applying `\\X` -> `X` escape
171
+ # handling.
172
+ #
173
+ # @return [String] the value with quotes stripped and escapes
174
+ # applied.
175
+ # @raise [Error] if the closing quote is missing.
176
+ def parse_quoted_value
177
+ value = +""
178
+ @buffer.skip(/'/)
179
+ loop do
180
+ value << (@buffer.scan(/[^'\\]+/) || "")
181
+ case @buffer.getch
182
+ when "'" then return value
183
+ when "\\" then value << (@buffer.getch || "")
184
+ else raise Error, "Unterminated quoted value"
185
+ end
186
+ end
187
+ end
188
+
189
+ # Reads an unquoted value, terminating at the next whitespace and
190
+ # applying `\\X` -> `X` escape handling. A trailing backslash at
191
+ # EOF is silently dropped (matching libpq).
192
+ #
193
+ # @return [String] the value with escapes applied (may be empty).
194
+ def parse_unquoted_value
195
+ value = +""
196
+ loop do
197
+ value << (@buffer.scan(/[^\s\\]+/) || "")
198
+ return value unless @buffer.getch == "\\"
199
+
200
+ value << (@buffer.getch || "")
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module PG
6
+ module AzureWorkloadIdentity
7
+ module ConnectionInfo
8
+ # Parses and manipulates a PostgreSQL connection string in URI format
9
+ # (e.g. `postgres://user@host:5432/db?param=value`).
10
+ #
11
+ # Connection parameters may be provided either as URI components (userinfo,
12
+ # host, port) or as query-string parameters. Accessors prefer the URI
13
+ # component and fall back to the query string when absent.
14
+ #
15
+ # The non-standard `azure_workload_identity` query parameter is recognized
16
+ # and extracted on construction so the value can drive Azure Workload
17
+ # Identity authentication while keeping the rest of the connection string
18
+ # valid for libpq.
19
+ class URI
20
+ # Tests whether a connection string is in URI format.
21
+ #
22
+ # @param connection_string [String] the connection string to test.
23
+ # @return [Boolean] `true` if the string matches the RFC 2396 absolute
24
+ # URI reference grammar, `false` otherwise.
25
+ def self.matches?(connection_string)
26
+ /\A#{::URI::RFC2396_PARSER.regexp[:ABS_URI_REF]}\z/.match?(connection_string)
27
+ end
28
+
29
+ # @return [String, nil] the value of the `azure_workload_identity` query
30
+ # parameter that was present in the original connection string, or
31
+ # `nil` if the parameter was not set.
32
+ attr_reader :azure_workload_identity
33
+
34
+ # Parses the given connection string and extracts the
35
+ # `azure_workload_identity` query parameter (if any) so it is not
36
+ # forwarded to libpq.
37
+ #
38
+ # @param connection_string [String] a PostgreSQL connection URI.
39
+ def initialize(connection_string)
40
+ @uri = ::URI.parse(connection_string)
41
+ @query = @uri.query ? ::URI.decode_www_form(@uri.query).to_h : {} # : Hash[String, String]
42
+ @azure_workload_identity = @query.delete("azure_workload_identity")
43
+ end
44
+
45
+ # @return [String, nil] the username from the URI userinfo, falling back
46
+ # to the `user` query parameter.
47
+ def user
48
+ @uri.user || @query["user"]
49
+ end
50
+
51
+ # @return [String, nil] the host from the URI authority, falling back to
52
+ # the `host` query parameter when the URI host is missing or empty.
53
+ def host
54
+ return @query["host"] if @uri.host.nil? || @uri.host.empty?
55
+
56
+ @uri.host
57
+ end
58
+
59
+ # @return [Integer, String, nil] the port from the URI authority,
60
+ # falling back to the `port` query parameter.
61
+ def port
62
+ @uri.port || @query["port"]
63
+ end
64
+
65
+ # Sets the password, storing it as a `password` query parameter and
66
+ # clearing any password embedded in the URI userinfo.
67
+ #
68
+ # @param value [String] the password to set.
69
+ # @return [String] the value that was set.
70
+ def password=(value)
71
+ @uri.password = nil
72
+ @query["password"] = value
73
+ end
74
+
75
+ # Serializes the connection info back to a URI string.
76
+ #
77
+ # The query string is rebuilt from the current parameters (so the
78
+ # `azure_workload_identity` flag is omitted) and the scheme is normalized
79
+ # to always include the `"//"` authority separator, ensuring the result
80
+ # is a valid libpq connection URI.
81
+ #
82
+ # @return [String] the connection URI.
83
+ def to_s
84
+ @uri.query = ::URI.encode_www_form(@query)
85
+
86
+ @uri.to_s.sub(%r{^#{@uri.scheme}:(?!//)}, "#{@uri.scheme}://")
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pg"
4
+
5
+ require_relative "connection_info/uri"
6
+ require_relative "connection_info/key_value_string"
7
+ require_relative "connection_info/active_record_configuration_hash"
8
+
9
+ module PG
10
+ module AzureWorkloadIdentity
11
+ # Factory methods that wrap a PostgreSQL connection description (a
12
+ # connection string or an ActiveRecord configuration hash) in an object
13
+ # exposing a uniform accessor contract (`user`, `host`, `port`,
14
+ # `azure_workload_identity`, ...).
15
+ #
16
+ # Concrete return types are {URI}, {KeyValueString}, and
17
+ # {ActiveRecordConfigurationHash}.
18
+ module ConnectionInfo
19
+ # Wraps a PostgreSQL connection string, dispatching to the URI or
20
+ # key=value parser based on its format.
21
+ #
22
+ # @param connection_string [String] either a URI-style connection
23
+ # string (e.g. `"postgres://user@host/db"`) or a libpq key=value
24
+ # string (e.g. `"host=localhost user=admin"`).
25
+ # @return [URI, KeyValueString] a wrapper around the parsed
26
+ # connection parameters.
27
+ def self.from_connection_string(connection_string)
28
+ if URI.matches?(connection_string)
29
+ URI.new(connection_string)
30
+ else
31
+ KeyValueString.new(connection_string)
32
+ end
33
+ end
34
+
35
+ # Wraps an ActiveRecord database configuration hash.
36
+ #
37
+ # @param configuration_hash [Hash{Symbol => Object}] an ActiveRecord
38
+ # database configuration hash (as produced from `database.yml`).
39
+ # @return [ActiveRecordConfigurationHash] a wrapper around the
40
+ # configuration hash.
41
+ def self.from_active_record_configuration_hash(configuration_hash)
42
+ ActiveRecordConfigurationHash.new(configuration_hash)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PG
4
+ module AzureWorkloadIdentity
5
+ # Base error class for PG::AzureWorkloadIdentity-related errors.
6
+ class Error < StandardError
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PG
4
+ module AzureWorkloadIdentity
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "azure_workload_identity/auth_token_generator"
4
+ require_relative "azure_workload_identity/connection"
5
+ require_relative "azure_workload_identity/error"
6
+ require_relative "azure_workload_identity/version"
7
+
8
+ module PG
9
+ # Namespace for the pg-azure_workload_identity gem, which provides Azure
10
+ # Workload Identity authentication support for the pg gem and related tools.
11
+ module AzureWorkloadIdentity
12
+ # Returns the process-wide {AuthTokenGenerator}, lazily instantiated on
13
+ # first access via {AuthTokenGenerator.default} so that env-loading
14
+ # libraries (e.g. dotenv) have a chance to populate `ENV` before the
15
+ # generator captures its configuration.
16
+ #
17
+ # @return [AuthTokenGenerator]
18
+ def self.auth_token_generator
19
+ @auth_token_generator ||= AuthTokenGenerator.default
20
+ end
21
+
22
+ # Overrides the process-wide {AuthTokenGenerator}. Mainly useful for
23
+ # tests or for callers that want to provide a custom-configured
24
+ # generator instead of the env-driven default.
25
+ #
26
+ # @param generator [#call] any callable that returns an access token.
27
+ # @return [#call] the generator that was set.
28
+ def self.auth_token_generator=(generator)
29
+ @auth_token_generator = generator
30
+ end
31
+
32
+ PG::Connection.singleton_class.prepend Connection
33
+
34
+ if defined?(ActiveRecord)
35
+ require "active_record/connection_adapters/postgresql_adapter"
36
+
37
+ require_relative "azure_workload_identity/active_record_postgresql_adapter"
38
+ require_relative "azure_workload_identity/active_record_postgresql_database_tasks"
39
+
40
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.singleton_class.prepend ActiveRecordPostgreSQLAdapter
41
+ ActiveRecord::Tasks::PostgreSQLDatabaseTasks.prepend ActiveRecordPostgreSQLDatabaseTasks
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/pg/azure_workload_identity/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "pg-azure_workload_identity"
7
+ spec.version = PG::AzureWorkloadIdentity::VERSION
8
+ spec.authors = ["Simon Schmid"]
9
+ spec.email = ["simon@at-point.ch"]
10
+
11
+ spec.summary = "Workload identity authentication for Azure PostgreSQL flexible server/"
12
+ spec.homepage = "https://github.com/atpoint-cloud/pg-azure_workload_identity"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 3.3"
15
+ spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "#{spec.homepage}/blob/main"
18
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
19
+
20
+ # Uncomment the line below to require MFA for gem pushes.
21
+ # This helps protect your gem from supply chain attacks by ensuring
22
+ # no one can publish a new version without multi-factor authentication.
23
+ # See: https://guides.rubygems.org/mfa-requirement-opt-in/
24
+ spec.metadata["rubygems_mfa_required"] = "true"
25
+
26
+ # Specify which files should be added to the gem when it is released.
27
+ spec.files = Dir[
28
+ "lib/**/*.rb",
29
+ ".yardopts",
30
+ "CHANGELOG.md",
31
+ "LICENSE.txt",
32
+ "README.md",
33
+ "pg-azure_workload_identity.gemspec"
34
+ ]
35
+
36
+ spec.require_paths = ["lib"]
37
+
38
+ # Uncomment to register a new dependency of your gem
39
+ spec.add_dependency "pg", "~> 1.5"
40
+
41
+ # For more information and examples about making a new gem, check out our
42
+ # guide at: https://guides.rubygems.org/make-your-own-gem/
43
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pg-azure_workload_identity
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Simon Schmid
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: pg
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.5'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.5'
26
+ email:
27
+ - simon@at-point.ch
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - ".yardopts"
33
+ - CHANGELOG.md
34
+ - LICENSE.txt
35
+ - README.md
36
+ - lib/pg/azure_workload_identity.rb
37
+ - lib/pg/azure_workload_identity/active_record_postgresql_adapter.rb
38
+ - lib/pg/azure_workload_identity/active_record_postgresql_database_tasks.rb
39
+ - lib/pg/azure_workload_identity/auth_token.rb
40
+ - lib/pg/azure_workload_identity/auth_token_generator.rb
41
+ - lib/pg/azure_workload_identity/auth_token_injector.rb
42
+ - lib/pg/azure_workload_identity/connection.rb
43
+ - lib/pg/azure_workload_identity/connection_info.rb
44
+ - lib/pg/azure_workload_identity/connection_info/active_record_configuration_hash.rb
45
+ - lib/pg/azure_workload_identity/connection_info/key_value_string.rb
46
+ - lib/pg/azure_workload_identity/connection_info/uri.rb
47
+ - lib/pg/azure_workload_identity/error.rb
48
+ - lib/pg/azure_workload_identity/version.rb
49
+ - pg-azure_workload_identity.gemspec
50
+ homepage: https://github.com/atpoint-cloud/pg-azure_workload_identity
51
+ licenses:
52
+ - MIT
53
+ metadata:
54
+ bug_tracker_uri: https://github.com/atpoint-cloud/pg-azure_workload_identity/issues
55
+ homepage_uri: https://github.com/atpoint-cloud/pg-azure_workload_identity
56
+ source_code_uri: https://github.com/atpoint-cloud/pg-azure_workload_identity/blob/main
57
+ changelog_uri: https://github.com/atpoint-cloud/pg-azure_workload_identity/blob/main/CHANGELOG.md
58
+ rubygems_mfa_required: 'true'
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '3.3'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubygems_version: 4.0.10
74
+ specification_version: 4
75
+ summary: Workload identity authentication for Azure PostgreSQL flexible server/
76
+ test_files: []