From 8807f5fd1b1b97198863dd6c4310107dfb575265 Mon Sep 17 00:00:00 2001 From: Joe Armstrong Date: Wed, 5 Aug 2026 08:40:32 -0400 Subject: [PATCH 1/2] feat: add OIDC custom claims configuration Adds spec.config.auth.oidc.customClaims so an identity provider that already emits its own claim names or group naming scheme can be used without reshaping the IdP to Bindplane's defaults. Two groups of settings, both entirely optional: - Claim name overrides (groups, groupIds, roles, organizationAdmin, projects, defaultRole) rename the claims Bindplane reads on the ID token. - Group/role string overrides (orgAdminGroupName, projectsGroupPrefix, adminGroupName, userGroupName, viewerGroupName) change the strings Bindplane matches inside the groups and roles claims. Each field maps to a BINDPLANE_OIDC_CUSTOM_CLAIMS_* env var and is only emitted when set, so unset fields fall through to the server defaults. Requires Bindplane server v1.100.2 or later. --- api/v1alpha1/bindplane_types.go | 69 ++++++++++++++ api/v1alpha1/zz_generated.deepcopy.go | 20 ++++ .../bases/k8s.bindplane.com_bindplanes.yaml | 25 +++++ docs/configuration/api.md | 28 ++++++ docs/configuration/configuration.md | 84 +++++++++++++++++ docs/configuration/security.md | 4 +- internal/controller/bindplane_controller.go | 15 +++ internal/controller/config_env.go | 33 +++++++ internal/controller/config_env_test.go | 91 +++++++++++++++++++ 9 files changed, 368 insertions(+), 1 deletion(-) diff --git a/api/v1alpha1/bindplane_types.go b/api/v1alpha1/bindplane_types.go index a3555e4..02ba309 100644 --- a/api/v1alpha1/bindplane_types.go +++ b/api/v1alpha1/bindplane_types.go @@ -1382,6 +1382,75 @@ type OIDCConfig struct { // When true, users cannot be invited via email and must log in via OIDC directly. // +optional DisableInvitations bool `json:"disableInvitations,omitempty"` + + // CustomClaims customizes the ID token claim names Bindplane reads and the + // group / role strings it matches within those claims. Omit a field to keep + // the Bindplane default. + // +optional + CustomClaims *OIDCCustomClaimsConfig `json:"customClaims,omitempty"` +} + +// OIDCCustomClaimsConfig customizes OIDC claim names and the group / role strings +// Bindplane matches, so an existing identity provider does not have to be reshaped +// to Bindplane's default claim names. +// +// The first group of fields renames the claims Bindplane reads on the ID token. +// The second group changes the strings Bindplane matches inside the groups and +// roles claims (after claim names are resolved). +type OIDCCustomClaimsConfig struct { + // Groups is the claim name for user groups. Defaults to "groups". + // +optional + Groups string `json:"groups,omitempty"` + + // GroupIDs is the claim name for group IDs, used as a fallback when the groups + // claim is missing, empty, or parses to an empty list. Defaults to "group_ids". + // +optional + GroupIDs string `json:"groupIds,omitempty"` + + // Roles is the claim name for user roles. Defaults to "roles". + // +optional + Roles string `json:"roles,omitempty"` + + // OrganizationAdmin is the claim name indicating organization admin status. + // Defaults to "bindplane_org_admin". + // +optional + OrganizationAdmin string `json:"organizationAdmin,omitempty"` + + // Projects is the claim name for Bindplane project assignments. + // Defaults to "bindplane_projects". + // +optional + Projects string `json:"projects,omitempty"` + + // DefaultRole is the claim name for default role assignment. + // Defaults to "bindplane_default_role". + // +optional + DefaultRole string `json:"defaultRole,omitempty"` + + // OrgAdminGroupName is the group / role string that indicates organization admin + // status. Defaults to "bindplane-org-admin". + // +optional + OrgAdminGroupName string `json:"orgAdminGroupName,omitempty"` + + // ProjectsGroupPrefix is the group / role prefix that encodes project assignments, + // e.g. "bindplane-projects-" or "bindplane-projects-:". + // Defaults to "bindplane-projects-". + // +optional + ProjectsGroupPrefix string `json:"projectsGroupPrefix,omitempty"` + + // AdminGroupName is the group / role string that indicates the admin role. + // Defaults to "bindplane-admin". + // +optional + AdminGroupName string `json:"adminGroupName,omitempty"` + + // UserGroupName is the group / role string that indicates the user role. + // Defaults to "bindplane-user". + // +optional + UserGroupName string `json:"userGroupName,omitempty"` + + // ViewerGroupName is the group / role string that indicates the viewer role. + // Defaults to "bindplane-viewer". + // +optional + ViewerGroupName string `json:"viewerGroupName,omitempty"` } // NetworkConfig defines network configuration diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 872e8b6..56821b0 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -984,6 +984,11 @@ func (in *OIDCConfig) DeepCopyInto(out *OIDCConfig) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.CustomClaims != nil { + in, out := &in.CustomClaims, &out.CustomClaims + *out = new(OIDCCustomClaimsConfig) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCConfig. @@ -996,6 +1001,21 @@ func (in *OIDCConfig) DeepCopy() *OIDCConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCCustomClaimsConfig) DeepCopyInto(out *OIDCCustomClaimsConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCCustomClaimsConfig. +func (in *OIDCCustomClaimsConfig) DeepCopy() *OIDCCustomClaimsConfig { + if in == nil { + return nil + } + out := new(OIDCCustomClaimsConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OpAMPComponentSpec) DeepCopyInto(out *OpAMPComponentSpec) { *out = *in diff --git a/config/crd/bases/k8s.bindplane.com_bindplanes.yaml b/config/crd/bases/k8s.bindplane.com_bindplanes.yaml index 4e636c2..9899229 100644 --- a/config/crd/bases/k8s.bindplane.com_bindplanes.yaml +++ b/config/crd/bases/k8s.bindplane.com_bindplanes.yaml @@ -14834,6 +14834,31 @@ spec: - key type: object x-kubernetes-map-type: atomic + customClaims: + properties: + adminGroupName: + type: string + defaultRole: + type: string + groupIds: + type: string + groups: + type: string + orgAdminGroupName: + type: string + organizationAdmin: + type: string + projects: + type: string + projectsGroupPrefix: + type: string + roles: + type: string + userGroupName: + type: string + viewerGroupName: + type: string + type: object disableInvitations: type: boolean issuer: diff --git a/docs/configuration/api.md b/docs/configuration/api.md index de561b9..1fd7e63 100644 --- a/docs/configuration/api.md +++ b/docs/configuration/api.md @@ -497,6 +497,34 @@ _Appears in:_ | `issuer` _string_ | Issuer is the URL of the OIDC provider | | Optional: \{\}
| | `scopes` _string array_ | Scopes is the list of OAuth2 scopes to request | | Optional: \{\}
| | `disableInvitations` _boolean_ | DisableInvitations disables the invitation flow for OIDC-authenticated users.
When true, users cannot be invited via email and must log in via OIDC directly. | | Optional: \{\}
| +| `customClaims` _[OIDCCustomClaimsConfig](#oidccustomclaimsconfig)_ | CustomClaims customizes the ID token claim names Bindplane reads and the
group / role strings it matches within those claims. Omit a field to keep
the Bindplane default. | | Optional: \{\}
| + +#### OIDCCustomClaimsConfig + +OIDCCustomClaimsConfig customizes OIDC claim names and the group / role strings +Bindplane matches, so an existing identity provider does not have to be reshaped +to Bindplane's default claim names. + +The first group of fields renames the claims Bindplane reads on the ID token. +The second group changes the strings Bindplane matches inside the groups and +roles claims (after claim names are resolved). + +_Appears in:_ +- [OIDCConfig](#oidcconfig) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `groups` _string_ | Groups is the claim name for user groups. Defaults to "groups". | | Optional: \{\}
| +| `groupIds` _string_ | GroupIDs is the claim name for group IDs, used as a fallback when the groups
claim is missing, empty, or parses to an empty list. Defaults to "group_ids". | | Optional: \{\}
| +| `roles` _string_ | Roles is the claim name for user roles. Defaults to "roles". | | Optional: \{\}
| +| `organizationAdmin` _string_ | OrganizationAdmin is the claim name indicating organization admin status.
Defaults to "bindplane_org_admin". | | Optional: \{\}
| +| `projects` _string_ | Projects is the claim name for Bindplane project assignments.
Defaults to "bindplane_projects". | | Optional: \{\}
| +| `defaultRole` _string_ | DefaultRole is the claim name for default role assignment.
Defaults to "bindplane_default_role". | | Optional: \{\}
| +| `orgAdminGroupName` _string_ | OrgAdminGroupName is the group / role string that indicates organization admin
status. Defaults to "bindplane-org-admin". | | Optional: \{\}
| +| `projectsGroupPrefix` _string_ | ProjectsGroupPrefix is the group / role prefix that encodes project assignments,
e.g. "bindplane-projects-" or "bindplane-projects-:".
Defaults to "bindplane-projects-". | | Optional: \{\}
| +| `adminGroupName` _string_ | AdminGroupName is the group / role string that indicates the admin role.
Defaults to "bindplane-admin". | | Optional: \{\}
| +| `userGroupName` _string_ | UserGroupName is the group / role string that indicates the user role.
Defaults to "bindplane-user". | | Optional: \{\}
| +| `viewerGroupName` _string_ | ViewerGroupName is the group / role string that indicates the viewer role.
Defaults to "bindplane-viewer". | | Optional: \{\}
| #### OpAMPComponentSpec diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index ef15938..0404597 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -11,6 +11,7 @@ Configuration is provided via the `spec.config` field of the `Bindplane` custom - [System auth](#system-auth) - [LDAP and Active Directory](#ldap-and-active-directory) - [OIDC](#oidc) + - [Custom claims](#custom-claims) - [Network](#network) - [Store](#store) - [PostgreSQL](#postgresql) @@ -278,6 +279,89 @@ spec: key: client-secret ``` +#### Custom claims + +`spec.config.auth.oidc.customClaims` adapts Bindplane to an identity provider that already emits its own claim names or group naming scheme, so the IdP does not have to be reshaped to Bindplane's defaults. + +There are two groups of settings: + +- **Claim name overrides** (`groups`, `groupIds`, `roles`, `organizationAdmin`, `projects`, `defaultRole`) rename the claims Bindplane reads on the ID token. +- **Group / role string overrides** (`orgAdminGroupName`, `projectsGroupPrefix`, `adminGroupName`, `userGroupName`, `viewerGroupName`) change the strings Bindplane matches *inside* the groups and roles claims, after the claim names above are resolved. + +Every field is optional. Omit a field to keep the Bindplane default; the operator only sets an environment variable for fields you populate. + +`groupIds` is a fallback: Bindplane uses it when the groups claim is missing, empty, or parses to an empty list. + +`projectsGroupPrefix` matches group entries that encode project access, either `` or `:` (for example `bindplane-projects-admin:01PROJECTID`). + +| CRD Field | Environment Variable | Default | Required | +|---|---|---|---| +| `spec.config.auth.oidc.customClaims.groups` | `BINDPLANE_OIDC_CUSTOM_CLAIMS_GROUPS` | `groups` | No | +| `spec.config.auth.oidc.customClaims.groupIds` | `BINDPLANE_OIDC_CUSTOM_CLAIMS_GROUP_IDS` | `group_ids` | No | +| `spec.config.auth.oidc.customClaims.roles` | `BINDPLANE_OIDC_CUSTOM_CLAIMS_ROLES` | `roles` | No | +| `spec.config.auth.oidc.customClaims.organizationAdmin` | `BINDPLANE_OIDC_CUSTOM_CLAIMS_ORGANIZATION_ADMIN` | `bindplane_org_admin` | No | +| `spec.config.auth.oidc.customClaims.projects` | `BINDPLANE_OIDC_CUSTOM_CLAIMS_PROJECTS` | `bindplane_projects` | No | +| `spec.config.auth.oidc.customClaims.defaultRole` | `BINDPLANE_OIDC_CUSTOM_CLAIMS_DEFAULT_ROLE` | `bindplane_default_role` | No | +| `spec.config.auth.oidc.customClaims.orgAdminGroupName` | `BINDPLANE_OIDC_CUSTOM_CLAIMS_ORG_ADMIN_GROUP_NAME` | `bindplane-org-admin` | No | +| `spec.config.auth.oidc.customClaims.projectsGroupPrefix` | `BINDPLANE_OIDC_CUSTOM_CLAIMS_PROJECTS_GROUP_PREFIX` | `bindplane-projects-` | No | +| `spec.config.auth.oidc.customClaims.adminGroupName` | `BINDPLANE_OIDC_CUSTOM_CLAIMS_ADMIN_GROUP_NAME` | `bindplane-admin` | No | +| `spec.config.auth.oidc.customClaims.userGroupName` | `BINDPLANE_OIDC_CUSTOM_CLAIMS_USER_GROUP_NAME` | `bindplane-user` | No | +| `spec.config.auth.oidc.customClaims.viewerGroupName` | `BINDPLANE_OIDC_CUSTOM_CLAIMS_VIEWER_GROUP_NAME` | `bindplane-viewer` | No | + +Custom claims require Bindplane server v1.100.2 or later. On earlier versions the environment variables are ignored and the defaults apply. + +Example (renaming claim names only): + +```yaml +spec: + config: + auth: + type: oidc + oidc: + issuer: https://accounts.example.com + scopes: + - openid + - profile + - email + clientIDSecretRef: + name: oidc-secrets + key: client-id + clientSecretSecretRef: + name: oidc-secrets + key: client-secret + customClaims: + groups: "user_groups" + roles: "user_roles" + organizationAdmin: "acme_org_admin" +``` + +Example (renaming the group and role strings Bindplane matches): + +```yaml +spec: + config: + auth: + type: oidc + oidc: + issuer: https://accounts.example.com + scopes: + - openid + - profile + - email + clientIDSecretRef: + name: oidc-secrets + key: client-id + clientSecretSecretRef: + name: oidc-secrets + key: client-secret + customClaims: + orgAdminGroupName: "acme-org-admin" + projectsGroupPrefix: "acme-projects-" + adminGroupName: "acme-admin" + userGroupName: "acme-user" + viewerGroupName: "acme-viewer" +``` + ## Network TLS is generally not configured on the Bindplane server when you use Ingress or Gateway API to terminate TLS. In that case, only `remoteURL` (and optionally `webURL`) need to reflect the external URL; the server continues to listen over HTTP inside the cluster. diff --git a/docs/configuration/security.md b/docs/configuration/security.md index b1c76bc..b56cc14 100644 --- a/docs/configuration/security.md +++ b/docs/configuration/security.md @@ -14,7 +14,7 @@ The following are configured via the Bindplane custom resource and documented in | Area | What you configure | How the operator uses it | |------|--------------------|---------------------------| | **License** | `spec.config.license` or `licenseSecretRef` | Injects `BINDPLANE_LICENSE` from value or Secret into Node, Jobs, NATS, and the Jobs Migrate Job. | -| **Authentication** | System auth (`username`/`password` or Secret refs), session secret (`sessionSecret` or `sessionSecretSecretRef`), API key (`apiKey` or `apiKeySecretRef`), LDAP bind user/password and [LDAP TLS](configuration.md#ldap-and-active-directory), OIDC client ID/secret or Secret refs, agent auth (`spec.config.agents.auth`) | Sets auth-related env vars; mounts LDAP TLS Secret when `spec.config.auth.ldap.tls` is set. | +| **Authentication** | System auth (`username`/`password` or Secret refs), session secret (`sessionSecret` or `sessionSecretSecretRef`), API key (`apiKey` or `apiKeySecretRef`), LDAP bind user/password and [LDAP TLS](configuration.md#ldap-and-active-directory), OIDC client ID/secret or Secret refs, [OIDC custom claims](configuration.md#custom-claims), agent auth (`spec.config.agents.auth`) | Sets auth-related env vars; mounts LDAP TLS Secret when `spec.config.auth.ldap.tls` is set. | | **Network TLS** | [Network TLS](configuration.md#network): `spec.config.network.tls` (secretName, certKey, keyKey, caKey, minVersion, skipVerify) | Mounts the Secret at a fixed path and sets `BINDPLANE_TLS_*` env vars to the mounted file paths. Used when you want server-side or mutual TLS on the Bindplane server (often omitted when using [Ingress or Gateway API](configuration.md#network) to terminate TLS). | | **PostgreSQL** | Postgres username/password (or Secret refs) and [PostgreSQL TLS](configuration.md#postgresql) (`spec.config.store.postgres.tls`, sslmode) | Injects credentials; mounts Postgres TLS Secret when TLS is configured and sets `BINDPLANE_POSTGRES_SSL_*` env vars. | | **Metrics (Prometheus)** | Optional basic auth for the HTTP endpoint where Bindplane **exposes** its own metrics via `spec.config.metrics.prometheus.username` and `password` or `passwordSecretRef` | Sets `BINDPLANE_METRICS_PROMETHEUS_USERNAME` / `BINDPLANE_METRICS_PROMETHEUS_PASSWORD` when configured. Distinct from Bindplane TSDB remote write auth (below). | @@ -226,6 +226,8 @@ This rule is enforced at admission time by the Kubernetes API server (and additi | `spec.config.errors.frontendDSN` | `frontendDSNSecretRef` | | `spec.config.status.keys` | `keysSecretRef` | +[OIDC custom claims](configuration.md#custom-claims) (`spec.config.auth.oidc.customClaims`) deliberately have no SecretRef variant: they are claim and group *names* that identify where Bindplane looks in the ID token, not credentials, and are set as plain env var values. + ## Summary | Secret / TLS | User-configurable? | Env vars (where applicable) | Where configured | Documentation | diff --git a/internal/controller/bindplane_controller.go b/internal/controller/bindplane_controller.go index 7a3d387..cc486dc 100644 --- a/internal/controller/bindplane_controller.go +++ b/internal/controller/bindplane_controller.go @@ -269,6 +269,21 @@ const ( // OIDC additional configuration bindplaneOIDCDisableInvitationsEnvVar = "BINDPLANE_OIDC_DISABLE_INVITATIONS" + // OIDC custom claims: claim name overrides + bindplaneOIDCCustomClaimsGroupsEnvVar = "BINDPLANE_OIDC_CUSTOM_CLAIMS_GROUPS" + bindplaneOIDCCustomClaimsGroupIDsEnvVar = "BINDPLANE_OIDC_CUSTOM_CLAIMS_GROUP_IDS" + bindplaneOIDCCustomClaimsRolesEnvVar = "BINDPLANE_OIDC_CUSTOM_CLAIMS_ROLES" + bindplaneOIDCCustomClaimsOrganizationAdminEnvVar = "BINDPLANE_OIDC_CUSTOM_CLAIMS_ORGANIZATION_ADMIN" + bindplaneOIDCCustomClaimsProjectsEnvVar = "BINDPLANE_OIDC_CUSTOM_CLAIMS_PROJECTS" + bindplaneOIDCCustomClaimsDefaultRoleEnvVar = "BINDPLANE_OIDC_CUSTOM_CLAIMS_DEFAULT_ROLE" + + // OIDC custom claims: group / role pattern overrides + bindplaneOIDCCustomClaimsOrgAdminGroupNameEnvVar = "BINDPLANE_OIDC_CUSTOM_CLAIMS_ORG_ADMIN_GROUP_NAME" + bindplaneOIDCCustomClaimsProjectsGroupPrefixEnvVar = "BINDPLANE_OIDC_CUSTOM_CLAIMS_PROJECTS_GROUP_PREFIX" + bindplaneOIDCCustomClaimsAdminGroupNameEnvVar = "BINDPLANE_OIDC_CUSTOM_CLAIMS_ADMIN_GROUP_NAME" + bindplaneOIDCCustomClaimsUserGroupNameEnvVar = "BINDPLANE_OIDC_CUSTOM_CLAIMS_USER_GROUP_NAME" + bindplaneOIDCCustomClaimsViewerGroupNameEnvVar = "BINDPLANE_OIDC_CUSTOM_CLAIMS_VIEWER_GROUP_NAME" + // NATS TLS additional configuration bindplaneNatsTLSSkipVerifyEnvVar = "BINDPLANE_NATS_TLS_SKIP_VERIFY" ) diff --git a/internal/controller/config_env.go b/internal/controller/config_env.go index 178681f..019bd1e 100644 --- a/internal/controller/config_env.go +++ b/internal/controller/config_env.go @@ -109,6 +109,39 @@ func getOIDCEnvVars(oidc *bindplanev1alpha1.OIDCConfig) []corev1.EnvVar { if oidc.DisableInvitations { envVars = append(envVars, corev1.EnvVar{Name: bindplaneOIDCDisableInvitationsEnvVar, Value: "true"}) } + envVars = append(envVars, getOIDCCustomClaimsEnvVars(oidc.CustomClaims)...) + return envVars +} + +// getOIDCCustomClaimsEnvVars returns OIDC custom claims environment variables. +// Each field is omitted when empty so the Bindplane server default applies. +// Returns nil when customClaims is nil. +func getOIDCCustomClaimsEnvVars(customClaims *bindplanev1alpha1.OIDCCustomClaimsConfig) []corev1.EnvVar { + if customClaims == nil { + return nil + } + pairs := []struct { + name string + value string + }{ + {bindplaneOIDCCustomClaimsGroupsEnvVar, customClaims.Groups}, + {bindplaneOIDCCustomClaimsGroupIDsEnvVar, customClaims.GroupIDs}, + {bindplaneOIDCCustomClaimsRolesEnvVar, customClaims.Roles}, + {bindplaneOIDCCustomClaimsOrganizationAdminEnvVar, customClaims.OrganizationAdmin}, + {bindplaneOIDCCustomClaimsProjectsEnvVar, customClaims.Projects}, + {bindplaneOIDCCustomClaimsDefaultRoleEnvVar, customClaims.DefaultRole}, + {bindplaneOIDCCustomClaimsOrgAdminGroupNameEnvVar, customClaims.OrgAdminGroupName}, + {bindplaneOIDCCustomClaimsProjectsGroupPrefixEnvVar, customClaims.ProjectsGroupPrefix}, + {bindplaneOIDCCustomClaimsAdminGroupNameEnvVar, customClaims.AdminGroupName}, + {bindplaneOIDCCustomClaimsUserGroupNameEnvVar, customClaims.UserGroupName}, + {bindplaneOIDCCustomClaimsViewerGroupNameEnvVar, customClaims.ViewerGroupName}, + } + var envVars []corev1.EnvVar + for _, p := range pairs { + if p.value != "" { + envVars = append(envVars, corev1.EnvVar{Name: p.name, Value: p.value}) + } + } return envVars } diff --git a/internal/controller/config_env_test.go b/internal/controller/config_env_test.go index 35b3be7..c6160e5 100644 --- a/internal/controller/config_env_test.go +++ b/internal/controller/config_env_test.go @@ -60,6 +60,97 @@ func TestGetOIDCEnvVars_DisableInvitationsFalse(t *testing.T) { requireNoEnvVar(t, envVars, bindplaneOIDCDisableInvitationsEnvVar) } +// ------- getOIDCCustomClaimsEnvVars ------- + +func TestGetOIDCCustomClaimsEnvVars_NilReturnsNil(t *testing.T) { + if envVars := getOIDCCustomClaimsEnvVars(nil); envVars != nil { + t.Errorf("expected nil env vars for nil custom claims, got %v", envVars) + } +} + +func TestGetOIDCCustomClaimsEnvVars_EmptyEmitsNothing(t *testing.T) { + envVars := getOIDCCustomClaimsEnvVars(&bindplanev1alpha1.OIDCCustomClaimsConfig{}) + if len(envVars) != 0 { + t.Errorf("expected no env vars for empty custom claims, got %v", envVars) + } +} + +func TestGetOIDCCustomClaimsEnvVars_AllFields(t *testing.T) { + customClaims := &bindplanev1alpha1.OIDCCustomClaimsConfig{ + Groups: "my_groups", + GroupIDs: "my_group_ids", + Roles: "my_roles", + OrganizationAdmin: "my_org_admin", + Projects: "my_projects", + DefaultRole: "my_default_role", + OrgAdminGroupName: "acme-org-admin", + ProjectsGroupPrefix: "acme-projects-", + AdminGroupName: "acme-admin", + UserGroupName: "acme-user", + ViewerGroupName: "acme-viewer", + } + envVars := getOIDCCustomClaimsEnvVars(customClaims) + + requireEnvVar(t, envVars, bindplaneOIDCCustomClaimsGroupsEnvVar, "my_groups") + requireEnvVar(t, envVars, bindplaneOIDCCustomClaimsGroupIDsEnvVar, "my_group_ids") + requireEnvVar(t, envVars, bindplaneOIDCCustomClaimsRolesEnvVar, "my_roles") + requireEnvVar(t, envVars, bindplaneOIDCCustomClaimsOrganizationAdminEnvVar, "my_org_admin") + requireEnvVar(t, envVars, bindplaneOIDCCustomClaimsProjectsEnvVar, "my_projects") + requireEnvVar(t, envVars, bindplaneOIDCCustomClaimsDefaultRoleEnvVar, "my_default_role") + requireEnvVar(t, envVars, bindplaneOIDCCustomClaimsOrgAdminGroupNameEnvVar, "acme-org-admin") + requireEnvVar(t, envVars, bindplaneOIDCCustomClaimsProjectsGroupPrefixEnvVar, "acme-projects-") + requireEnvVar(t, envVars, bindplaneOIDCCustomClaimsAdminGroupNameEnvVar, "acme-admin") + requireEnvVar(t, envVars, bindplaneOIDCCustomClaimsUserGroupNameEnvVar, "acme-user") + requireEnvVar(t, envVars, bindplaneOIDCCustomClaimsViewerGroupNameEnvVar, "acme-viewer") + + if len(envVars) != 11 { + t.Errorf("expected 11 env vars, got %d: %v", len(envVars), envVars) + } +} + +func TestGetOIDCCustomClaimsEnvVars_PartialOmitsUnset(t *testing.T) { + customClaims := &bindplanev1alpha1.OIDCCustomClaimsConfig{ + Groups: "my_groups", + OrgAdminGroupName: "acme-org-admin", + } + envVars := getOIDCCustomClaimsEnvVars(customClaims) + + requireEnvVar(t, envVars, bindplaneOIDCCustomClaimsGroupsEnvVar, "my_groups") + requireEnvVar(t, envVars, bindplaneOIDCCustomClaimsOrgAdminGroupNameEnvVar, "acme-org-admin") + requireNoEnvVar(t, envVars, bindplaneOIDCCustomClaimsGroupIDsEnvVar) + requireNoEnvVar(t, envVars, bindplaneOIDCCustomClaimsRolesEnvVar) + requireNoEnvVar(t, envVars, bindplaneOIDCCustomClaimsOrganizationAdminEnvVar) + requireNoEnvVar(t, envVars, bindplaneOIDCCustomClaimsProjectsEnvVar) + requireNoEnvVar(t, envVars, bindplaneOIDCCustomClaimsDefaultRoleEnvVar) + requireNoEnvVar(t, envVars, bindplaneOIDCCustomClaimsProjectsGroupPrefixEnvVar) + requireNoEnvVar(t, envVars, bindplaneOIDCCustomClaimsAdminGroupNameEnvVar) + requireNoEnvVar(t, envVars, bindplaneOIDCCustomClaimsUserGroupNameEnvVar) + requireNoEnvVar(t, envVars, bindplaneOIDCCustomClaimsViewerGroupNameEnvVar) +} + +// TestGetOIDCEnvVars_CustomClaimsPlumbedThrough verifies custom claims reach the +// full OIDC env var set, not just the helper. +func TestGetOIDCEnvVars_CustomClaimsPlumbedThrough(t *testing.T) { + oidc := &bindplanev1alpha1.OIDCConfig{ + Issuer: "https://accounts.example.com", + CustomClaims: &bindplanev1alpha1.OIDCCustomClaimsConfig{ + Groups: "my_groups", + ViewerGroupName: "acme-viewer", + }, + } + envVars := getOIDCEnvVars(oidc) + requireEnvVar(t, envVars, bindplaneOIDCIssuerEnvVar, "https://accounts.example.com") + requireEnvVar(t, envVars, bindplaneOIDCCustomClaimsGroupsEnvVar, "my_groups") + requireEnvVar(t, envVars, bindplaneOIDCCustomClaimsViewerGroupNameEnvVar, "acme-viewer") +} + +func TestGetOIDCEnvVars_NoCustomClaimsEmitsNone(t *testing.T) { + oidc := &bindplanev1alpha1.OIDCConfig{Issuer: "https://accounts.example.com"} + envVars := getOIDCEnvVars(oidc) + requireNoEnvVar(t, envVars, bindplaneOIDCCustomClaimsGroupsEnvVar) + requireNoEnvVar(t, envVars, bindplaneOIDCCustomClaimsOrgAdminGroupNameEnvVar) +} + // ------- getNatsTLSEnvVars (SkipVerify) ------- func TestGetNatsTLSEnvVars_SkipVerifyEmittedWhenSet(t *testing.T) { From 885bad7a319b7afccb187381e419000ae9cff94a Mon Sep 17 00:00:00 2001 From: Joe Armstrong Date: Wed, 5 Aug 2026 08:43:47 -0400 Subject: [PATCH 2/2] ci: publish prerelease tags as GitHub prereleases Goreleaser defaulted to prerelease: false and make_latest: true, so an rc or beta tag published as a normal release and displaced the stable release as "Latest". Set prerelease: auto so semver prerelease suffixes are detected, and gate make_latest on it. --- .goreleaser.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 5bebae0..2dee176 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -61,6 +61,10 @@ release: github: owner: observiq name: bindplane-operator + # Tags with a semver prerelease suffix (e.g. 0.2.0-rc.1, 0.1.0-beta.0) are + # published as GitHub prereleases, and only stable tags become "Latest". + prerelease: auto + make_latest: "{{ not .Prerelease }}" extra_files: - glob: tmp/install.yaml - glob: tmp/install-no-webhook.yaml