From 6ba00d56d77a1f4f0811f7cd4ee8d4bdc95d6ad7 Mon Sep 17 00:00:00 2001 From: Alejandro Bernal Date: Thu, 21 May 2026 11:03:49 -0500 Subject: [PATCH 1/5] feat: add source_type fields to GroupTrait and AppTrait (CXH-1473) Add typed source_type metadata to two traits so IDP-supplied group/app kind information can flow through the SDK instead of being dropped or stuffed into the loose profile struct. GroupTrait gets group_source_type (field 3) plus raw_group_source_type (field 4); AppTrait gets app_source_type (field 6) and raw_app_source_type (field 7). All four are additive optional string fields with ignore_empty validation, so existing connectors keep working unchanged. For GroupTrait, expose a typed-string GroupSourceType vocabulary in pkg/types/resource matching the RFC table (native, app_imported, built_in, directory_synced, dynamic, distribution) plus the matching WithGroupSourceType / WithRawGroupSourceType functional options. For AppTrait, ship WithAppSourceType / WithRawAppSourceType as free-form strings; a normalized app vocabulary is out of scope for this slice. Tests lock the wire-format values of the group vocabulary and round-trip both option pairs. --- pb/c1/connector/v2/annotation_trait.pb.go | 114 +++++++++++++++--- .../v2/annotation_trait.pb.validate.go | 60 +++++++++ .../v2/annotation_trait_protoopaque.pb.go | 105 +++++++++++++--- pkg/types/resource/app_trait.go | 14 +++ pkg/types/resource/app_trait_test.go | 15 +++ pkg/types/resource/group_trait.go | 29 +++++ pkg/types/resource/group_trait_test.go | 27 +++++ proto/c1/connector/v2/annotation_trait.proto | 25 ++++ 8 files changed, 355 insertions(+), 34 deletions(-) diff --git a/pb/c1/connector/v2/annotation_trait.pb.go b/pb/c1/connector/v2/annotation_trait.pb.go index 8d0b42028..cf4e8b3c0 100644 --- a/pb/c1/connector/v2/annotation_trait.pb.go +++ b/pb/c1/connector/v2/annotation_trait.pb.go @@ -490,11 +490,19 @@ func (b0 UserTrait_builder) Build() *UserTrait { } type GroupTrait struct { - state protoimpl.MessageState `protogen:"hybrid.v1"` - Icon *AssetRef `protobuf:"bytes,1,opt,name=icon,proto3" json:"icon,omitempty"` - Profile *structpb.Struct `protobuf:"bytes,2,opt,name=profile,proto3" json:"profile,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"hybrid.v1"` + Icon *AssetRef `protobuf:"bytes,1,opt,name=icon,proto3" json:"icon,omitempty"` + Profile *structpb.Struct `protobuf:"bytes,2,opt,name=profile,proto3" json:"profile,omitempty"` + // C1-normalized source type. Canonical vocabulary lives in + // pkg/types/resource (GroupSourceType): native, app_imported, built_in, + // directory_synced, dynamic, distribution. + GroupSourceType string `protobuf:"bytes,3,opt,name=group_source_type,json=groupSourceType,proto3" json:"group_source_type,omitempty"` + // Raw IDP-specific value as returned by the connector's upstream API + // (e.g. "OKTA_GROUP", "APP_GROUP"). Preserved alongside the normalized + // field for traceability. + RawGroupSourceType string `protobuf:"bytes,4,opt,name=raw_group_source_type,json=rawGroupSourceType,proto3" json:"raw_group_source_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GroupTrait) Reset() { @@ -536,6 +544,20 @@ func (x *GroupTrait) GetProfile() *structpb.Struct { return nil } +func (x *GroupTrait) GetGroupSourceType() string { + if x != nil { + return x.GroupSourceType + } + return "" +} + +func (x *GroupTrait) GetRawGroupSourceType() string { + if x != nil { + return x.RawGroupSourceType + } + return "" +} + func (x *GroupTrait) SetIcon(v *AssetRef) { x.Icon = v } @@ -544,6 +566,14 @@ func (x *GroupTrait) SetProfile(v *structpb.Struct) { x.Profile = v } +func (x *GroupTrait) SetGroupSourceType(v string) { + x.GroupSourceType = v +} + +func (x *GroupTrait) SetRawGroupSourceType(v string) { + x.RawGroupSourceType = v +} + func (x *GroupTrait) HasIcon() bool { if x == nil { return false @@ -571,6 +601,14 @@ type GroupTrait_builder struct { Icon *AssetRef Profile *structpb.Struct + // C1-normalized source type. Canonical vocabulary lives in + // pkg/types/resource (GroupSourceType): native, app_imported, built_in, + // directory_synced, dynamic, distribution. + GroupSourceType string + // Raw IDP-specific value as returned by the connector's upstream API + // (e.g. "OKTA_GROUP", "APP_GROUP"). Preserved alongside the normalized + // field for traceability. + RawGroupSourceType string } func (b0 GroupTrait_builder) Build() *GroupTrait { @@ -579,6 +617,8 @@ func (b0 GroupTrait_builder) Build() *GroupTrait { _, _ = b, x x.Icon = b.Icon x.Profile = b.Profile + x.GroupSourceType = b.GroupSourceType + x.RawGroupSourceType = b.RawGroupSourceType return m0 } @@ -903,14 +943,19 @@ func (b0 ScopeBindingTrait_builder) Build() *ScopeBindingTrait { } type AppTrait struct { - state protoimpl.MessageState `protogen:"hybrid.v1"` - HelpUrl string `protobuf:"bytes,1,opt,name=help_url,json=helpUrl,proto3" json:"help_url,omitempty"` - Icon *AssetRef `protobuf:"bytes,2,opt,name=icon,proto3" json:"icon,omitempty"` - Logo *AssetRef `protobuf:"bytes,3,opt,name=logo,proto3" json:"logo,omitempty"` - Profile *structpb.Struct `protobuf:"bytes,4,opt,name=profile,proto3" json:"profile,omitempty"` - Flags []AppTrait_AppFlag `protobuf:"varint,5,rep,packed,name=flags,proto3,enum=c1.connector.v2.AppTrait_AppFlag" json:"flags,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"hybrid.v1"` + HelpUrl string `protobuf:"bytes,1,opt,name=help_url,json=helpUrl,proto3" json:"help_url,omitempty"` + Icon *AssetRef `protobuf:"bytes,2,opt,name=icon,proto3" json:"icon,omitempty"` + Logo *AssetRef `protobuf:"bytes,3,opt,name=logo,proto3" json:"logo,omitempty"` + Profile *structpb.Struct `protobuf:"bytes,4,opt,name=profile,proto3" json:"profile,omitempty"` + Flags []AppTrait_AppFlag `protobuf:"varint,5,rep,packed,name=flags,proto3,enum=c1.connector.v2.AppTrait_AppFlag" json:"flags,omitempty"` + // C1-normalized source type for the app. Free-form for now; a typed + // vocabulary will be introduced in a follow-up RFC. + AppSourceType string `protobuf:"bytes,6,opt,name=app_source_type,json=appSourceType,proto3" json:"app_source_type,omitempty"` + // Raw IDP-specific value as returned by the connector's upstream API. + RawAppSourceType string `protobuf:"bytes,7,opt,name=raw_app_source_type,json=rawAppSourceType,proto3" json:"raw_app_source_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AppTrait) Reset() { @@ -973,6 +1018,20 @@ func (x *AppTrait) GetFlags() []AppTrait_AppFlag { return nil } +func (x *AppTrait) GetAppSourceType() string { + if x != nil { + return x.AppSourceType + } + return "" +} + +func (x *AppTrait) GetRawAppSourceType() string { + if x != nil { + return x.RawAppSourceType + } + return "" +} + func (x *AppTrait) SetHelpUrl(v string) { x.HelpUrl = v } @@ -993,6 +1052,14 @@ func (x *AppTrait) SetFlags(v []AppTrait_AppFlag) { x.Flags = v } +func (x *AppTrait) SetAppSourceType(v string) { + x.AppSourceType = v +} + +func (x *AppTrait) SetRawAppSourceType(v string) { + x.RawAppSourceType = v +} + func (x *AppTrait) HasIcon() bool { if x == nil { return false @@ -1034,6 +1101,11 @@ type AppTrait_builder struct { Logo *AssetRef Profile *structpb.Struct Flags []AppTrait_AppFlag + // C1-normalized source type for the app. Free-form for now; a typed + // vocabulary will be introduced in a follow-up RFC. + AppSourceType string + // Raw IDP-specific value as returned by the connector's upstream API. + RawAppSourceType string } func (b0 AppTrait_builder) Build() *AppTrait { @@ -1045,6 +1117,8 @@ func (b0 AppTrait_builder) Build() *AppTrait { x.Logo = b.Logo x.Profile = b.Profile x.Flags = b.Flags + x.AppSourceType = b.AppSourceType + x.RawAppSourceType = b.RawAppSourceType return m0 } @@ -1813,11 +1887,14 @@ const file_c1_connector_v2_annotation_trait_proto_rawDesc = "" + "\x18ACCOUNT_TYPE_UNSPECIFIED\x10\x00\x12\x16\n" + "\x12ACCOUNT_TYPE_HUMAN\x10\x01\x12\x18\n" + "\x14ACCOUNT_TYPE_SERVICE\x10\x02\x12\x17\n" + - "\x13ACCOUNT_TYPE_SYSTEM\x10\x03\"n\n" + + "\x13ACCOUNT_TYPE_SYSTEM\x10\x03\"\xe6\x01\n" + "\n" + "GroupTrait\x12-\n" + "\x04icon\x18\x01 \x01(\v2\x19.c1.connector.v2.AssetRefR\x04icon\x121\n" + - "\aprofile\x18\x02 \x01(\v2\x17.google.protobuf.StructR\aprofile\"\x98\x01\n" + + "\aprofile\x18\x02 \x01(\v2\x17.google.protobuf.StructR\aprofile\x126\n" + + "\x11group_source_type\x18\x03 \x01(\tB\n" + + "\xfaB\ar\x05(@\xd0\x01\x01R\x0fgroupSourceType\x12>\n" + + "\x15raw_group_source_type\x18\x04 \x01(\tB\v\xfaB\br\x06(\x80\x02\xd0\x01\x01R\x12rawGroupSourceType\"\x98\x01\n" + "\tRoleTrait\x121\n" + "\aprofile\x18\x01 \x01(\v2\x17.google.protobuf.StructR\aprofile\x12X\n" + "\x15role_scope_conditions\x18\x02 \x01(\v2$.c1.connector.v2.RoleScopeConditionsR\x13roleScopeConditions\"n\n" + @@ -1832,13 +1909,16 @@ const file_c1_connector_v2_annotation_trait_proto_rawDesc = "" + "expression\"\xa6\x01\n" + "\x11ScopeBindingTrait\x12>\n" + "\arole_id\x18\x01 \x01(\v2\x1b.c1.connector.v2.ResourceIdB\b\xfaB\x05\x8a\x01\x02\x10\x01R\x06roleId\x12Q\n" + - "\x11scope_resource_id\x18\x02 \x01(\v2\x1b.c1.connector.v2.ResourceIdB\b\xfaB\x05\x8a\x01\x02\x10\x01R\x0fscopeResourceId\"\x9a\x03\n" + + "\x11scope_resource_id\x18\x02 \x01(\v2\x1b.c1.connector.v2.ResourceIdB\b\xfaB\x05\x8a\x01\x02\x10\x01R\x0fscopeResourceId\"\x8a\x04\n" + "\bAppTrait\x125\n" + "\bhelp_url\x18\x01 \x01(\tB\x1a\xfaB\x17r\x15 \x01(\x80\b:\bhttps://\xd0\x01\x01\x88\x01\x01R\ahelpUrl\x12-\n" + "\x04icon\x18\x02 \x01(\v2\x19.c1.connector.v2.AssetRefR\x04icon\x12-\n" + "\x04logo\x18\x03 \x01(\v2\x19.c1.connector.v2.AssetRefR\x04logo\x121\n" + "\aprofile\x18\x04 \x01(\v2\x17.google.protobuf.StructR\aprofile\x127\n" + - "\x05flags\x18\x05 \x03(\x0e2!.c1.connector.v2.AppTrait.AppFlagR\x05flags\"\x8c\x01\n" + + "\x05flags\x18\x05 \x03(\x0e2!.c1.connector.v2.AppTrait.AppFlagR\x05flags\x122\n" + + "\x0fapp_source_type\x18\x06 \x01(\tB\n" + + "\xfaB\ar\x05(@\xd0\x01\x01R\rappSourceType\x12:\n" + + "\x13raw_app_source_type\x18\a \x01(\tB\v\xfaB\br\x06(\x80\x02\xd0\x01\x01R\x10rawAppSourceType\"\x8c\x01\n" + "\aAppFlag\x12\x18\n" + "\x14APP_FLAG_UNSPECIFIED\x10\x00\x12\x13\n" + "\x0fAPP_FLAG_HIDDEN\x10\x01\x12\x15\n" + diff --git a/pb/c1/connector/v2/annotation_trait.pb.validate.go b/pb/c1/connector/v2/annotation_trait.pb.validate.go index 55a2a43d0..f3d7ee7a9 100644 --- a/pb/c1/connector/v2/annotation_trait.pb.validate.go +++ b/pb/c1/connector/v2/annotation_trait.pb.validate.go @@ -504,6 +504,36 @@ func (m *GroupTrait) validate(all bool) error { } } + if m.GetGroupSourceType() != "" { + + if len(m.GetGroupSourceType()) > 64 { + err := GroupTraitValidationError{ + field: "GroupSourceType", + reason: "value length must be at most 64 bytes", + } + if !all { + return err + } + errors = append(errors, err) + } + + } + + if m.GetRawGroupSourceType() != "" { + + if len(m.GetRawGroupSourceType()) > 256 { + err := GroupTraitValidationError{ + field: "RawGroupSourceType", + reason: "value length must be at most 256 bytes", + } + if !all { + return err + } + errors = append(errors, err) + } + + } + if len(errors) > 0 { return GroupTraitMultiError(errors) } @@ -1318,6 +1348,36 @@ func (m *AppTrait) validate(all bool) error { } } + if m.GetAppSourceType() != "" { + + if len(m.GetAppSourceType()) > 64 { + err := AppTraitValidationError{ + field: "AppSourceType", + reason: "value length must be at most 64 bytes", + } + if !all { + return err + } + errors = append(errors, err) + } + + } + + if m.GetRawAppSourceType() != "" { + + if len(m.GetRawAppSourceType()) > 256 { + err := AppTraitValidationError{ + field: "RawAppSourceType", + reason: "value length must be at most 256 bytes", + } + if !all { + return err + } + errors = append(errors, err) + } + + } + if len(errors) > 0 { return AppTraitMultiError(errors) } diff --git a/pb/c1/connector/v2/annotation_trait_protoopaque.pb.go b/pb/c1/connector/v2/annotation_trait_protoopaque.pb.go index 650293052..3dbcc6363 100644 --- a/pb/c1/connector/v2/annotation_trait_protoopaque.pb.go +++ b/pb/c1/connector/v2/annotation_trait_protoopaque.pb.go @@ -490,11 +490,13 @@ func (b0 UserTrait_builder) Build() *UserTrait { } type GroupTrait struct { - state protoimpl.MessageState `protogen:"opaque.v1"` - xxx_hidden_Icon *AssetRef `protobuf:"bytes,1,opt,name=icon,proto3"` - xxx_hidden_Profile *structpb.Struct `protobuf:"bytes,2,opt,name=profile,proto3"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Icon *AssetRef `protobuf:"bytes,1,opt,name=icon,proto3"` + xxx_hidden_Profile *structpb.Struct `protobuf:"bytes,2,opt,name=profile,proto3"` + xxx_hidden_GroupSourceType string `protobuf:"bytes,3,opt,name=group_source_type,json=groupSourceType,proto3"` + xxx_hidden_RawGroupSourceType string `protobuf:"bytes,4,opt,name=raw_group_source_type,json=rawGroupSourceType,proto3"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GroupTrait) Reset() { @@ -536,6 +538,20 @@ func (x *GroupTrait) GetProfile() *structpb.Struct { return nil } +func (x *GroupTrait) GetGroupSourceType() string { + if x != nil { + return x.xxx_hidden_GroupSourceType + } + return "" +} + +func (x *GroupTrait) GetRawGroupSourceType() string { + if x != nil { + return x.xxx_hidden_RawGroupSourceType + } + return "" +} + func (x *GroupTrait) SetIcon(v *AssetRef) { x.xxx_hidden_Icon = v } @@ -544,6 +560,14 @@ func (x *GroupTrait) SetProfile(v *structpb.Struct) { x.xxx_hidden_Profile = v } +func (x *GroupTrait) SetGroupSourceType(v string) { + x.xxx_hidden_GroupSourceType = v +} + +func (x *GroupTrait) SetRawGroupSourceType(v string) { + x.xxx_hidden_RawGroupSourceType = v +} + func (x *GroupTrait) HasIcon() bool { if x == nil { return false @@ -571,6 +595,14 @@ type GroupTrait_builder struct { Icon *AssetRef Profile *structpb.Struct + // C1-normalized source type. Canonical vocabulary lives in + // pkg/types/resource (GroupSourceType): native, app_imported, built_in, + // directory_synced, dynamic, distribution. + GroupSourceType string + // Raw IDP-specific value as returned by the connector's upstream API + // (e.g. "OKTA_GROUP", "APP_GROUP"). Preserved alongside the normalized + // field for traceability. + RawGroupSourceType string } func (b0 GroupTrait_builder) Build() *GroupTrait { @@ -579,6 +611,8 @@ func (b0 GroupTrait_builder) Build() *GroupTrait { _, _ = b, x x.xxx_hidden_Icon = b.Icon x.xxx_hidden_Profile = b.Profile + x.xxx_hidden_GroupSourceType = b.GroupSourceType + x.xxx_hidden_RawGroupSourceType = b.RawGroupSourceType return m0 } @@ -904,14 +938,16 @@ func (b0 ScopeBindingTrait_builder) Build() *ScopeBindingTrait { } type AppTrait struct { - state protoimpl.MessageState `protogen:"opaque.v1"` - xxx_hidden_HelpUrl string `protobuf:"bytes,1,opt,name=help_url,json=helpUrl,proto3"` - xxx_hidden_Icon *AssetRef `protobuf:"bytes,2,opt,name=icon,proto3"` - xxx_hidden_Logo *AssetRef `protobuf:"bytes,3,opt,name=logo,proto3"` - xxx_hidden_Profile *structpb.Struct `protobuf:"bytes,4,opt,name=profile,proto3"` - xxx_hidden_Flags []AppTrait_AppFlag `protobuf:"varint,5,rep,packed,name=flags,proto3,enum=c1.connector.v2.AppTrait_AppFlag"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_HelpUrl string `protobuf:"bytes,1,opt,name=help_url,json=helpUrl,proto3"` + xxx_hidden_Icon *AssetRef `protobuf:"bytes,2,opt,name=icon,proto3"` + xxx_hidden_Logo *AssetRef `protobuf:"bytes,3,opt,name=logo,proto3"` + xxx_hidden_Profile *structpb.Struct `protobuf:"bytes,4,opt,name=profile,proto3"` + xxx_hidden_Flags []AppTrait_AppFlag `protobuf:"varint,5,rep,packed,name=flags,proto3,enum=c1.connector.v2.AppTrait_AppFlag"` + xxx_hidden_AppSourceType string `protobuf:"bytes,6,opt,name=app_source_type,json=appSourceType,proto3"` + xxx_hidden_RawAppSourceType string `protobuf:"bytes,7,opt,name=raw_app_source_type,json=rawAppSourceType,proto3"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AppTrait) Reset() { @@ -974,6 +1010,20 @@ func (x *AppTrait) GetFlags() []AppTrait_AppFlag { return nil } +func (x *AppTrait) GetAppSourceType() string { + if x != nil { + return x.xxx_hidden_AppSourceType + } + return "" +} + +func (x *AppTrait) GetRawAppSourceType() string { + if x != nil { + return x.xxx_hidden_RawAppSourceType + } + return "" +} + func (x *AppTrait) SetHelpUrl(v string) { x.xxx_hidden_HelpUrl = v } @@ -994,6 +1044,14 @@ func (x *AppTrait) SetFlags(v []AppTrait_AppFlag) { x.xxx_hidden_Flags = v } +func (x *AppTrait) SetAppSourceType(v string) { + x.xxx_hidden_AppSourceType = v +} + +func (x *AppTrait) SetRawAppSourceType(v string) { + x.xxx_hidden_RawAppSourceType = v +} + func (x *AppTrait) HasIcon() bool { if x == nil { return false @@ -1035,6 +1093,11 @@ type AppTrait_builder struct { Logo *AssetRef Profile *structpb.Struct Flags []AppTrait_AppFlag + // C1-normalized source type for the app. Free-form for now; a typed + // vocabulary will be introduced in a follow-up RFC. + AppSourceType string + // Raw IDP-specific value as returned by the connector's upstream API. + RawAppSourceType string } func (b0 AppTrait_builder) Build() *AppTrait { @@ -1046,6 +1109,8 @@ func (b0 AppTrait_builder) Build() *AppTrait { x.xxx_hidden_Logo = b.Logo x.xxx_hidden_Profile = b.Profile x.xxx_hidden_Flags = b.Flags + x.xxx_hidden_AppSourceType = b.AppSourceType + x.xxx_hidden_RawAppSourceType = b.RawAppSourceType return m0 } @@ -1807,11 +1872,14 @@ const file_c1_connector_v2_annotation_trait_proto_rawDesc = "" + "\x18ACCOUNT_TYPE_UNSPECIFIED\x10\x00\x12\x16\n" + "\x12ACCOUNT_TYPE_HUMAN\x10\x01\x12\x18\n" + "\x14ACCOUNT_TYPE_SERVICE\x10\x02\x12\x17\n" + - "\x13ACCOUNT_TYPE_SYSTEM\x10\x03\"n\n" + + "\x13ACCOUNT_TYPE_SYSTEM\x10\x03\"\xe6\x01\n" + "\n" + "GroupTrait\x12-\n" + "\x04icon\x18\x01 \x01(\v2\x19.c1.connector.v2.AssetRefR\x04icon\x121\n" + - "\aprofile\x18\x02 \x01(\v2\x17.google.protobuf.StructR\aprofile\"\x98\x01\n" + + "\aprofile\x18\x02 \x01(\v2\x17.google.protobuf.StructR\aprofile\x126\n" + + "\x11group_source_type\x18\x03 \x01(\tB\n" + + "\xfaB\ar\x05(@\xd0\x01\x01R\x0fgroupSourceType\x12>\n" + + "\x15raw_group_source_type\x18\x04 \x01(\tB\v\xfaB\br\x06(\x80\x02\xd0\x01\x01R\x12rawGroupSourceType\"\x98\x01\n" + "\tRoleTrait\x121\n" + "\aprofile\x18\x01 \x01(\v2\x17.google.protobuf.StructR\aprofile\x12X\n" + "\x15role_scope_conditions\x18\x02 \x01(\v2$.c1.connector.v2.RoleScopeConditionsR\x13roleScopeConditions\"n\n" + @@ -1826,13 +1894,16 @@ const file_c1_connector_v2_annotation_trait_proto_rawDesc = "" + "expression\"\xa6\x01\n" + "\x11ScopeBindingTrait\x12>\n" + "\arole_id\x18\x01 \x01(\v2\x1b.c1.connector.v2.ResourceIdB\b\xfaB\x05\x8a\x01\x02\x10\x01R\x06roleId\x12Q\n" + - "\x11scope_resource_id\x18\x02 \x01(\v2\x1b.c1.connector.v2.ResourceIdB\b\xfaB\x05\x8a\x01\x02\x10\x01R\x0fscopeResourceId\"\x9a\x03\n" + + "\x11scope_resource_id\x18\x02 \x01(\v2\x1b.c1.connector.v2.ResourceIdB\b\xfaB\x05\x8a\x01\x02\x10\x01R\x0fscopeResourceId\"\x8a\x04\n" + "\bAppTrait\x125\n" + "\bhelp_url\x18\x01 \x01(\tB\x1a\xfaB\x17r\x15 \x01(\x80\b:\bhttps://\xd0\x01\x01\x88\x01\x01R\ahelpUrl\x12-\n" + "\x04icon\x18\x02 \x01(\v2\x19.c1.connector.v2.AssetRefR\x04icon\x12-\n" + "\x04logo\x18\x03 \x01(\v2\x19.c1.connector.v2.AssetRefR\x04logo\x121\n" + "\aprofile\x18\x04 \x01(\v2\x17.google.protobuf.StructR\aprofile\x127\n" + - "\x05flags\x18\x05 \x03(\x0e2!.c1.connector.v2.AppTrait.AppFlagR\x05flags\"\x8c\x01\n" + + "\x05flags\x18\x05 \x03(\x0e2!.c1.connector.v2.AppTrait.AppFlagR\x05flags\x122\n" + + "\x0fapp_source_type\x18\x06 \x01(\tB\n" + + "\xfaB\ar\x05(@\xd0\x01\x01R\rappSourceType\x12:\n" + + "\x13raw_app_source_type\x18\a \x01(\tB\v\xfaB\br\x06(\x80\x02\xd0\x01\x01R\x10rawAppSourceType\"\x8c\x01\n" + "\aAppFlag\x12\x18\n" + "\x14APP_FLAG_UNSPECIFIED\x10\x00\x12\x13\n" + "\x0fAPP_FLAG_HIDDEN\x10\x01\x12\x15\n" + diff --git a/pkg/types/resource/app_trait.go b/pkg/types/resource/app_trait.go index b1d416726..3909a583f 100644 --- a/pkg/types/resource/app_trait.go +++ b/pkg/types/resource/app_trait.go @@ -53,6 +53,20 @@ func WithAppHelpURL(helpURL string) AppTraitOption { } } +func WithAppSourceType(sourceType string) AppTraitOption { + return func(at *v2.AppTrait) error { + at.SetAppSourceType(sourceType) + return nil + } +} + +func WithRawAppSourceType(raw string) AppTraitOption { + return func(at *v2.AppTrait) error { + at.SetRawAppSourceType(raw) + return nil + } +} + // NewAppTrait creates a new `AppTrait` with the given help URL, and profile. func NewAppTrait(opts ...AppTraitOption) (*v2.AppTrait, error) { at := &v2.AppTrait{} diff --git a/pkg/types/resource/app_trait_test.go b/pkg/types/resource/app_trait_test.go index b5ed70696..cf973ae69 100644 --- a/pkg/types/resource/app_trait_test.go +++ b/pkg/types/resource/app_trait_test.go @@ -64,3 +64,18 @@ func TestAppTrait(t *testing.T) { require.NotNil(t, at.GetLogo()) require.Equal(t, "logoID", at.GetLogo().GetId()) } + +func TestAppTraitSourceType(t *testing.T) { + at, err := NewAppTrait() + require.NoError(t, err) + require.Empty(t, at.GetAppSourceType()) + require.Empty(t, at.GetRawAppSourceType()) + + at, err = NewAppTrait( + WithAppSourceType("native"), + WithRawAppSourceType("OKTA_APP"), + ) + require.NoError(t, err) + require.Equal(t, "native", at.GetAppSourceType()) + require.Equal(t, "OKTA_APP", at.GetRawAppSourceType()) +} diff --git a/pkg/types/resource/group_trait.go b/pkg/types/resource/group_trait.go index 2857fe991..d0845cef0 100644 --- a/pkg/types/resource/group_trait.go +++ b/pkg/types/resource/group_trait.go @@ -8,6 +8,21 @@ import ( "google.golang.org/protobuf/types/known/structpb" ) +// GroupSourceType is the C1-normalized vocabulary used for +// GroupTrait.group_source_type. Connectors should pick the closest match +// for the IDP's group kind and pass the raw IDP value to +// raw_group_source_type via WithRawGroupSourceType. +type GroupSourceType string + +const ( + GroupSourceTypeNative GroupSourceType = "native" + GroupSourceTypeAppImported GroupSourceType = "app_imported" + GroupSourceTypeBuiltIn GroupSourceType = "built_in" + GroupSourceTypeDirectorySynced GroupSourceType = "directory_synced" + GroupSourceTypeDynamic GroupSourceType = "dynamic" + GroupSourceTypeDistribution GroupSourceType = "distribution" +) + type GroupTraitOption func(gt *v2.GroupTrait) error func WithGroupProfile(profile map[string]interface{}) GroupTraitOption { @@ -30,6 +45,20 @@ func WithGroupIcon(assetRef *v2.AssetRef) GroupTraitOption { } } +func WithGroupSourceType(sourceType GroupSourceType) GroupTraitOption { + return func(gt *v2.GroupTrait) error { + gt.SetGroupSourceType(string(sourceType)) + return nil + } +} + +func WithRawGroupSourceType(raw string) GroupTraitOption { + return func(gt *v2.GroupTrait) error { + gt.SetRawGroupSourceType(raw) + return nil + } +} + // NewGroupTrait creates a new `GroupTrait` with the provided profile. func NewGroupTrait(opts ...GroupTraitOption) (*v2.GroupTrait, error) { groupTrait := &v2.GroupTrait{} diff --git a/pkg/types/resource/group_trait_test.go b/pkg/types/resource/group_trait_test.go index 62741443c..07567629c 100644 --- a/pkg/types/resource/group_trait_test.go +++ b/pkg/types/resource/group_trait_test.go @@ -36,3 +36,30 @@ func TestGroupTrait(t *testing.T) { require.True(t, ok) require.Equal(t, "group-profile-field", val) } + +func TestGroupTraitSourceType(t *testing.T) { + gt, err := NewGroupTrait() + require.NoError(t, err) + require.Empty(t, gt.GetGroupSourceType()) + require.Empty(t, gt.GetRawGroupSourceType()) + + gt, err = NewGroupTrait( + WithGroupSourceType(GroupSourceTypeAppImported), + WithRawGroupSourceType("OKTA_GROUP"), + ) + require.NoError(t, err) + require.Equal(t, "app_imported", gt.GetGroupSourceType()) + require.Equal(t, "OKTA_GROUP", gt.GetRawGroupSourceType()) +} + +// Locks the wire-format strings of the normalized vocabulary: a change +// here is a cross-stack compatibility break for any connector or +// downstream consumer that hardcodes one of the values. +func TestGroupSourceTypeVocabulary(t *testing.T) { + require.Equal(t, GroupSourceType("native"), GroupSourceTypeNative) + require.Equal(t, GroupSourceType("app_imported"), GroupSourceTypeAppImported) + require.Equal(t, GroupSourceType("built_in"), GroupSourceTypeBuiltIn) + require.Equal(t, GroupSourceType("directory_synced"), GroupSourceTypeDirectorySynced) + require.Equal(t, GroupSourceType("dynamic"), GroupSourceTypeDynamic) + require.Equal(t, GroupSourceType("distribution"), GroupSourceTypeDistribution) +} diff --git a/proto/c1/connector/v2/annotation_trait.proto b/proto/c1/connector/v2/annotation_trait.proto index f15af648b..9fb2de6ae 100644 --- a/proto/c1/connector/v2/annotation_trait.proto +++ b/proto/c1/connector/v2/annotation_trait.proto @@ -76,6 +76,20 @@ message UserTrait { message GroupTrait { c1.connector.v2.AssetRef icon = 1; google.protobuf.Struct profile = 2; + // C1-normalized source type. Canonical vocabulary lives in + // pkg/types/resource (GroupSourceType): native, app_imported, built_in, + // directory_synced, dynamic, distribution. + string group_source_type = 3 [(validate.rules).string = { + max_bytes: 64 + ignore_empty: true + }]; + // Raw IDP-specific value as returned by the connector's upstream API + // (e.g. "OKTA_GROUP", "APP_GROUP"). Preserved alongside the normalized + // field for traceability. + string raw_group_source_type = 4 [(validate.rules).string = { + max_bytes: 256 + ignore_empty: true + }]; } message RoleTrait { @@ -122,6 +136,17 @@ message AppTrait { APP_FLAG_BOOKMARK = 5; } repeated AppFlag flags = 5; + // C1-normalized source type for the app. Free-form for now; a typed + // vocabulary will be introduced in a follow-up RFC. + string app_source_type = 6 [(validate.rules).string = { + max_bytes: 64 + ignore_empty: true + }]; + // Raw IDP-specific value as returned by the connector's upstream API. + string raw_app_source_type = 7 [(validate.rules).string = { + max_bytes: 256 + ignore_empty: true + }]; } message SecretTrait { From 7cc5299dc7992a602599e661f3b1ac1126856e25 Mon Sep 17 00:00:00 2001 From: Alejandro Bernal Date: Thu, 21 May 2026 11:03:53 -0500 Subject: [PATCH 2/5] Update SDK version to v0.9.15 --- pkg/sdk/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/sdk/version.go b/pkg/sdk/version.go index 8efce9110..02827e52f 100644 --- a/pkg/sdk/version.go +++ b/pkg/sdk/version.go @@ -1,3 +1,3 @@ package sdk -const Version = "v0.9.14" +const Version = "v0.9.15" From 635a2c6b70a06aff736d114091da0622166897e1 Mon Sep 17 00:00:00 2001 From: Alejandro Bernal Date: Thu, 21 May 2026 11:20:11 -0500 Subject: [PATCH 3/5] test: harden source_type coverage and document AppTrait asymmetry Replace the tautological TestGroupSourceTypeVocabulary (which asserted GroupSourceType("native") == GroupSourceTypeNative) with TestGroupSourceTypeWireFormat: marshal each constant through a real proto Marshal/Unmarshal and assert the deserialized GroupTrait carries the documented literal string. A constant rename or typo now trips the test; same-value aliases still pass. Add per-trait validation tests covering the four new fields against the proto validate.rules (max_bytes 64 on normalized, 256 on raw, ignore_empty=true). Empty strings pass; 65-byte normalized and 257-byte raw values surface a GroupTraitValidationError / AppTraitValidationError with the expected Field() string. Add annotation round-trip tests for both traits: pack the trait into a v2.Resource via annotations.New, read it back through GetGroupTrait / GetAppTrait, and confirm the new source_type fields survive the anypb.UnmarshalTo path. On the proto/Go side, expand the AppTrait.app_source_type comment with example values ("saml", "oidc", "scim") instead of the unanchored "follow-up RFC" pointer, and document that WithAppSourceType is deliberately free-form (unlike WithGroupSourceType) so the next reader doesn't read the asymmetry as an oversight. --- pb/c1/connector/v2/annotation_trait.pb.go | 10 ++- .../v2/annotation_trait_protoopaque.pb.go | 5 +- pkg/types/resource/app_trait.go | 2 + pkg/types/resource/app_trait_test.go | 46 ++++++++++ pkg/types/resource/group_trait_test.go | 83 ++++++++++++++++--- proto/c1/connector/v2/annotation_trait.proto | 5 +- 6 files changed, 133 insertions(+), 18 deletions(-) diff --git a/pb/c1/connector/v2/annotation_trait.pb.go b/pb/c1/connector/v2/annotation_trait.pb.go index cf4e8b3c0..4d8a1855c 100644 --- a/pb/c1/connector/v2/annotation_trait.pb.go +++ b/pb/c1/connector/v2/annotation_trait.pb.go @@ -949,8 +949,9 @@ type AppTrait struct { Logo *AssetRef `protobuf:"bytes,3,opt,name=logo,proto3" json:"logo,omitempty"` Profile *structpb.Struct `protobuf:"bytes,4,opt,name=profile,proto3" json:"profile,omitempty"` Flags []AppTrait_AppFlag `protobuf:"varint,5,rep,packed,name=flags,proto3,enum=c1.connector.v2.AppTrait_AppFlag" json:"flags,omitempty"` - // C1-normalized source type for the app. Free-form for now; a typed - // vocabulary will be introduced in a follow-up RFC. + // C1-normalized source type for the app. Free-form pending a typed + // vocabulary; expected values describe the integration kind (for + // example: "saml", "oidc", "scim", or vendor-defined classes). AppSourceType string `protobuf:"bytes,6,opt,name=app_source_type,json=appSourceType,proto3" json:"app_source_type,omitempty"` // Raw IDP-specific value as returned by the connector's upstream API. RawAppSourceType string `protobuf:"bytes,7,opt,name=raw_app_source_type,json=rawAppSourceType,proto3" json:"raw_app_source_type,omitempty"` @@ -1101,8 +1102,9 @@ type AppTrait_builder struct { Logo *AssetRef Profile *structpb.Struct Flags []AppTrait_AppFlag - // C1-normalized source type for the app. Free-form for now; a typed - // vocabulary will be introduced in a follow-up RFC. + // C1-normalized source type for the app. Free-form pending a typed + // vocabulary; expected values describe the integration kind (for + // example: "saml", "oidc", "scim", or vendor-defined classes). AppSourceType string // Raw IDP-specific value as returned by the connector's upstream API. RawAppSourceType string diff --git a/pb/c1/connector/v2/annotation_trait_protoopaque.pb.go b/pb/c1/connector/v2/annotation_trait_protoopaque.pb.go index 3dbcc6363..97f3c55f3 100644 --- a/pb/c1/connector/v2/annotation_trait_protoopaque.pb.go +++ b/pb/c1/connector/v2/annotation_trait_protoopaque.pb.go @@ -1093,8 +1093,9 @@ type AppTrait_builder struct { Logo *AssetRef Profile *structpb.Struct Flags []AppTrait_AppFlag - // C1-normalized source type for the app. Free-form for now; a typed - // vocabulary will be introduced in a follow-up RFC. + // C1-normalized source type for the app. Free-form pending a typed + // vocabulary; expected values describe the integration kind (for + // example: "saml", "oidc", "scim", or vendor-defined classes). AppSourceType string // Raw IDP-specific value as returned by the connector's upstream API. RawAppSourceType string diff --git a/pkg/types/resource/app_trait.go b/pkg/types/resource/app_trait.go index 3909a583f..ec5ca9a42 100644 --- a/pkg/types/resource/app_trait.go +++ b/pkg/types/resource/app_trait.go @@ -53,6 +53,8 @@ func WithAppHelpURL(helpURL string) AppTraitOption { } } +// WithAppSourceType is free-form: AppTrait has no normalized vocabulary +// yet, unlike GroupSourceType. func WithAppSourceType(sourceType string) AppTraitOption { return func(at *v2.AppTrait) error { at.SetAppSourceType(sourceType) diff --git a/pkg/types/resource/app_trait_test.go b/pkg/types/resource/app_trait_test.go index cf973ae69..92fc4ac2f 100644 --- a/pkg/types/resource/app_trait_test.go +++ b/pkg/types/resource/app_trait_test.go @@ -1,9 +1,12 @@ package resource import ( + "errors" + "strings" "testing" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" + "github.com/conductorone/baton-sdk/pkg/annotations" "github.com/stretchr/testify/require" ) @@ -79,3 +82,46 @@ func TestAppTraitSourceType(t *testing.T) { require.Equal(t, "native", at.GetAppSourceType()) require.Equal(t, "OKTA_APP", at.GetRawAppSourceType()) } + +func TestAppTraitValidateSourceType(t *testing.T) { + cases := []struct { + name string + srcType string + rawType string + errField string + }{ + {"empty passes", "", "", ""}, + {"valid passes", "saml", "OKTA_APP", ""}, + {"normalized too long", strings.Repeat("a", 65), "", "AppSourceType"}, + {"raw too long", "saml", strings.Repeat("a", 257), "RawAppSourceType"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + at := &v2.AppTrait{} + at.SetAppSourceType(tc.srcType) + at.SetRawAppSourceType(tc.rawType) + err := at.Validate() + if tc.errField == "" { + require.NoError(t, err) + return + } + require.Error(t, err) + var vErr v2.AppTraitValidationError + require.True(t, errors.As(err, &vErr)) + require.Equal(t, tc.errField, vErr.Field()) + }) + } +} + +func TestAppTraitAnnotationRoundTrip(t *testing.T) { + src, err := NewAppTrait( + WithAppSourceType("saml"), + WithRawAppSourceType("OKTA_APP"), + ) + require.NoError(t, err) + resource := &v2.Resource{Annotations: annotations.New(src)} + got, err := GetAppTrait(resource) + require.NoError(t, err) + require.Equal(t, "saml", got.GetAppSourceType()) + require.Equal(t, "OKTA_APP", got.GetRawAppSourceType()) +} diff --git a/pkg/types/resource/group_trait_test.go b/pkg/types/resource/group_trait_test.go index 07567629c..84ca8c071 100644 --- a/pkg/types/resource/group_trait_test.go +++ b/pkg/types/resource/group_trait_test.go @@ -1,10 +1,14 @@ package resource import ( + "errors" + "strings" "testing" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" + "github.com/conductorone/baton-sdk/pkg/annotations" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" ) func TestGroupTrait(t *testing.T) { @@ -52,14 +56,73 @@ func TestGroupTraitSourceType(t *testing.T) { require.Equal(t, "OKTA_GROUP", gt.GetRawGroupSourceType()) } -// Locks the wire-format strings of the normalized vocabulary: a change -// here is a cross-stack compatibility break for any connector or -// downstream consumer that hardcodes one of the values. -func TestGroupSourceTypeVocabulary(t *testing.T) { - require.Equal(t, GroupSourceType("native"), GroupSourceTypeNative) - require.Equal(t, GroupSourceType("app_imported"), GroupSourceTypeAppImported) - require.Equal(t, GroupSourceType("built_in"), GroupSourceTypeBuiltIn) - require.Equal(t, GroupSourceType("directory_synced"), GroupSourceTypeDirectorySynced) - require.Equal(t, GroupSourceType("dynamic"), GroupSourceTypeDynamic) - require.Equal(t, GroupSourceType("distribution"), GroupSourceTypeDistribution) +// Marshals a GroupTrait built from each constant and asserts the wire +// bytes round-trip back to the documented literal string. A constant +// rename or typo trips this; a same-value alias does not. +func TestGroupSourceTypeWireFormat(t *testing.T) { + cases := []struct { + c GroupSourceType + want string + }{ + {GroupSourceTypeNative, "native"}, + {GroupSourceTypeAppImported, "app_imported"}, + {GroupSourceTypeBuiltIn, "built_in"}, + {GroupSourceTypeDirectorySynced, "directory_synced"}, + {GroupSourceTypeDynamic, "dynamic"}, + {GroupSourceTypeDistribution, "distribution"}, + } + for _, tc := range cases { + t.Run(tc.want, func(t *testing.T) { + src, err := NewGroupTrait(WithGroupSourceType(tc.c)) + require.NoError(t, err) + wire, err := proto.Marshal(src) + require.NoError(t, err) + got := &v2.GroupTrait{} + require.NoError(t, proto.Unmarshal(wire, got)) + require.Equal(t, tc.want, got.GetGroupSourceType()) + }) + } +} + +func TestGroupTraitValidateSourceType(t *testing.T) { + cases := []struct { + name string + srcType string + rawType string + errField string + }{ + {"empty passes", "", "", ""}, + {"valid passes", "native", "OKTA_GROUP", ""}, + {"normalized too long", strings.Repeat("a", 65), "", "GroupSourceType"}, + {"raw too long", "native", strings.Repeat("a", 257), "RawGroupSourceType"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + gt := &v2.GroupTrait{} + gt.SetGroupSourceType(tc.srcType) + gt.SetRawGroupSourceType(tc.rawType) + err := gt.Validate() + if tc.errField == "" { + require.NoError(t, err) + return + } + require.Error(t, err) + var vErr v2.GroupTraitValidationError + require.True(t, errors.As(err, &vErr)) + require.Equal(t, tc.errField, vErr.Field()) + }) + } +} + +func TestGroupTraitAnnotationRoundTrip(t *testing.T) { + src, err := NewGroupTrait( + WithGroupSourceType(GroupSourceTypeBuiltIn), + WithRawGroupSourceType("OKTA_GROUP"), + ) + require.NoError(t, err) + resource := &v2.Resource{Annotations: annotations.New(src)} + got, err := GetGroupTrait(resource) + require.NoError(t, err) + require.Equal(t, "built_in", got.GetGroupSourceType()) + require.Equal(t, "OKTA_GROUP", got.GetRawGroupSourceType()) } diff --git a/proto/c1/connector/v2/annotation_trait.proto b/proto/c1/connector/v2/annotation_trait.proto index 9fb2de6ae..d00831b4e 100644 --- a/proto/c1/connector/v2/annotation_trait.proto +++ b/proto/c1/connector/v2/annotation_trait.proto @@ -136,8 +136,9 @@ message AppTrait { APP_FLAG_BOOKMARK = 5; } repeated AppFlag flags = 5; - // C1-normalized source type for the app. Free-form for now; a typed - // vocabulary will be introduced in a follow-up RFC. + // C1-normalized source type for the app. Free-form pending a typed + // vocabulary; expected values describe the integration kind (for + // example: "saml", "oidc", "scim", or vendor-defined classes). string app_source_type = 6 [(validate.rules).string = { max_bytes: 64 ignore_empty: true From 9e80a5d4d23d6ee7f5ab4b32cfd2f5daf15dc950 Mon Sep 17 00:00:00 2001 From: Alejandro Bernal Date: Thu, 21 May 2026 12:12:08 -0500 Subject: [PATCH 4/5] docs: trim over-spec source_type comments Drop the inline group source-type vocabulary list from the proto comment (it duplicates the Go source of truth at pkg/types/resource and creates a two-places-to-update sync risk). Replace with a single-line pointer to GroupSourceType. Drop the raw_group_source_type / raw_app_source_type proto comments entirely. Both restated the field name; the typed/raw pairing is already explained on GroupSourceType's doc comment in pkg/types/resource. Trim the AppTrait.app_source_type comment to one line. Examples ("saml", "oidc", "scim") were borderline normative without being constraints, so they invited misuse. Trim TestGroupSourceTypeWireFormat's header to a single line that says what the test catches (constant rename or typo) rather than restating what the body does. --- pb/c1/connector/v2/annotation_trait.pb.go | 64 +++++++------------ .../v2/annotation_trait_protoopaque.pb.go | 28 +++----- pkg/types/resource/app_trait.go | 2 - pkg/types/resource/group_trait.go | 4 -- pkg/types/resource/group_trait_test.go | 4 +- proto/c1/connector/v2/annotation_trait.proto | 10 --- 6 files changed, 32 insertions(+), 80 deletions(-) diff --git a/pb/c1/connector/v2/annotation_trait.pb.go b/pb/c1/connector/v2/annotation_trait.pb.go index 4d8a1855c..ff2b4e885 100644 --- a/pb/c1/connector/v2/annotation_trait.pb.go +++ b/pb/c1/connector/v2/annotation_trait.pb.go @@ -490,17 +490,11 @@ func (b0 UserTrait_builder) Build() *UserTrait { } type GroupTrait struct { - state protoimpl.MessageState `protogen:"hybrid.v1"` - Icon *AssetRef `protobuf:"bytes,1,opt,name=icon,proto3" json:"icon,omitempty"` - Profile *structpb.Struct `protobuf:"bytes,2,opt,name=profile,proto3" json:"profile,omitempty"` - // C1-normalized source type. Canonical vocabulary lives in - // pkg/types/resource (GroupSourceType): native, app_imported, built_in, - // directory_synced, dynamic, distribution. - GroupSourceType string `protobuf:"bytes,3,opt,name=group_source_type,json=groupSourceType,proto3" json:"group_source_type,omitempty"` - // Raw IDP-specific value as returned by the connector's upstream API - // (e.g. "OKTA_GROUP", "APP_GROUP"). Preserved alongside the normalized - // field for traceability. - RawGroupSourceType string `protobuf:"bytes,4,opt,name=raw_group_source_type,json=rawGroupSourceType,proto3" json:"raw_group_source_type,omitempty"` + state protoimpl.MessageState `protogen:"hybrid.v1"` + Icon *AssetRef `protobuf:"bytes,1,opt,name=icon,proto3" json:"icon,omitempty"` + Profile *structpb.Struct `protobuf:"bytes,2,opt,name=profile,proto3" json:"profile,omitempty"` + GroupSourceType string `protobuf:"bytes,3,opt,name=group_source_type,json=groupSourceType,proto3" json:"group_source_type,omitempty"` + RawGroupSourceType string `protobuf:"bytes,4,opt,name=raw_group_source_type,json=rawGroupSourceType,proto3" json:"raw_group_source_type,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -599,15 +593,9 @@ func (x *GroupTrait) ClearProfile() { type GroupTrait_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. - Icon *AssetRef - Profile *structpb.Struct - // C1-normalized source type. Canonical vocabulary lives in - // pkg/types/resource (GroupSourceType): native, app_imported, built_in, - // directory_synced, dynamic, distribution. - GroupSourceType string - // Raw IDP-specific value as returned by the connector's upstream API - // (e.g. "OKTA_GROUP", "APP_GROUP"). Preserved alongside the normalized - // field for traceability. + Icon *AssetRef + Profile *structpb.Struct + GroupSourceType string RawGroupSourceType string } @@ -943,18 +931,14 @@ func (b0 ScopeBindingTrait_builder) Build() *ScopeBindingTrait { } type AppTrait struct { - state protoimpl.MessageState `protogen:"hybrid.v1"` - HelpUrl string `protobuf:"bytes,1,opt,name=help_url,json=helpUrl,proto3" json:"help_url,omitempty"` - Icon *AssetRef `protobuf:"bytes,2,opt,name=icon,proto3" json:"icon,omitempty"` - Logo *AssetRef `protobuf:"bytes,3,opt,name=logo,proto3" json:"logo,omitempty"` - Profile *structpb.Struct `protobuf:"bytes,4,opt,name=profile,proto3" json:"profile,omitempty"` - Flags []AppTrait_AppFlag `protobuf:"varint,5,rep,packed,name=flags,proto3,enum=c1.connector.v2.AppTrait_AppFlag" json:"flags,omitempty"` - // C1-normalized source type for the app. Free-form pending a typed - // vocabulary; expected values describe the integration kind (for - // example: "saml", "oidc", "scim", or vendor-defined classes). - AppSourceType string `protobuf:"bytes,6,opt,name=app_source_type,json=appSourceType,proto3" json:"app_source_type,omitempty"` - // Raw IDP-specific value as returned by the connector's upstream API. - RawAppSourceType string `protobuf:"bytes,7,opt,name=raw_app_source_type,json=rawAppSourceType,proto3" json:"raw_app_source_type,omitempty"` + state protoimpl.MessageState `protogen:"hybrid.v1"` + HelpUrl string `protobuf:"bytes,1,opt,name=help_url,json=helpUrl,proto3" json:"help_url,omitempty"` + Icon *AssetRef `protobuf:"bytes,2,opt,name=icon,proto3" json:"icon,omitempty"` + Logo *AssetRef `protobuf:"bytes,3,opt,name=logo,proto3" json:"logo,omitempty"` + Profile *structpb.Struct `protobuf:"bytes,4,opt,name=profile,proto3" json:"profile,omitempty"` + Flags []AppTrait_AppFlag `protobuf:"varint,5,rep,packed,name=flags,proto3,enum=c1.connector.v2.AppTrait_AppFlag" json:"flags,omitempty"` + AppSourceType string `protobuf:"bytes,6,opt,name=app_source_type,json=appSourceType,proto3" json:"app_source_type,omitempty"` + RawAppSourceType string `protobuf:"bytes,7,opt,name=raw_app_source_type,json=rawAppSourceType,proto3" json:"raw_app_source_type,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1097,16 +1081,12 @@ func (x *AppTrait) ClearProfile() { type AppTrait_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. - HelpUrl string - Icon *AssetRef - Logo *AssetRef - Profile *structpb.Struct - Flags []AppTrait_AppFlag - // C1-normalized source type for the app. Free-form pending a typed - // vocabulary; expected values describe the integration kind (for - // example: "saml", "oidc", "scim", or vendor-defined classes). - AppSourceType string - // Raw IDP-specific value as returned by the connector's upstream API. + HelpUrl string + Icon *AssetRef + Logo *AssetRef + Profile *structpb.Struct + Flags []AppTrait_AppFlag + AppSourceType string RawAppSourceType string } diff --git a/pb/c1/connector/v2/annotation_trait_protoopaque.pb.go b/pb/c1/connector/v2/annotation_trait_protoopaque.pb.go index 97f3c55f3..0e1643418 100644 --- a/pb/c1/connector/v2/annotation_trait_protoopaque.pb.go +++ b/pb/c1/connector/v2/annotation_trait_protoopaque.pb.go @@ -593,15 +593,9 @@ func (x *GroupTrait) ClearProfile() { type GroupTrait_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. - Icon *AssetRef - Profile *structpb.Struct - // C1-normalized source type. Canonical vocabulary lives in - // pkg/types/resource (GroupSourceType): native, app_imported, built_in, - // directory_synced, dynamic, distribution. - GroupSourceType string - // Raw IDP-specific value as returned by the connector's upstream API - // (e.g. "OKTA_GROUP", "APP_GROUP"). Preserved alongside the normalized - // field for traceability. + Icon *AssetRef + Profile *structpb.Struct + GroupSourceType string RawGroupSourceType string } @@ -1088,16 +1082,12 @@ func (x *AppTrait) ClearProfile() { type AppTrait_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. - HelpUrl string - Icon *AssetRef - Logo *AssetRef - Profile *structpb.Struct - Flags []AppTrait_AppFlag - // C1-normalized source type for the app. Free-form pending a typed - // vocabulary; expected values describe the integration kind (for - // example: "saml", "oidc", "scim", or vendor-defined classes). - AppSourceType string - // Raw IDP-specific value as returned by the connector's upstream API. + HelpUrl string + Icon *AssetRef + Logo *AssetRef + Profile *structpb.Struct + Flags []AppTrait_AppFlag + AppSourceType string RawAppSourceType string } diff --git a/pkg/types/resource/app_trait.go b/pkg/types/resource/app_trait.go index ec5ca9a42..3909a583f 100644 --- a/pkg/types/resource/app_trait.go +++ b/pkg/types/resource/app_trait.go @@ -53,8 +53,6 @@ func WithAppHelpURL(helpURL string) AppTraitOption { } } -// WithAppSourceType is free-form: AppTrait has no normalized vocabulary -// yet, unlike GroupSourceType. func WithAppSourceType(sourceType string) AppTraitOption { return func(at *v2.AppTrait) error { at.SetAppSourceType(sourceType) diff --git a/pkg/types/resource/group_trait.go b/pkg/types/resource/group_trait.go index d0845cef0..16cee8470 100644 --- a/pkg/types/resource/group_trait.go +++ b/pkg/types/resource/group_trait.go @@ -8,10 +8,6 @@ import ( "google.golang.org/protobuf/types/known/structpb" ) -// GroupSourceType is the C1-normalized vocabulary used for -// GroupTrait.group_source_type. Connectors should pick the closest match -// for the IDP's group kind and pass the raw IDP value to -// raw_group_source_type via WithRawGroupSourceType. type GroupSourceType string const ( diff --git a/pkg/types/resource/group_trait_test.go b/pkg/types/resource/group_trait_test.go index 84ca8c071..b32618c0a 100644 --- a/pkg/types/resource/group_trait_test.go +++ b/pkg/types/resource/group_trait_test.go @@ -56,9 +56,7 @@ func TestGroupTraitSourceType(t *testing.T) { require.Equal(t, "OKTA_GROUP", gt.GetRawGroupSourceType()) } -// Marshals a GroupTrait built from each constant and asserts the wire -// bytes round-trip back to the documented literal string. A constant -// rename or typo trips this; a same-value alias does not. +// A constant rename or typo trips this; a same-value alias does not. func TestGroupSourceTypeWireFormat(t *testing.T) { cases := []struct { c GroupSourceType diff --git a/proto/c1/connector/v2/annotation_trait.proto b/proto/c1/connector/v2/annotation_trait.proto index d00831b4e..e17bef9e8 100644 --- a/proto/c1/connector/v2/annotation_trait.proto +++ b/proto/c1/connector/v2/annotation_trait.proto @@ -76,16 +76,10 @@ message UserTrait { message GroupTrait { c1.connector.v2.AssetRef icon = 1; google.protobuf.Struct profile = 2; - // C1-normalized source type. Canonical vocabulary lives in - // pkg/types/resource (GroupSourceType): native, app_imported, built_in, - // directory_synced, dynamic, distribution. string group_source_type = 3 [(validate.rules).string = { max_bytes: 64 ignore_empty: true }]; - // Raw IDP-specific value as returned by the connector's upstream API - // (e.g. "OKTA_GROUP", "APP_GROUP"). Preserved alongside the normalized - // field for traceability. string raw_group_source_type = 4 [(validate.rules).string = { max_bytes: 256 ignore_empty: true @@ -136,14 +130,10 @@ message AppTrait { APP_FLAG_BOOKMARK = 5; } repeated AppFlag flags = 5; - // C1-normalized source type for the app. Free-form pending a typed - // vocabulary; expected values describe the integration kind (for - // example: "saml", "oidc", "scim", or vendor-defined classes). string app_source_type = 6 [(validate.rules).string = { max_bytes: 64 ignore_empty: true }]; - // Raw IDP-specific value as returned by the connector's upstream API. string raw_app_source_type = 7 [(validate.rules).string = { max_bytes: 256 ignore_empty: true From 1c21ac1ac4c6b96b1290f454d79cc555735d2946 Mon Sep 17 00:00:00 2001 From: Alejandro Bernal Date: Fri, 22 May 2026 09:05:32 -0500 Subject: [PATCH 5/5] refactor: type WithAppSourceType to prevent raw/normalized swap Introduce AppSourceType string alias so callers can no longer pass a raw value to WithAppSourceType (or vice versa) without a compiler error. No constants yet, no app source-type vocab is defined. Also drop the stray blank lines in pkg/sdk/version.go. --- pkg/sdk/version.go | 2 -- pkg/types/resource/app_trait.go | 6 ++++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/sdk/version.go b/pkg/sdk/version.go index 27de4dbe8..cbb6305d2 100644 --- a/pkg/sdk/version.go +++ b/pkg/sdk/version.go @@ -1,5 +1,3 @@ package sdk - const Version = "v0.9.16" - diff --git a/pkg/types/resource/app_trait.go b/pkg/types/resource/app_trait.go index 3909a583f..77c3622eb 100644 --- a/pkg/types/resource/app_trait.go +++ b/pkg/types/resource/app_trait.go @@ -8,6 +8,8 @@ import ( "google.golang.org/protobuf/types/known/structpb" ) +type AppSourceType string + type AppTraitOption func(gt *v2.AppTrait) error func WithAppIcon(assetRef *v2.AssetRef) AppTraitOption { @@ -53,9 +55,9 @@ func WithAppHelpURL(helpURL string) AppTraitOption { } } -func WithAppSourceType(sourceType string) AppTraitOption { +func WithAppSourceType(sourceType AppSourceType) AppTraitOption { return func(at *v2.AppTrait) error { - at.SetAppSourceType(sourceType) + at.SetAppSourceType(string(sourceType)) return nil } }