diff --git a/api/authmethods/oidc_auth_method_attributes.gen.go b/api/authmethods/oidc_auth_method_attributes.gen.go index 1f9554377..f681d75e0 100644 --- a/api/authmethods/oidc_auth_method_attributes.gen.go +++ b/api/authmethods/oidc_auth_method_attributes.gen.go @@ -27,6 +27,7 @@ type OidcAuthMethodAttributes struct { DisableDiscoveredConfigValidation bool `json:"disable_discovered_config_validation,omitempty"` DryRun bool `json:"dry_run,omitempty"` Prompts []string `json:"prompts,omitempty"` + ProviderType string `json:"provider_type,omitempty"` } func AttributesMapToOidcAuthMethodAttributes(in map[string]any) (*OidcAuthMethodAttributes, error) { diff --git a/api/authmethods/option.gen.go b/api/authmethods/option.gen.go index eb94db001..725000c3d 100644 --- a/api/authmethods/option.gen.go +++ b/api/authmethods/option.gen.go @@ -863,6 +863,30 @@ func DefaultOidcAuthMethodPrompts() Option { } } +func WithOidcAuthMethodProviderType(inProviderType string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = any(map[string]any{}) + } + val := raw.(map[string]any) + val["provider_type"] = inProviderType + o.postMap["attributes"] = val + } +} + +func DefaultOidcAuthMethodProviderType() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = any(map[string]any{}) + } + val := raw.(map[string]any) + val["provider_type"] = nil + o.postMap["attributes"] = val + } +} + func WithOidcAuthMethodSigningAlgorithms(inSigningAlgorithms []string) Option { return func(o *options) { raw, ok := o.postMap["attributes"] diff --git a/internal/auth/oidc/store/oidc.pb.go b/internal/auth/oidc/store/oidc.pb.go index 162d21696..d62c58a78 100644 --- a/internal/auth/oidc/store/oidc.pb.go +++ b/internal/auth/oidc/store/oidc.pb.go @@ -129,7 +129,12 @@ type AuthMethod struct { // These are Value Objects that will be stored as Prompt messages, // and are operatated on as a complete set. // @inject_tag: `gorm:"-"` - Prompts []string `protobuf:"bytes,220,rep,name=prompts,proto3" json:"prompts,omitempty" gorm:"-"` + Prompts []string `protobuf:"bytes,220,rep,name=prompts,proto3" json:"prompts,omitempty" gorm:"-"` + // provider_type optionally identifies the OIDC provider. + // When set, it enables provider specific behavior during authentication. + // When unset, no provider specific behavior is applied. + // @inject_tag: `gorm:"default:null"` + ProviderType string `protobuf:"bytes,230,opt,name=provider_type,json=providerType,proto3" json:"provider_type,omitempty" gorm:"default:null"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -332,6 +337,13 @@ func (x *AuthMethod) GetPrompts() []string { return nil } +func (x *AuthMethod) GetProviderType() string { + if x != nil { + return x.ProviderType + } + return "" +} + // Account represents an OIDC account // the scope_id column is not included here as it is used only to ensure // data integrity in the database between iam users and auth methods. @@ -1099,7 +1111,7 @@ var File_controller_storage_auth_oidc_store_v1_oidc_proto protoreflect.FileDescr const file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDesc = "" + "\n" + - "0controller/storage/auth/oidc/store/v1/oidc.proto\x12%controller.storage.auth.oidc.store.v1\x1a*controller/custom_options/v1/options.proto\x1a/controller/storage/timestamp/v1/timestamp.proto\"\xc1\v\n" + + "0controller/storage/auth/oidc/store/v1/oidc.proto\x12%controller.storage.auth.oidc.store.v1\x1a*controller/custom_options/v1/options.proto\x1a/controller/storage/timestamp/v1/timestamp.proto\"\x95\f\n" + "\n" + "AuthMethod\x12\x1b\n" + "\tpublic_id\x18\n" + @@ -1142,7 +1154,9 @@ const file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDesc = "" + "\x12account_claim_maps\x18\xd2\x01 \x03(\tB5\xc2\xdd)1\n" + "\x10AccountClaimMaps\x12\x1dattributes.account_claim_mapsR\x10accountClaimMaps\x12<\n" + "\aprompts\x18\xdc\x01 \x03(\tB!\xc2\xdd)\x1d\n" + - "\aPrompts\x12\x12attributes.promptsR\aprompts\"\x9a\x04\n" + + "\aPrompts\x12\x12attributes.promptsR\aprompts\x12R\n" + + "\rprovider_type\x18\xe6\x01 \x01(\tB,\xc2\xdd)(\n" + + "\fProviderType\x12\x18attributes.provider_typeR\fproviderType\"\x9a\x04\n" + "\aAccount\x12\x1b\n" + "\tpublic_id\x18\n" + " \x01(\tR\bpublicId\x12K\n" + diff --git a/internal/db/schema/migrations/oss/postgres/112/01_auth_oidc_provider_type.up.sql b/internal/db/schema/migrations/oss/postgres/112/01_auth_oidc_provider_type.up.sql new file mode 100644 index 000000000..f42e80b24 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/112/01_auth_oidc_provider_type.up.sql @@ -0,0 +1,98 @@ +-- Copyright IBM Corp. 2020, 2026 +-- SPDX-License-Identifier: BUSL-1.1 + +-- Adds auth_oidc_method_provider_type_enm enum table and a nullable provider_type column +-- on auth_oidc_method to support Azure Entra ID group overage resolution. Recreates the +-- oidc_auth_method_with_value_obj view to include the new provider_type column. + +begin; + +-- auth_oidc_method_provider_type_enm entries define the supported oidc auth method provider types. +create table auth_oidc_method_provider_type_enm ( + name text primary key + constraint only_predefined_auth_oidc_provider_types_allowed + check ( + name in ( + 'azure' + ) + ) +); + +comment on table auth_oidc_method_provider_type_enm is + 'auth_oidc_method_provider_type_enm entries enumerate the valid oidc auth method provider types'; + +-- define the immutable fields for auth_oidc_method_provider_type_enm +create trigger immutable_columns before update on auth_oidc_method_provider_type_enm + for each row execute procedure immutable_columns('name'); + +insert into auth_oidc_method_provider_type_enm (name) +values + ('azure'); + +-- add an optional provider_type column to auth_oidc_method. null means the +-- provider type is unspecified and no provider-specific behavior is applied. +alter table auth_oidc_method + add column provider_type text + constraint auth_oidc_method_provider_type_enm_fkey + references auth_oidc_method_provider_type_enm(name) + on delete restrict + on update cascade; + +-- we will drop the oidc_auth_method_with_value_obj view, so we can recreate it +-- and add the provider_type to the returned set. +drop view oidc_auth_method_with_value_obj; + +-- Replaces view from 79/01_auth_oidc_prompt.up.sql +-- oidc_auth_method_with_value_obj is useful for reading an oidc auth method +-- with its associated value objects (algs, auds, certs, claims scopes, +-- account claim maps and prompts) as columns with | delimited values. The +-- use of the postgres string_agg(...) to aggregate the value objects into a +-- column works because we are only pulling in one column from the associated +-- tables and that value is part of the primary key and unique. This view +-- will make things like recursive listing of oidc auth methods fairly +-- straightforward to implement for the oidc repo. The view also includes an +-- is_primary_auth_method bool +create view oidc_auth_method_with_value_obj as +select + case when s.primary_auth_method_id is not null then + true + else false end + as is_primary_auth_method, + am.public_id, + am.scope_id, + am.name, + am.description, + am.create_time, + am.update_time, + am.version, + am.state, + am.api_url, + am.disable_discovered_config_validation, + am.issuer, + am.client_id, + am.client_secret, + am.client_secret_hmac, + am.key_id, + am.max_age, + am.provider_type, + -- the string_agg(..) column will be null if there are no associated value objects + string_agg(distinct alg.signing_alg_name, '|') as algs, + string_agg(distinct aud.aud_claim, '|') as auds, + string_agg(distinct cert.certificate, '|') as certs, + string_agg(distinct cs.scope, '|') as claims_scopes, + string_agg(distinct p.prompt, '|') as prompts, + string_agg(distinct concat_ws('=', acm.from_claim, acm.to_claim), '|') as account_claim_maps +from + auth_oidc_method am + left outer join iam_scope s on am.public_id = s.primary_auth_method_id + left outer join auth_oidc_signing_alg alg on am.public_id = alg.oidc_method_id + left outer join auth_oidc_aud_claim aud on am.public_id = aud.oidc_method_id + left outer join auth_oidc_certificate cert on am.public_id = cert.oidc_method_id + left outer join auth_oidc_scope cs on am.public_id = cs.oidc_method_id + left outer join auth_oidc_account_claim_map acm on am.public_id = acm.oidc_method_id + left outer join auth_oidc_prompt p on am.public_id = p.oidc_method_id +group by am.public_id, is_primary_auth_method; -- there can be only one public_id + is_primary_auth_method, so group by isn't a problem. +comment on view oidc_auth_method_with_value_obj is + 'oidc auth method with its associated value objects (algs, auds, certs, scopes, prompts) as columns with | delimited values'; + +commit; diff --git a/internal/db/schema/migrations/oss/postgres/79/01_auth_oidc_prompt.up.sql b/internal/db/schema/migrations/oss/postgres/79/01_auth_oidc_prompt.up.sql index bf07fa723..f9873d0ef 100644 --- a/internal/db/schema/migrations/oss/postgres/79/01_auth_oidc_prompt.up.sql +++ b/internal/db/schema/migrations/oss/postgres/79/01_auth_oidc_prompt.up.sql @@ -67,6 +67,7 @@ drop view oidc_auth_method_with_value_obj; -- will make things like recursive listing of oidc auth methods fairly -- straightforward to implement for the oidc repo. The view also includes an -- is_primary_auth_method bool +-- Replaced in 112/01_auth_oidc_provider_type.up.sql create view oidc_auth_method_with_value_obj as select case when s.primary_auth_method_id is not null then diff --git a/internal/db/sqltest/tests/auth/oidc/provider_type.sql b/internal/db/sqltest/tests/auth/oidc/provider_type.sql new file mode 100644 index 000000000..900f0a2c5 --- /dev/null +++ b/internal/db/sqltest/tests/auth/oidc/provider_type.sql @@ -0,0 +1,91 @@ +-- Copyright IBM Corp. 2020, 2026 +-- SPDX-License-Identifier: BUSL-1.1 + +begin; + select plan(11); + select wtt_load('widgets', 'iam', 'kms', 'auth'); + + -- Set a valid provider_type on the widget oidc auth method + prepare valid_provider_type as + update auth_oidc_method + set provider_type = 'azure' + where public_id = 'aom___widget'; + select lives_ok('valid_provider_type', 'azure is a valid provider_type'); + + select is(provider_type, 'azure', 'provider_type should be azure') + from auth_oidc_method + where public_id = 'aom___widget'; + + -- Set a provider_type that is not in the enum table + prepare invalid_provider_type as + update auth_oidc_method + set provider_type = 'unsupported' + where public_id = 'aom___widget'; + select throws_ok( + 'invalid_provider_type', + '23503', + 'insert or update on table "auth_oidc_method" violates foreign key constraint "auth_oidc_method_provider_type_enm_fkey"', + 'setting a provider_type not in the enum table' + ); + + select is(provider_type, 'azure', 'provider_type is unchanged after a rejected update') + from auth_oidc_method + where public_id = 'aom___widget'; + + -- Set provider_type to null + prepare null_provider_type as + update auth_oidc_method + set provider_type = null + where public_id = 'aom___widget'; + select lives_ok('null_provider_type', 'null is an allowed provider_type'); + + select is(provider_type, null, 'provider_type should be null') + from auth_oidc_method + where public_id = 'aom___widget'; + + -- Insert an unsupported value into the provider_type enum table + prepare invalid_enum_value as + insert into auth_oidc_method_provider_type_enm + (name) + values + ('unsupported'); + select throws_ok( + 'invalid_enum_value', + '23514', + 'new row for relation "auth_oidc_method_provider_type_enm" violates check constraint "only_predefined_auth_oidc_provider_types_allowed"', + 'inserting an unsupported value into the provider_type enum table' + ); + + -- Update an immutable provider_type enum row + prepare immutable_enum_value as + update auth_oidc_method_provider_type_enm + set name = 'unsupported' + where name = 'azure'; + select throws_ok( + 'immutable_enum_value', + '23601', + 'immutable column: auth_oidc_method_provider_type_enm.name', + 'updating an immutable provider_type enum row' + ); + + -- validate oidc_auth_method_with_value_obj view returns provider_type + update auth_oidc_method + set provider_type = 'azure' + where public_id = 'aom___widget'; + select is(provider_type, 'azure', 'the view returns the provider_type') + from oidc_auth_method_with_value_obj + where public_id = 'aom___widget'; + + -- Reset provider_type back to null after it was set + prepare reset_provider_type as + update auth_oidc_method + set provider_type = null + where public_id = 'aom___widget'; + select lives_ok('reset_provider_type', 'provider_type can be reset to null'); + + select is(provider_type, null, 'provider_type should be null after reset') + from auth_oidc_method + where public_id = 'aom___widget'; + + select * from finish(); +rollback; diff --git a/internal/proto/controller/api/resources/authmethods/v1/auth_method.proto b/internal/proto/controller/api/resources/authmethods/v1/auth_method.proto index d44b61438..759cfaec5 100644 --- a/internal/proto/controller/api/resources/authmethods/v1/auth_method.proto +++ b/internal/proto/controller/api/resources/authmethods/v1/auth_method.proto @@ -360,6 +360,18 @@ message OidcAuthMethodAttributes { that: "Prompts" } ]; // @gotags: `class:"public"` + + // provider_type optionally identifies the OIDC provider. + // When set, it enables provider specific behavior during authentication. + // When unset, no provider specific behavior is applied. + google.protobuf.StringValue provider_type = 150 [ + json_name = "provider_type", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.provider_type" + that: "ProviderType" + } + ]; // @gotags: `class:"public"` } // The structure of the OIDC authenticate start response, in the JSON object diff --git a/internal/proto/controller/storage/auth/oidc/store/v1/oidc.proto b/internal/proto/controller/storage/auth/oidc/store/v1/oidc.proto index 3c3b9ec77..a57c7b3a7 100644 --- a/internal/proto/controller/storage/auth/oidc/store/v1/oidc.proto +++ b/internal/proto/controller/storage/auth/oidc/store/v1/oidc.proto @@ -174,6 +174,15 @@ message AuthMethod { this: "Prompts" that: "attributes.prompts" }]; + + // provider_type optionally identifies the OIDC provider. + // When set, it enables provider specific behavior during authentication. + // When unset, no provider specific behavior is applied. + // @inject_tag: `gorm:"default:null"` + string provider_type = 230 [(custom_options.v1.mask_mapping) = { + this: "ProviderType" + that: "attributes.provider_type" + }]; } // Account represents an OIDC account diff --git a/sdk/pbs/controller/api/resources/authmethods/auth_method.pb.go b/sdk/pbs/controller/api/resources/authmethods/auth_method.pb.go index 53861f0d6..972e56f0a 100644 --- a/sdk/pbs/controller/api/resources/authmethods/auth_method.pb.go +++ b/sdk/pbs/controller/api/resources/authmethods/auth_method.pb.go @@ -376,7 +376,11 @@ type OidcAuthMethodAttributes struct { // a result of the update request. DryRun bool `protobuf:"varint,130,opt,name=dry_run,proto3" json:"dry_run,omitempty" class:"public"` // @gotags: `class:"public"` // The prompts allowed for the auth method. - Prompts []string `protobuf:"bytes,140,rep,name=prompts,proto3" json:"prompts,omitempty" class:"public"` // @gotags: `class:"public"` + Prompts []string `protobuf:"bytes,140,rep,name=prompts,proto3" json:"prompts,omitempty" class:"public"` // @gotags: `class:"public"` + // provider_type optionally identifies the OIDC provider. + // When set, it enables provider specific behavior during authentication. + // When unset, no provider specific behavior is applied. + ProviderType *wrapperspb.StringValue `protobuf:"bytes,150,opt,name=provider_type,proto3" json:"provider_type,omitempty" class:"public"` // @gotags: `class:"public"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -523,6 +527,13 @@ func (x *OidcAuthMethodAttributes) GetPrompts() []string { return nil } +func (x *OidcAuthMethodAttributes) GetProviderType() *wrapperspb.StringValue { + if x != nil { + return x.ProviderType + } + return nil +} + // The structure of the OIDC authenticate start response, in the JSON object type OidcAuthMethodAuthenticateStartResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1205,8 +1216,7 @@ const file_controller_api_resources_authmethods_v1_auth_method_proto_rawDesc = " " \x01(\rB>\xa0\xda)\x01\xc2\xdd)6\n" + " attributes.min_login_name_length\x12\x12MinLoginNameLengthR\x15min_login_name_length\x12m\n" + "\x13min_password_length\x18\x14 \x01(\rB;\xa0\xda)\x01\xc2\xdd)3\n" + - "\x1eattributes.min_password_length\x12\x11MinPasswordLengthR\x13min_password_length\"\xbe\n" + - "\n" + + "\x1eattributes.min_password_length\x12\x11MinPasswordLengthR\x13min_password_length\"\xb5\v\n" + "\x18OidcAuthMethodAttributes\x12\x1a\n" + "\x05state\x18\n" + " \x01(\tB\x04\xe2A\x01\x03R\x05state\x12Y\n" + @@ -1235,7 +1245,9 @@ const file_controller_api_resources_authmethods_v1_auth_method_proto_rawDesc = " "$disable_discovered_config_validation\x18x \x01(\bB\x04\xa0\xda)\x01R$disable_discovered_config_validation\x12\x1f\n" + "\adry_run\x18\x82\x01 \x01(\bB\x04\xa0\xda)\x01R\adry_run\x12@\n" + "\aprompts\x18\x8c\x01 \x03(\tB%\xa0\xda)\x01\xc2\xdd)\x1d\n" + - "\x12attributes.prompts\x12\aPromptsR\aprompts\"a\n" + + "\x12attributes.prompts\x12\aPromptsR\aprompts\x12u\n" + + "\rprovider_type\x18\x96\x01 \x01(\v2\x1c.google.protobuf.StringValueB0\xa0\xda)\x01\xc2\xdd)(\n" + + "\x18attributes.provider_type\x12\fProviderTypeR\rprovider_type\"a\n" + "'OidcAuthMethodAuthenticateStartResponse\x12\x1a\n" + "\bauth_url\x18\n" + " \x01(\tR\bauth_url\x12\x1a\n" + @@ -1360,24 +1372,25 @@ var file_controller_api_resources_authmethods_v1_auth_method_proto_depIdxs = []i 11, // 12: controller.api.resources.authmethods.v1.OidcAuthMethodAttributes.client_secret:type_name -> google.protobuf.StringValue 14, // 13: controller.api.resources.authmethods.v1.OidcAuthMethodAttributes.max_age:type_name -> google.protobuf.UInt32Value 11, // 14: controller.api.resources.authmethods.v1.OidcAuthMethodAttributes.api_url_prefix:type_name -> google.protobuf.StringValue - 11, // 15: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.upn_domain:type_name -> google.protobuf.StringValue - 11, // 16: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.user_dn:type_name -> google.protobuf.StringValue - 11, // 17: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.user_attr:type_name -> google.protobuf.StringValue - 11, // 18: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.user_filter:type_name -> google.protobuf.StringValue - 11, // 19: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.group_dn:type_name -> google.protobuf.StringValue - 11, // 20: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.group_attr:type_name -> google.protobuf.StringValue - 11, // 21: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.group_filter:type_name -> google.protobuf.StringValue - 11, // 22: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.client_certificate:type_name -> google.protobuf.StringValue - 11, // 23: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.client_certificate_key:type_name -> google.protobuf.StringValue - 11, // 24: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.bind_dn:type_name -> google.protobuf.StringValue - 11, // 25: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.bind_password:type_name -> google.protobuf.StringValue - 11, // 26: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.dereference_aliases:type_name -> google.protobuf.StringValue - 15, // 27: controller.api.resources.authmethods.v1.AuthMethod.AuthorizedCollectionActionsEntry.value:type_name -> google.protobuf.ListValue - 28, // [28:28] is the sub-list for method output_type - 28, // [28:28] is the sub-list for method input_type - 28, // [28:28] is the sub-list for extension type_name - 28, // [28:28] is the sub-list for extension extendee - 0, // [0:28] is the sub-list for field type_name + 11, // 15: controller.api.resources.authmethods.v1.OidcAuthMethodAttributes.provider_type:type_name -> google.protobuf.StringValue + 11, // 16: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.upn_domain:type_name -> google.protobuf.StringValue + 11, // 17: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.user_dn:type_name -> google.protobuf.StringValue + 11, // 18: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.user_attr:type_name -> google.protobuf.StringValue + 11, // 19: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.user_filter:type_name -> google.protobuf.StringValue + 11, // 20: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.group_dn:type_name -> google.protobuf.StringValue + 11, // 21: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.group_attr:type_name -> google.protobuf.StringValue + 11, // 22: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.group_filter:type_name -> google.protobuf.StringValue + 11, // 23: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.client_certificate:type_name -> google.protobuf.StringValue + 11, // 24: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.client_certificate_key:type_name -> google.protobuf.StringValue + 11, // 25: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.bind_dn:type_name -> google.protobuf.StringValue + 11, // 26: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.bind_password:type_name -> google.protobuf.StringValue + 11, // 27: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.dereference_aliases:type_name -> google.protobuf.StringValue + 15, // 28: controller.api.resources.authmethods.v1.AuthMethod.AuthorizedCollectionActionsEntry.value:type_name -> google.protobuf.ListValue + 29, // [29:29] is the sub-list for method output_type + 29, // [29:29] is the sub-list for method input_type + 29, // [29:29] is the sub-list for extension type_name + 29, // [29:29] is the sub-list for extension extendee + 0, // [0:29] is the sub-list for field type_name } func init() { file_controller_api_resources_authmethods_v1_auth_method_proto_init() }