Skip to content

feat(mysql/user): support non-default authentication plugins for IAM auth#363

Open
ronlevy1211 wants to merge 5 commits into
crossplane-contrib:masterfrom
ronlevy1211:feat/mysql-iam-auth
Open

feat(mysql/user): support non-default authentication plugins for IAM auth#363
ronlevy1211 wants to merge 5 commits into
crossplane-contrib:masterfrom
ronlevy1211:feat/mysql-iam-auth

Conversation

@ronlevy1211
Copy link
Copy Markdown

@ronlevy1211 ronlevy1211 commented Apr 30, 2026

Description of your changes

Adds an optional AuthenticationPlugin field on the MySQL User resource. When set, the user is created with CREATE USER ... IDENTIFIED WITH <plugin> [AS '<authString>'] instead of IDENTIFIED BY <password>. No password is generated, the connection secret omits the password key, and ALTER USER ... IDENTIFIED BY is skipped on Update so the provider does not downgrade plugin-auth users to native password auth.

Primary motivation is AWS RDS IAM authentication, where the DB user has no static password and clients fetch a short-lived auth token from AWS at connect time:

spec:
  forProvider:
    authenticationPlugin:
      name: AWSAuthenticationPlugin
      authString: RDS

The same field also works for other auth plugins (auth_socket, caching_sha2_password, etc.).

Backward compatibility

When AuthenticationPlugin is unset, the existing password-based flow is preserved. Existing users / manifests are unaffected.

Notable changes

  • New AuthenticationPlugin{Name, AuthString} struct on UserParameters (cluster + namespaced variants)
  • Create: branches to IDENTIFIED WITH when the plugin is set; password generation skipped
  • Observe: skips password drift check when the plugin is set (no password to drift)
  • UpdatePassword: no-op when the plugin is set (avoids downgrading the user back to password auth)
  • Plugin name is quoted as a SQL identifier; auth string is quoted as a SQL value
  • Examples under examples/cluster/mysql/user_iam_auth.yaml and examples/namespaced/mysql/user_iam_auth.yaml
  • Regenerated CRDs and deepcopy

Fixes #106

I have:

  • Read and followed Crossplane's contribution process.
  • Run make reviewable to ensure this PR is ready for review.

How has this code been tested

  • Unit tests added for both cluster and namespaced reconcilers covering: create with plugin + authString, create with plugin only (no AS clause), update no-op for plugin-auth users
  • End-to-end validated against a real AWS Aurora MySQL — full results in a comment below

…auth

Add an optional AuthenticationPlugin field on the MySQL User resource. When
set, the user is created with CREATE USER ... IDENTIFIED WITH <plugin>
[AS '<authString>'] instead of IDENTIFIED BY <password>. No password is
generated, the connection secret omits the password key, and ALTER USER
... IDENTIFIED BY is skipped on Update so the provider does not downgrade
plugin-auth users to native password auth.

Primary motivation is AWS RDS IAM authentication, where the DB user has
no static password and clients fetch a short-lived auth token from AWS
at connect time:

  spec:
    forProvider:
      authenticationPlugin:
        name: AWSAuthenticationPlugin
        authString: RDS

The same field also works for other auth plugins (auth_socket,
caching_sha2_password, etc.).

Backward compatible: when AuthenticationPlugin is unset the existing
password-based flow is preserved.

Applied to both the cluster and namespaced API variants. Plugin name is
quoted as a SQL identifier; auth string is quoted as a SQL value.

Signed-off-by: Ron Levy <rlevy@fireblocks.com>
@ronlevy1211
Copy link
Copy Markdown
Author

ronlevy1211 commented Apr 30, 2026

E2E test results

Validated end-to-end against an AWS Aurora MySQL (Aurora 3 / MySQL 8.0 compatible) on a non-prod cluster. Built the provider from this branch as a custom OCI image and deployed it on Crossplane pointing at the cluster.

Setup

apiVersion: mysql.sql.crossplane.io/v1alpha1
kind: User
metadata:
  name: iam-auth-test
  annotations:
    crossplane.io/external-name: iam-auth-test
spec:
  forProvider:
    authenticationPlugin:
      name: AWSAuthenticationPlugin
      authString: RDS
  providerConfigRef:
    name: mysql-shell-services
  writeConnectionSecretToRef:
    name: iam-auth-test-conn
    namespace: default

Plus a Database and Grant referring to the same user.

Results

Test Result
User CR transitions to Ready=True, Synced=True
mysql.user row shows plugin=AWSAuthenticationPlugin (proves the SQL we emit is accepted by AWS RDS)
Connection secret has no password key
Grant and Database reconcile successfully alongside the IAM-auth user
No drift over 3 consecutive reconcile cycles (90s) — metadata.resourceVersion unchanged, no ALTER USER in provider logs (validates the Observe and UpdatePassword skip-when-plugin paths)
Deleting the User CR runs DROP USERSELECT … FROM mysql.user WHERE user = 'iam-auth-test' returns no rows

Direct DB query proving the plugin was applied:

mysql> SELECT user, host, plugin FROM mysql.user WHERE user = 'iam-auth-test';
+---------------+------+--------------------------+
| user          | host | plugin                   |
+---------------+------+--------------------------+
| iam-auth-test | %    | AWSAuthenticationPlugin  |
+---------------+------+--------------------------+

@ronlevy1211
Copy link
Copy Markdown
Author

ronlevy1211 commented May 3, 2026

Hi @fernandezcuesta @chlunde @JevgeniF — friendly ping for a first pass when you have a moment. The PR adds AuthenticationPlugin to the MySQL User resource so the user can be created with non-default plugins (specifically the AWS RDS AWSAuthenticationPlugin/RDS for IAM auth). E2E results against Aurora MySQL 8.0 are documented in the comment above.

// for this user. If no reference is given, a password will be auto-generated.
// for this user. If no reference is given and AuthenticationPlugin is
// unset, a password will be auto-generated.
// Ignored when AuthenticationPlugin is set.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can those be explicitly set mutually exclusive?
I mean either kubebuilder annotation or lateinitializer ignoredFields.
If you're able to restrict that from the API/CRD, the logic can be simplified a bit

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — pushed af1d37f. Added a +kubebuilder:validation:XValidation rule on UserParameters that rejects setting both passwordSecretRef and authenticationPlugin (matches the pattern used by MSSQL User). The runtime branches in Observe/Create/UpdatePassword stay because they encode the semantic that plugin-auth users have no password, not a defensive guard. Doc comments on both fields tightened to drop the "ignored when..." wording.

…ually exclusive

Add a CRD-level XValidation rule rejecting any User where both
passwordSecretRef and authenticationPlugin are set, addressing review
feedback. The runtime branches in Observe/Create/UpdatePassword stay
because they encode the semantic that plugin-auth users have no
password (they are not defensive guards against both fields being
set).

Tighten doc comments on both fields to reflect that the two cannot
coexist.

Signed-off-by: Ron Levy <rlevy@fireblocks.com>
@ronlevy1211 ronlevy1211 force-pushed the feat/mysql-iam-auth branch from 3763ae5 to af1d37f Compare May 4, 2026 08:47
@ronlevy1211
Copy link
Copy Markdown
Author

Hey @fernandezcuesta — no rush, just a gentle nudge in case this slipped past. Pushed af1d37f last Monday with the +kubebuilder:validation:XValidation rule per your suggestion — cleaned up the implementation nicely. CI is green and the change is small. Whenever you have a few minutes for a re-look, much appreciated 🙏

Also pinging @chlunde @JevgeniF for any second opinion if Fernando is heads-down elsewhere.

"max_updates, " +
"max_connections, " +
"max_user_connections " +
"FROM mysql.user WHERE User = ? AND Host = ?"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you get plugin here for drift detection and add support for changing method in Update? crossplane managed resources should be able to update any field as long as the upstream system supports it, so we don't silently ignore some types of changes

Copy link
Copy Markdown
Author

@ronlevy1211 ronlevy1211 May 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done - pushed 12cd322. Three pieces:

  1. Observe now SELECTs plugin, authentication_string alongside the resource-options columns. Hydrates observed.AuthenticationPlugin only when the observed plugin is non-default-password (caching_sha2_password / mysql_native_password / sha256_password are treated as "user has a password" and left as nil - they're the implicit default and would otherwise force drift on every reconcile of a password user).

  2. upToDate compares AuthenticationPlugin (name + authString, nil-aware) ahead of the resource-options checks so plugin drift is caught even when ResourceOptions is nil.

  3. Update detects plugin drift via Status.AtProvider.AuthenticationPlugin (cached by Observe) and handles three transitions:

    • password → plugin: emits ALTER USER ... IDENTIFIED WITH <plugin> [AS '<authString>'] (new executeAlterUserWithPluginQuery helper, mirrors the Create-time quoting - identifier-quote plugin name, value-quote authString).
    • plugin → password: falls through to the existing UpdatePassword path. ALTER USER ... IDENTIFIED BY '<pw>' both sets the password and restores the default password plugin in one statement.
    • same plugin, different authString: same ALTER USER ... IDENTIFIED WITH query with the new auth string.

The API-level XValidation rule already enforces mutual exclusivity of passwordSecretRef and authenticationPlugin, so the spec always represents one mode and the transition is unambiguous.

Tests cover the three transitions above plus an updated AuthenticationPluginSkipsAlterPassword that now sets observed == desired and asserts neither IDENTIFIED BY nor IDENTIFIED WITH runs.

Copy link
Copy Markdown
Collaborator

@chlunde chlunde left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add drift detection, or did you already discuss this?

ronlevy1211 and others added 2 commits May 17, 2026 09:25
Address @chlunde's review on crossplane-contrib#363: Observe didn't read the user's
actual `plugin` from mysql.user, and Update silently ignored
authenticationPlugin changes. Crossplane managed resources should
reconcile every field the upstream system supports.

Changes:

  Observe — extend the SELECT to also fetch `plugin` and
  `authentication_string`. Hydrate observed.AuthenticationPlugin
  only when the observed plugin is a non-default-password plugin
  (caching_sha2_password / mysql_native_password / sha256_password
  represent "use a password" rather than an opt-in plugin choice
  and are left as nil in the observed state).

  upToDate — compare AuthenticationPlugin (name + authString). nil
  vs non-nil is a drift; same nil-ness with matching name+authString
  is up-to-date.

  Update — when authenticationPlugin drift is detected and the
  desired spec sets a plugin, emit
    ALTER USER <user>@<host> IDENTIFIED WITH <plugin> [AS '<authString>']
  via a new executeAlterUserWithPluginQuery helper that mirrors the
  Create-time query's quoting rules (identifier-quote the plugin
  name, value-quote the authString). When the desired spec drops the
  plugin (plugin → password transition), the existing UpdatePassword
  path is reused — ALTER USER ... IDENTIFIED BY '<pw>' both sets the
  password and restores the default password plugin in one statement.

  Status — UserObservation gains an AuthenticationPlugin field, set
  by Observe and consumed by Update for drift detection across
  reconciliation cycles.

Tests cover three new transitions:
  - password → plugin (observed nil, desired set)
  - plugin → different authString (same name, different AS clause)
  - existing AuthenticationPluginSkipsAlterPassword updated to set
    observed == desired and now asserts no IDENTIFIED WITH OR BY runs

Signed-off-by: ronlevy <rlevy@fireblocks.com>
@ronlevy1211 ronlevy1211 requested a review from chlunde May 17, 2026 06:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support RDS IAM authentication

3 participants