diff --git a/api/authmethods/oidc_auth_method_attributes.gen.go b/api/authmethods/oidc_auth_method_attributes.gen.go index dded7bf84..c07f357e4 100644 --- a/api/authmethods/oidc_auth_method_attributes.gen.go +++ b/api/authmethods/oidc_auth_method_attributes.gen.go @@ -14,6 +14,7 @@ type OidcAuthMethodAttributes struct { IdpCaCerts []string `json:"idp_ca_certs,omitempty"` AllowedAudiences []string `json:"allowed_audiences,omitempty"` ClaimsScopes []string `json:"claims_scopes,omitempty"` + AccountClaimMaps []string `json:"account_claim_maps,omitempty"` DisableDiscoveredConfigValidation bool `json:"disable_discovered_config_validation,omitempty"` DryRun bool `json:"dry_run,omitempty"` } diff --git a/api/authmethods/option.gen.go b/api/authmethods/option.gen.go index 23246c400..ca8212f69 100644 --- a/api/authmethods/option.gen.go +++ b/api/authmethods/option.gen.go @@ -87,6 +87,30 @@ func WithRecursive(recurse bool) Option { } } +func WithOidcAuthMethodAccountClaimMaps(inAccountClaimMaps []string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["account_claim_maps"] = inAccountClaimMaps + o.postMap["attributes"] = val + } +} + +func DefaultOidcAuthMethodAccountClaimMaps() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["account_claim_maps"] = nil + o.postMap["attributes"] = val + } +} + func WithOidcAuthMethodAllowedAudiences(inAllowedAudiences []string) Option { return func(o *options) { raw, ok := o.postMap["attributes"] diff --git a/internal/auth/oidc/account_claim_map.go b/internal/auth/oidc/account_claim_map.go new file mode 100644 index 000000000..2c0187682 --- /dev/null +++ b/internal/auth/oidc/account_claim_map.go @@ -0,0 +1,100 @@ +package oidc + +import ( + "fmt" + + "github.com/hashicorp/boundary/internal/auth/oidc/store" + "github.com/hashicorp/boundary/internal/errors" + "google.golang.org/protobuf/proto" +) + +const ( + // defaultAcctClaimMapTableName defines the default table name for an AccountClaimMap + defaultAcctClaimMapTableName = "auth_oidc_account_claim_map" +) + +type AccountToClaim string + +const ( + ToSubClaim AccountToClaim = "sub" + ToEmailClaim AccountToClaim = "email" + ToNameClaim AccountToClaim = "name" +) + +func ConvertToAccountToClaim(s string) (AccountToClaim, error) { + const op = "oidc.(AccountToClaim).convertToAccountToClaim" + switch s { + case string(ToSubClaim): + return ToSubClaim, nil + case string(ToEmailClaim): + return ToEmailClaim, nil + case string(ToNameClaim): + return ToNameClaim, nil + default: + return "", errors.New(errors.InvalidParameter, op, fmt.Sprintf("%s is not a valid ToAccountClaim value", s)) + } +} + +// AccountClaimMap defines optional OIDC scope values that are used to request +// claims, in addition to the default scope of "openid" (see: DefaultClaimsScope). +type AccountClaimMap struct { + *store.AccountClaimMap + tableName string +} + +func NewAccountClaimMap(authMethodId, fromClaim string, toClaim AccountToClaim) (*AccountClaimMap, error) { + const op = "oidc.NewAccountClaimMap" + cs := &AccountClaimMap{ + AccountClaimMap: &store.AccountClaimMap{ + OidcMethodId: authMethodId, + FromClaim: fromClaim, + ToClaim: string(toClaim), + }, + } + if err := cs.validate(op); err != nil { + return nil, err + } + return cs, nil +} + +// validate the AccountClaimMap. On success, it will return nil. +func (cs *AccountClaimMap) validate(caller errors.Op) error { + if cs.OidcMethodId == "" { + return errors.New(errors.InvalidParameter, caller, "missing oidc auth method id") + } + if cs.FromClaim == "" { + return errors.New(errors.InvalidParameter, caller, "missing from claim") + } + if _, err := ConvertToAccountToClaim(cs.ToClaim); err != nil { + return errors.Wrap(err, caller) + } + return nil +} + +// AllocClaimsScope makes an empty one in memory +func AllocAccountClaimMap() AccountClaimMap { + return AccountClaimMap{ + AccountClaimMap: &store.AccountClaimMap{}, + } +} + +// Clone a AccountClaimMap +func (cs *AccountClaimMap) Clone() *AccountClaimMap { + cp := proto.Clone(cs.AccountClaimMap) + return &AccountClaimMap{ + AccountClaimMap: cp.(*store.AccountClaimMap), + } +} + +// TableName returns the table name. +func (s *AccountClaimMap) TableName() string { + if s.tableName != "" { + return s.tableName + } + return defaultAcctClaimMapTableName +} + +// SetTableName sets the table name. +func (s *AccountClaimMap) SetTableName(n string) { + s.tableName = n +} diff --git a/internal/auth/oidc/account_claim_map_test.go b/internal/auth/oidc/account_claim_map_test.go new file mode 100644 index 000000000..7a306317b --- /dev/null +++ b/internal/auth/oidc/account_claim_map_test.go @@ -0,0 +1,282 @@ +package oidc + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestAccountClaimMap_Create(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + rw := db.New(conn) + + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + testAuthMethod := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + + type args struct { + authMethodId string + to AccountToClaim + from string + } + tests := []struct { + name string + args args + want *AccountClaimMap + wantErr bool + wantIsErr errors.Code + create bool + wantCreateErr bool + wantCreateIsErr errors.Code + }{ + { + name: "valid", + args: args{ + authMethodId: testAuthMethod.PublicId, + to: ToSubClaim, + from: "oid", + }, + create: true, + want: func() *AccountClaimMap { + want := AllocAccountClaimMap() + want.OidcMethodId = testAuthMethod.PublicId + want.ToClaim = string(ToSubClaim) + want.FromClaim = "oid" + return &want + }(), + }, + { + name: "dup", // must follow "valid" test. Url must be be unique for an OidcMethodId + args: args{ + authMethodId: testAuthMethod.PublicId, + to: ToSubClaim, + from: "oid", + }, + create: true, + want: func() *AccountClaimMap { + want := AllocAccountClaimMap() + want.OidcMethodId = testAuthMethod.PublicId + want.ToClaim = string(ToSubClaim) + want.FromClaim = "oid" + return &want + }(), + wantCreateErr: true, + wantCreateIsErr: errors.NotUnique, + }, + { + name: "empty-auth-method", + args: args{ + authMethodId: "", + to: ToSubClaim, + from: "oid", + }, + wantErr: true, + wantIsErr: errors.InvalidParameter, + }, + { + name: "empty-to", + args: args{ + authMethodId: testAuthMethod.PublicId, + from: "oid", + }, + wantErr: true, + wantIsErr: errors.InvalidParameter, + }, + { + name: "empty-from", + args: args{ + authMethodId: testAuthMethod.PublicId, + to: ToSubClaim, + }, + wantErr: true, + wantIsErr: errors.InvalidParameter, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewAccountClaimMap(tt.args.authMethodId, tt.args.from, tt.args.to) + if tt.wantErr { + require.Error(err) + assert.True(errors.Match(errors.T(tt.wantIsErr), err)) + return + } + require.NoError(err) + assert.Equal(tt.want, got) + if tt.create { + ctx := context.Background() + err = rw.Create(ctx, got) + if tt.wantCreateErr { + assert.Error(err) + assert.True(errors.Match(errors.T(tt.wantCreateIsErr), err)) + return + } else { + assert.NoError(err) + } + found := AllocAccountClaimMap() + require.NoError(rw.LookupWhere(ctx, &found, "oidc_method_id = ? and to_claim = ?", tt.args.authMethodId, tt.args.to)) + assert.Equal(got, &found) + } + }) + } +} + +func TestAccountClaimMap_Delete(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + rw := db.New(conn) + + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + testAuthMethod := + TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithApiUrl(TestConvertToUrls(t, "https://api.com")[0]), WithAudClaims("alice.com")) // seed an extra callback url to just make sure the delete only gets the right num of rows + + testResource := func(authMethodId string, fromClaim string, toClaim AccountToClaim) *AccountClaimMap { + c, err := NewAccountClaimMap(authMethodId, fromClaim, toClaim) + require.NoError(t, err) + return c + } + tests := []struct { + name string + AccountClaimMap *AccountClaimMap + wantRowsDeleted int + overrides func(*AccountClaimMap) + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + AccountClaimMap: testResource(testAuthMethod.PublicId, "oid", "sub"), + wantErr: false, + wantRowsDeleted: 1, + }, + { + name: "bad-OidcMethodId", + AccountClaimMap: testResource(testAuthMethod.PublicId, "oid", "sub"), + overrides: func(c *AccountClaimMap) { c.OidcMethodId = "bad-id"; c.ToClaim = "sub" }, + wantErr: false, + wantRowsDeleted: 0, + }, + { + name: "bad-toClaim", + AccountClaimMap: testResource(testAuthMethod.PublicId, "oid", "sub"), + overrides: func(c *AccountClaimMap) { c.ToClaim = "bad-aud" }, + wantErr: false, + wantRowsDeleted: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + + _, err := rw.Exec(ctx, "delete from auth_oidc_account_claim_map", nil) + require.NoError(err) + + cp := tt.AccountClaimMap.Clone() + require.NoError(rw.Create(ctx, &cp)) + + if tt.overrides != nil { + tt.overrides(cp) + } + deletedRows, err := rw.Delete(ctx, &cp) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + if tt.wantRowsDeleted == 0 { + assert.Equal(tt.wantRowsDeleted, deletedRows) + return + } + assert.Equal(tt.wantRowsDeleted, deletedRows) + found := AllocAccountClaimMap() + err = rw.LookupWhere(ctx, &found, "oidc_method_id = ? and to_claim = ?", tt.AccountClaimMap.OidcMethodId, tt.AccountClaimMap.ToClaim) + assert.True(errors.IsNotFoundError(err)) + }) + } +} + +func TestAccountClaimMap_Clone(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + + t.Run("valid", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + m := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + orig, err := NewAccountClaimMap(m.PublicId, "oid", ToSubClaim) + require.NoError(err) + cp := orig.Clone() + assert.True(proto.Equal(cp.AccountClaimMap, orig.AccountClaimMap)) + }) + t.Run("not-equal", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + m := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + orig, err := NewAccountClaimMap(m.PublicId, "oid", ToSubClaim) + require.NoError(err) + orig2, err := NewAccountClaimMap(m.PublicId, "uid", ToSubClaim) + require.NoError(err) + + cp := orig.Clone() + assert.True(!proto.Equal(cp.AccountClaimMap, orig2.AccountClaimMap)) + }) +} + +func TestAccountClaimMap_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := defaultAcctClaimMapTableName + tests := []struct { + name string + setNameTo string + want string + }{ + { + name: "new-name", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := AllocAccountClaimMap() + require.Equal(defaultTableName, def.TableName()) + m := AllocAccountClaimMap() + m.SetTableName(tt.setNameTo) + assert.Equal(tt.want, m.TableName()) + }) + } +} diff --git a/internal/auth/oidc/auth_method.go b/internal/auth/oidc/auth_method.go index 2a035e1b4..79d617f96 100644 --- a/internal/auth/oidc/auth_method.go +++ b/internal/auth/oidc/auth_method.go @@ -17,6 +17,7 @@ import ( wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/hashicorp/go-kms-wrapping/structwrapping" "github.com/hashicorp/go-multierror" + kvbuilder "github.com/hashicorp/shared-secure-libs/kv-builder" "google.golang.org/protobuf/proto" ) @@ -107,6 +108,12 @@ func NewAuthMethod(scopeId string, clientId string, clientSecret ClientSecret, o a.SigningAlgs = append(a.SigningAlgs, string(alg)) } } + if len(opts.withAccountClaimMap) > 0 { + a.AccountClaimMaps = make([]string, 0, len(opts.withAccountClaimMap)) + for k, v := range opts.withAccountClaimMap { + a.AccountClaimMaps = append(a.AccountClaimMaps, fmt.Sprintf("%s=%s", k, v)) + } + } if a.OperationalState != string(InactiveState) { if err := a.isComplete(); err != nil { return nil, errors.Wrap(err, op, errors.WithMsg("new auth method being created with incomplete data but non-inactive state")) @@ -270,10 +277,11 @@ func (am *AuthMethod) isComplete() error { } type convertedValues struct { - Algs []interface{} - Auds []interface{} - Certs []interface{} - ClaimsScopes []interface{} + Algs []interface{} + Auds []interface{} + Certs []interface{} + ClaimsScopes []interface{} + AccountClaimMaps []interface{} } // convertValueObjects converts the embedded value objects. It will return an @@ -284,7 +292,7 @@ func (am *AuthMethod) convertValueObjects() (*convertedValues, error) { return nil, errors.New(errors.InvalidPublicId, op, "missing public id") } var err error - var addAlgs, addAuds, addCerts, addScopes []interface{} + var addAlgs, addAuds, addCerts, addScopes, addAccountClaimMaps []interface{} if addAlgs, err = am.convertSigningAlgs(); err != nil { return nil, errors.Wrap(err, op) } @@ -297,11 +305,15 @@ func (am *AuthMethod) convertValueObjects() (*convertedValues, error) { if addScopes, err = am.convertClaimsScopes(); err != nil { return nil, errors.Wrap(err, op) } + if addAccountClaimMaps, err = am.convertAccountClaimMaps(); err != nil { + return nil, errors.Wrap(err, op) + } return &convertedValues{ - Algs: addAlgs, - Auds: addAuds, - Certs: addCerts, - ClaimsScopes: addScopes, + Algs: addAlgs, + Auds: addAuds, + Certs: addCerts, + ClaimsScopes: addScopes, + AccountClaimMaps: addAccountClaimMaps, }, nil } @@ -380,3 +392,51 @@ func (am *AuthMethod) convertClaimsScopes() ([]interface{}, error) { } return newInterfaces, nil } + +// convertAccountClaimMaps converts the embedded account claim maps from +// []string to []interface{} where each slice element is a *AccountClaimMap. It +// will return an error if the AuthMethod's public id is not set or it can +// convert the account claim maps. +func (am *AuthMethod) convertAccountClaimMaps() ([]interface{}, error) { + const op = "oidc.(AuthMethod).convertAccountClaimMaps" + if am.PublicId == "" { + return nil, errors.New(errors.InvalidPublicId, op, "missing public id") + } + newInterfaces := make([]interface{}, 0, len(am.AccountClaimMaps)) + const ( + from = 0 + to = 1 + ) + acms, err := ParseAccountClaimMaps(am.AccountClaimMaps...) + if err != nil { + return nil, errors.Wrap(err, op) + } + for from, to := range acms { + toClaim, err := ConvertToAccountToClaim(to) + if err != nil { + return nil, errors.Wrap(err, op) + } + obj, err := NewAccountClaimMap(am.PublicId, from, toClaim) + if err != nil { + return nil, errors.Wrap(err, op) + } + newInterfaces = append(newInterfaces, obj) + } + return newInterfaces, nil +} + +func ParseAccountClaimMaps(m ...string) (map[string]string, error) { + const op = "oidc.parseAccountClaimMaps" + var b kvbuilder.Builder + if err := b.Add(m...); err != nil { + return nil, errors.New(errors.InvalidParameter, op, "error parsing map", errors.WithWrap(err)) + } + claimMap := map[string]string{} + for k, v := range b.Map() { + var ok bool + if claimMap[k], ok = v.(string); !ok { + return nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("account claim map key %s value %q is not a string", k, v)) + } + } + return claimMap, nil +} diff --git a/internal/auth/oidc/auth_method_test.go b/internal/auth/oidc/auth_method_test.go index b0903e5c7..bda46d2d2 100644 --- a/internal/auth/oidc/auth_method_test.go +++ b/internal/auth/oidc/auth_method_test.go @@ -3,6 +3,7 @@ package oidc import ( "context" "net/url" + "sort" "testing" "github.com/hashicorp/boundary/internal/auth/oidc/store" @@ -573,6 +574,18 @@ func Test_convertValueObjects(t *testing.T) { testClaimsScopes = append(testClaimsScopes, obj) } + testClaimMaps := []string{"oid=sub", "display_name=name"} + testAccountClaimMaps := make([]interface{}, 0, len(testClaimMaps)) + acms, err := ParseAccountClaimMaps(testClaimMaps...) + require.NoError(t, err) + for from, to := range acms { + toClaim, err := ConvertToAccountToClaim(to) + require.NoError(t, err) + obj, err := NewAccountClaimMap(testPublicId, from, toClaim) + require.NoError(t, err) + testAccountClaimMaps = append(testAccountClaimMaps, obj) + } + tests := []struct { name string authMethodId string @@ -580,6 +593,7 @@ func Test_convertValueObjects(t *testing.T) { auds []string certs []string scopes []string + maps []string wantValues *convertedValues wantErrMatch *errors.Template wantErrContains string @@ -591,11 +605,13 @@ func Test_convertValueObjects(t *testing.T) { auds: testAuds, certs: testCerts, scopes: testScopes, + maps: testClaimMaps, wantValues: &convertedValues{ - Algs: testSigningAlgs, - Auds: testAudiences, - Certs: testCertificates, - ClaimsScopes: testClaimsScopes, + Algs: testSigningAlgs, + Auds: testAudiences, + Certs: testCertificates, + ClaimsScopes: testClaimsScopes, + AccountClaimMaps: testAccountClaimMaps, }, }, { @@ -609,11 +625,12 @@ func Test_convertValueObjects(t *testing.T) { assert, require := assert.New(t), require.New(t) am := &AuthMethod{ AuthMethod: &store.AuthMethod{ - PublicId: tt.authMethodId, - SigningAlgs: tt.algs, - AudClaims: tt.auds, - Certificates: tt.certs, - ClaimsScopes: tt.scopes, + PublicId: tt.authMethodId, + SigningAlgs: tt.algs, + AudClaims: tt.auds, + Certificates: tt.certs, + ClaimsScopes: tt.scopes, + AccountClaimMaps: tt.maps, }, } @@ -649,6 +666,28 @@ func Test_convertValueObjects(t *testing.T) { assert.Equal(tt.wantValues.ClaimsScopes, convertedScopes) } + convertedMaps, err := am.convertAccountClaimMaps() + if tt.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.wantErrMatch, err), "wanted err %q and got: %+v", tt.wantErrMatch.Code, err) + } else { + want := make([]*AccountClaimMap, 0, len(tt.wantValues.AccountClaimMaps)) + for _, v := range tt.wantValues.AccountClaimMaps { + want = append(want, v.(*AccountClaimMap)) + } + got := make([]*AccountClaimMap, 0, len(convertedMaps)) + for _, v := range convertedMaps { + got = append(got, v.(*AccountClaimMap)) + } + sort.Slice(want, func(a, b int) bool { + return want[a].ToClaim < want[b].ToClaim + }) + sort.Slice(got, func(a, b int) bool { + return got[a].ToClaim < got[b].ToClaim + }) + assert.Equal(want, got) + } + values, err := am.convertValueObjects() if tt.wantErrMatch != nil { require.Error(err) @@ -659,7 +698,37 @@ func Test_convertValueObjects(t *testing.T) { return } require.NoError(err) + testSortConverted(t, tt.wantValues) + testSortConverted(t, values) assert.Equal(tt.wantValues, values) }) } } + +type converted []interface{} + +func (a converted) Len() int { return len(a) } +func (a converted) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a converted) Less(i, j int) bool { + switch a[i].(type) { + case *SigningAlg: + return a[i].(*SigningAlg).GetAlg() < a[j].(*SigningAlg).GetAlg() + case *Certificate: + return a[i].(*Certificate).GetCert() < a[j].(*Certificate).GetCert() + case *AccountClaimMap: + return a[i].(*AccountClaimMap).GetToClaim() < a[j].(*AccountClaimMap).GetToClaim() + case *AudClaim: + return a[i].(*AudClaim).GetAud() < a[j].(*AudClaim).GetAud() + case *ClaimsScope: + return a[i].(*ClaimsScope).GetScope() < a[j].(*ClaimsScope).GetScope() + } + return false +} + +func testSortConverted(t *testing.T, c *convertedValues) { + sort.Sort(converted(c.Algs)) + sort.Sort(converted(c.Certs)) + sort.Sort(converted(c.AccountClaimMaps)) + sort.Sort(converted(c.Auds)) + sort.Sort(converted(c.ClaimsScopes)) +} diff --git a/internal/auth/oidc/options.go b/internal/auth/oidc/options.go index fc4718ba0..87dd578e7 100644 --- a/internal/auth/oidc/options.go +++ b/internal/auth/oidc/options.go @@ -41,6 +41,7 @@ type options struct { withKeyId string withIssuer *url.URL withOperationalState AuthMethodState + withAccountClaimMap map[string]AccountToClaim } func getDefaultOptions() options { @@ -210,3 +211,10 @@ func WithOperationalState(state AuthMethodState) Option { o.withOperationalState = state } } + +// WithAccountClaimMap provides an option for specifying an Account Claim map. +func WithAccountClaimMap(acm map[string]AccountToClaim) Option { + return func(o *options) { + o.withAccountClaimMap = acm + } +} diff --git a/internal/auth/oidc/options_test.go b/internal/auth/oidc/options_test.go index a033462a3..5ea8fbe1c 100644 --- a/internal/auth/oidc/options_test.go +++ b/internal/auth/oidc/options_test.go @@ -183,4 +183,15 @@ func Test_GetOpts(t *testing.T) { opts := getOpts(WithOperationalState(ActivePublicState)) assert.Equal(ActivePublicState, opts.withOperationalState) }) + t.Run("WithAccountClaimMap", func(t *testing.T) { + assert := assert.New(t) + acm := map[string]AccountToClaim{ + "oid": ToSubClaim, + "display_name": ToNameClaim, + } + opts := getOpts(WithAccountClaimMap(acm)) + testOpts := getDefaultOptions() + testOpts.withAccountClaimMap = acm + assert.Equal(opts, testOpts) + }) } diff --git a/internal/auth/oidc/repository_auth_method.go b/internal/auth/oidc/repository_auth_method.go index 5024bbf37..653bb80de 100644 --- a/internal/auth/oidc/repository_auth_method.go +++ b/internal/auth/oidc/repository_auth_method.go @@ -32,13 +32,39 @@ func (r *Repository) upsertAccount(ctx context.Context, am *AuthMethod, IdTokenC if AccessTokenClaims == nil { return nil, errors.New(errors.InvalidParameter, op, "missing Access Token claims") } + + fromSub, fromName, fromEmail := string(ToSubClaim), string(ToNameClaim), string(ToEmailClaim) + if len(am.AccountClaimMaps) > 0 { + acms, err := ParseAccountClaimMaps(am.AccountClaimMaps...) + if err != nil { + return nil, errors.Wrap(err, op) + } + for from, to := range acms { + toClaim, err := ConvertToAccountToClaim(to) + if err != nil { + return nil, errors.Wrap(err, op) + } + switch toClaim { + case ToSubClaim: + fromSub = from + case ToEmailClaim: + fromEmail = from + case ToNameClaim: + fromName = from + default: + // should never happen, but including it just in case. + return nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("%s=%s is not a valid account claim map", from, to)) + } + } + } + var iss, sub string var ok bool if iss, ok = IdTokenClaims["iss"].(string); !ok { return nil, errors.New(errors.Unknown, op, "issuer is not present in ID Token, which should not be possible") } - if sub, ok = IdTokenClaims["sub"].(string); !ok { - return nil, errors.New(errors.Unknown, op, "subject is not present in ID Token, which should not be possible") + if sub, ok = IdTokenClaims[fromSub].(string); !ok { + return nil, errors.New(errors.Unknown, op, fmt.Sprintf("mapping 'claim' %s to account subject and it is not present in ID Token", fromSub)) } pubId, err := newAccountId(am.GetPublicId(), iss, sub) if err != nil { @@ -51,22 +77,22 @@ func (r *Repository) upsertAccount(ctx context.Context, am *AuthMethod, IdTokenC var foundEmail, foundName interface{} switch { - case AccessTokenClaims["name"] != nil: - foundName = AccessTokenClaims["name"] + case AccessTokenClaims[fromName] != nil: + foundName = AccessTokenClaims[fromName] columns, values = append(columns, "full_name"), append(values, foundName) - case IdTokenClaims["name"] != nil: - foundName = IdTokenClaims["name"] + case IdTokenClaims[fromName] != nil: + foundName = IdTokenClaims[fromName] columns, values = append(columns, "full_name"), append(values, foundName) default: conflictClauses = append(conflictClauses, "full_name = NULL") nullMasks = append(nullMasks, NameField) } switch { - case AccessTokenClaims["email"] != nil: - foundEmail = AccessTokenClaims["email"] + case AccessTokenClaims[fromEmail] != nil: + foundEmail = AccessTokenClaims[fromEmail] columns, values = append(columns, "email"), append(values, foundEmail) - case IdTokenClaims["email"] != nil: - foundEmail = IdTokenClaims["email"] + case IdTokenClaims[fromEmail] != nil: + foundEmail = IdTokenClaims[fromEmail] columns, values = append(columns, "email"), append(values, foundEmail) default: conflictClauses = append(conflictClauses, "email = NULL") diff --git a/internal/auth/oidc/repository_auth_method_create.go b/internal/auth/oidc/repository_auth_method_create.go index d5551360a..bd073750b 100644 --- a/internal/auth/oidc/repository_auth_method_create.go +++ b/internal/auth/oidc/repository_auth_method_create.go @@ -112,7 +112,13 @@ func (r *Repository) CreateAuthMethod(ctx context.Context, am *AuthMethod, opt . } msgs = append(msgs, scopesOplogMsgs...) } - + if len(vo.AccountClaimMaps) > 0 { + accountClaimMapsOplogMsgs := make([]*oplog.Message, 0, len(vo.AccountClaimMaps)) + if err := w.CreateItems(ctx, vo.AccountClaimMaps, db.NewOplogMsgs(&accountClaimMapsOplogMsgs)); err != nil { + return err + } + msgs = append(msgs, accountClaimMapsOplogMsgs...) + } metadata := am.oplog(oplog.OpType_OP_TYPE_CREATE) if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil { return errors.Wrap(err, op, errors.WithMsg("unable to write oplog")) diff --git a/internal/auth/oidc/repository_auth_method_create_test.go b/internal/auth/oidc/repository_auth_method_create_test.go index e246e037b..11ea6e5f2 100644 --- a/internal/auth/oidc/repository_auth_method_create_test.go +++ b/internal/auth/oidc/repository_auth_method_create_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/google/go-cmp/cmp" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/iam" @@ -14,6 +15,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" ) func TestRepository_CreateAuthMethod(t *testing.T) { @@ -59,6 +61,7 @@ func TestRepository_CreateAuthMethod(t *testing.T) { WithName("alice's restaurant"), WithDescription("it's a good place to eat"), WithClaimsScopes("email", "profile"), + WithAccountClaimMap(map[string]AccountToClaim{"display_name": ToNameClaim, "oid": ToSubClaim}), ) require.NoError(t, err) require.Equal(t, am.SigningAlgs, convertAlg(algs...)) @@ -92,6 +95,7 @@ func TestRepository_CreateAuthMethod(t *testing.T) { WithName("alice's restaurant with a twist"), WithDescription("it's an okay but kinda weird place to eat"), WithClaimsScopes("email", "profile"), + WithAccountClaimMap(map[string]AccountToClaim{"display_name": ToNameClaim, "oid": ToSubClaim}), ) require.NoError(t, err) require.Equal(t, am.SigningAlgs, convertAlg(algs...)) @@ -126,6 +130,7 @@ func TestRepository_CreateAuthMethod(t *testing.T) { WithName("alice's restaurant is bad"), WithDescription("their food is awful"), WithClaimsScopes("email", "profile"), + WithAccountClaimMap(map[string]AccountToClaim{"display_name": ToNameClaim, "oid": ToSubClaim}), ) require.NoError(t, err) require.Equal(t, am.SigningAlgs, convertAlg(algs...)) @@ -211,6 +216,14 @@ func TestRepository_CreateAuthMethod(t *testing.T) { err = db.TestVerifyOplog(t, rw, am.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)) require.NoErrorf(err, "unexpected error verifying oplog entry: %s", err) + + found, err := repo.LookupAuthMethod(ctx, am.PublicId) + require.NoError(err) + found.CreateTime = got.CreateTime + found.UpdateTime = got.UpdateTime + found.Version = got.Version + TestSortAuthMethods(t, []*AuthMethod{found, am}) + assert.Empty(cmp.Diff(found.AuthMethod, am.AuthMethod, protocmp.Transform())) }) } } diff --git a/internal/auth/oidc/repository_auth_method_read.go b/internal/auth/oidc/repository_auth_method_read.go index 8169409c6..e4d22a6b8 100644 --- a/internal/auth/oidc/repository_auth_method_read.go +++ b/internal/auth/oidc/repository_auth_method_read.go @@ -161,6 +161,9 @@ func (r *Repository) getAuthMethods(ctx context.Context, authMethodId string, sc if agg.ClaimsScopes != "" { am.ClaimsScopes = strings.Split(agg.ClaimsScopes, aggregateDelimiter) } + if agg.AccountClaimMaps != "" { + am.AccountClaimMaps = strings.Split(agg.AccountClaimMaps, aggregateDelimiter) + } authMethods = append(authMethods, &am) } return authMethods, nil @@ -191,6 +194,7 @@ type authMethodAgg struct { Auds string Certs string ClaimsScopes string + AccountClaimMaps string } // TableName returns the table name for gorm diff --git a/internal/auth/oidc/repository_auth_method_read_test.go b/internal/auth/oidc/repository_auth_method_read_test.go index c41e25d36..9bbe04654 100644 --- a/internal/auth/oidc/repository_auth_method_read_test.go +++ b/internal/auth/oidc/repository_auth_method_read_test.go @@ -28,12 +28,14 @@ func TestRepository_LookupAuthMethod(t *testing.T) { t, conn, databaseWrapper, org.PublicId, ActivePrivateState, "alice_rp", "alices-dogs-name", + WithAccountClaimMap(map[string]AccountToClaim{"oid": ToSubClaim, "display_name": ToNameClaim}), WithApiUrl(TestConvertToUrls(t, "https://alice-active-priv.com/callback")[0]), WithSigningAlgs(RS256)) amActivePub := TestAuthMethod( t, conn, databaseWrapper, org.PublicId, ActivePublicState, "alice_rp", "alices-dogs-name", + WithAccountClaimMap(map[string]AccountToClaim{"oid": ToSubClaim, "display_name": ToNameClaim}), WithApiUrl(TestConvertToUrls(t, "https://alice-active-pub.com/callback")[0]), WithSigningAlgs(RS256), WithClaimsScopes("email", "profile"), @@ -100,6 +102,12 @@ func TestRepository_LookupAuthMethod(t *testing.T) { return } require.NoError(err) + if tt.want != nil && tt.want.AccountClaimMaps != nil { + sort.Strings(tt.want.AccountClaimMaps) + } + if got != nil && got.AccountClaimMaps != nil { + sort.Strings(got.AccountClaimMaps) + } assert.EqualValues(tt.want, got) }) } @@ -232,6 +240,7 @@ func TestRepository_getAuthMethods(t *testing.T) { WithSigningAlgs(RS256, ES256), WithCertificates(cert1, cert2), WithClaimsScopes("email", "profile"), + WithAccountClaimMap(map[string]AccountToClaim{"oid": ToSubClaim, "display_name": ToNameClaim}), ) am1b := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp-2", "alices-cat-name", WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) diff --git a/internal/auth/oidc/repository_auth_method_test.go b/internal/auth/oidc/repository_auth_method_test.go index e9cc010a2..b9f78bc10 100644 --- a/internal/auth/oidc/repository_auth_method_test.go +++ b/internal/auth/oidc/repository_auth_method_test.go @@ -38,6 +38,14 @@ func Test_upsertAccount(t *testing.T) { WithApiUrl(TestConvertToUrls(t, "https://alice-active-priv.com/callback")[0]), WithSigningAlgs(RS256)) + amWithMapping := TestAuthMethod( + t, + conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice_rp", "fido", + WithAccountClaimMap(map[string]AccountToClaim{"oid": ToSubClaim}), + WithApiUrl(TestConvertToUrls(t, "https://alice-active-priv.com/callback")[0]), + WithSigningAlgs(RS256)) + tests := []struct { name string am *AuthMethod @@ -84,6 +92,17 @@ func Test_upsertAccount(t *testing.T) { FullName: "alice eve-smith", }}, }, + { + name: "success-map-idTk", + am: amWithMapping, + idClaims: map[string]interface{}{"iss": "https://alice-active-priv.com", "sub": "success-defaults", "oid": "success-map"}, + atClaims: map[string]interface{}{}, + wantAcct: &Account{Account: &store.Account{ + AuthMethodId: amWithMapping.PublicId, + Issuer: "https://alice-active-priv.com", + Subject: "success-map", + }}, + }, { name: "non-existent-auth-method-scope-id", am: func() *AuthMethod { cp := amActivePriv.Clone(); cp.ScopeId = "non-existent-scope-id"; return cp }(), @@ -130,7 +149,7 @@ func Test_upsertAccount(t *testing.T) { idClaims: map[string]interface{}{"iss": "https://alice.com"}, atClaims: map[string]interface{}{}, wantErrMatch: errors.T(errors.Unknown), - wantErrContains: "subject is not present", + wantErrContains: "to account subject and it is not present in ID Token", }, { name: "missing-id-token-claims", diff --git a/internal/auth/oidc/repository_auth_method_update.go b/internal/auth/oidc/repository_auth_method_update.go index 1a483c200..e6af61ef1 100644 --- a/internal/auth/oidc/repository_auth_method_update.go +++ b/internal/auth/oidc/repository_auth_method_update.go @@ -32,6 +32,7 @@ const ( AudClaimsField = "AudClaims" CertificatesField = "Certificates" ClaimsScopesField = "ClaimsScopes" + AccountClaimMapsField = "AccountClaimMaps" ) // UpdateAuthMethod will retrieve the auth method from the repository, @@ -89,17 +90,18 @@ func (r *Repository) UpdateAuthMethod(ctx context.Context, am *AuthMethod, versi dbMask, nullFields := dbcommon.BuildUpdatePaths( map[string]interface{}{ - NameField: am.Name, - DescriptionField: am.Description, - IssuerField: am.Issuer, - ClientIdField: am.ClientId, - ClientSecretField: am.ClientSecret, - MaxAgeField: am.MaxAge, - SigningAlgsField: am.SigningAlgs, - ApiUrlField: am.ApiUrl, - AudClaimsField: am.AudClaims, - CertificatesField: am.Certificates, - ClaimsScopesField: am.ClaimsScopes, + NameField: am.Name, + DescriptionField: am.Description, + IssuerField: am.Issuer, + ClientIdField: am.ClientId, + ClientSecretField: am.ClientSecret, + MaxAgeField: am.MaxAge, + SigningAlgsField: am.SigningAlgs, + ApiUrlField: am.ApiUrl, + AudClaimsField: am.AudClaims, + CertificatesField: am.Certificates, + ClaimsScopesField: am.ClaimsScopes, + AccountClaimMapsField: am.AccountClaimMaps, }, fieldMaskPaths, nil, @@ -164,10 +166,30 @@ func (r *Repository) UpdateAuthMethod(ctx context.Context, am *AuthMethod, versi return nil, db.NoRowsAffected, errors.Wrap(err, op) } + addMaps, deleteMaps, err := valueObjectChanges(origAm.PublicId, AccountClaimMapsVO, am.AccountClaimMaps, origAm.AccountClaimMaps, dbMask, nullFields) + if err != nil { + return nil, db.NoRowsAffected, errors.Wrap(err, op) + } + // we don't allow updates for "sub" claim maps, because we have no way to + // determine if the updated "from" claim in the map might create collisions + // with any existing account's subject. + for _, rawCm := range addMaps { + cm := rawCm.(*AccountClaimMap) + if cm.ToClaim == string(ToSubClaim) { + return nil, db.NoRowsAffected, errors.New(errors.InvalidParameter, op, fmt.Sprintf("you cannot update account claim map %s=%s for the \"sub\" claim", cm.FromClaim, cm.ToClaim)) + } + } + for _, rawCm := range deleteMaps { + cm := rawCm.(*AccountClaimMap) + if cm.ToClaim == string(ToSubClaim) { + return nil, db.NoRowsAffected, errors.New(errors.InvalidParameter, op, fmt.Sprintf("you cannot update account claim map %s=%s for the \"sub\" claim", cm.FromClaim, cm.ToClaim)) + } + } + var filteredDbMask, filteredNullFields []string for _, f := range dbMask { switch f { - case SigningAlgsField, AudClaimsField, CertificatesField, ClaimsScopesField: + case SigningAlgsField, AudClaimsField, CertificatesField, ClaimsScopesField, AccountClaimMapsField: continue default: filteredDbMask = append(filteredDbMask, f) @@ -175,7 +197,7 @@ func (r *Repository) UpdateAuthMethod(ctx context.Context, am *AuthMethod, versi } for _, f := range nullFields { switch f { - case SigningAlgsField, AudClaimsField, CertificatesField, ClaimsScopesField: + case SigningAlgsField, AudClaimsField, CertificatesField, ClaimsScopesField, AccountClaimMapsField: continue default: filteredNullFields = append(filteredNullFields, f) @@ -192,7 +214,9 @@ func (r *Repository) UpdateAuthMethod(ctx context.Context, am *AuthMethod, versi len(addAuds) == 0 && len(deleteAuds) == 0 && len(addScopes) == 0 && - len(deleteScopes) == 0 { + len(deleteScopes) == 0 && + len(addMaps) == 0 && + len(deleteMaps) == 0 { return origAm, db.NoRowsAffected, nil } @@ -336,6 +360,25 @@ func (r *Repository) UpdateAuthMethod(ctx context.Context, am *AuthMethod, versi msgs = append(msgs, addScopesOplogMsgs...) } + if len(deleteMaps) > 0 { + deleteMapsOplogMsgs := make([]*oplog.Message, 0, len(deleteMaps)) + rowsDeleted, err := w.DeleteItems(ctx, deleteMaps, db.NewOplogMsgs(&deleteMapsOplogMsgs)) + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to delete account claim maps")) + } + if rowsDeleted != len(deleteMaps) { + return errors.New(errors.MultipleRecords, op, fmt.Sprintf("account claim maps deleted %d did not match request for %d", rowsDeleted, len(deleteMaps))) + } + msgs = append(msgs, deleteMapsOplogMsgs...) + } + if len(addMaps) > 0 { + addMapsOplogMsgs := make([]*oplog.Message, 0, len(addMaps)) + if err := w.CreateItems(ctx, addMaps, db.NewOplogMsgs(&addMapsOplogMsgs)); err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to add account claim maps")) + } + msgs = append(msgs, addMapsOplogMsgs...) + } + metadata := updatedAm.oplog(oplog.OpType_OP_TYPE_UPDATE) if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil { return errors.Wrap(err, op, errors.WithMsg("unable to write oplog")) @@ -371,16 +414,17 @@ func (r *Repository) UpdateAuthMethod(ctx context.Context, am *AuthMethod, versi type voName string const ( - SigningAlgVO voName = "SigningAlgs" - CertificateVO voName = "Certificates" - AudClaimVO voName = "AudClaims" - ClaimsScopesVO voName = "ClaimsScopes" + SigningAlgVO voName = "SigningAlgs" + CertificateVO voName = "Certificates" + AudClaimVO voName = "AudClaims" + ClaimsScopesVO voName = "ClaimsScopes" + AccountClaimMapsVO voName = "AccountClaimMaps" ) // validVoName decides if the name is valid func validVoName(name voName) bool { switch name { - case SigningAlgVO, CertificateVO, AudClaimVO, ClaimsScopesVO: + case SigningAlgVO, CertificateVO, AudClaimVO, ClaimsScopesVO, AccountClaimMapsVO: return true default: return false @@ -408,6 +452,25 @@ var supportedFactories = map[voName]factoryFunc{ str := fmt.Sprintf("%s", i) return NewClaimsScope(publicId, str) }, + AccountClaimMapsVO: func(publicId string, i interface{}) (interface{}, error) { + const op = "oidc.AccountClaimMapsFactory" + str := fmt.Sprintf("%s", i) + acm, err := ParseAccountClaimMaps(str) + if err != nil { + return nil, err + } + if len(acm) > 1 { + return nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("unable to parse account claim map %s", str)) + } + var from, rawTo string + for from, rawTo = range acm { + } + to, err := ConvertToAccountToClaim(rawTo) + if err != nil { + return nil, errors.Wrap(err, op) + } + return NewAccountClaimMap(publicId, from, to) + }, } // valueObjectChanges takes the new and old list of VOs (value objects) and @@ -455,38 +518,38 @@ func valueObjectChanges( var deletes []interface{} if strutil.StrListContains(nullFields, string(valueObjectName)) { deletes = make([]interface{}, 0, len(oldVOs)) - for _, a := range oldVOs { - alg, err := factory(publicId, a) + for _, v := range oldVOs { + deleteObj, err := factory(publicId, v) if err != nil { return nil, nil, errors.Wrap(err, op) } - deletes = append(deletes, alg) - delete(foundVOs, a) + deletes = append(deletes, deleteObj) + delete(foundVOs, v) } } if strutil.StrListContains(dbMask, string(valueObjectName)) { adds = make([]interface{}, 0, len(newVOs)) - for _, a := range newVOs { - if _, ok := foundVOs[a]; ok { - delete(foundVOs, a) + for _, v := range newVOs { + if _, ok := foundVOs[v]; ok { + delete(foundVOs, v) continue } - alg, err := factory(publicId, a) + obj, err := factory(publicId, v) if err != nil { return nil, nil, errors.Wrap(err, op) } - adds = append(adds, alg) - delete(foundVOs, a) + adds = append(adds, obj) + delete(foundVOs, v) } } if len(foundVOs) > 0 { - for a := range foundVOs { - alg, err := factory(publicId, a) + for v := range foundVOs { + obj, err := factory(publicId, v) if err != nil { return nil, nil, errors.Wrap(err, op) } - deletes = append(deletes, alg) - delete(foundVOs, a) + deletes = append(deletes, obj) + delete(foundVOs, v) } } return adds, deletes, nil @@ -508,6 +571,7 @@ func validateFieldMask(fieldMaskPaths []string) error { case strings.EqualFold(AudClaimsField, f): case strings.EqualFold(CertificatesField, f): case strings.EqualFold(ClaimsScopesField, f): + case strings.EqualFold(AccountClaimMapsField, f): default: return errors.New(errors.InvalidParameter, op, fmt.Sprintf("invalid field mask: %s", f)) } @@ -566,6 +630,14 @@ func applyUpdate(new, orig *AuthMethod, fieldMaskPaths []string) *AuthMethod { cp.ClaimsScopes = make([]string, 0, len(new.ClaimsScopes)) cp.ClaimsScopes = append(cp.ClaimsScopes, new.ClaimsScopes...) } + case AccountClaimMapsField: + switch { + case len(new.AccountClaimMaps) == 0: + cp.AccountClaimMaps = nil + default: + cp.AccountClaimMaps = make([]string, 0, len(new.AccountClaimMaps)) + cp.AccountClaimMaps = append(cp.AccountClaimMaps, new.AccountClaimMaps...) + } } } return cp diff --git a/internal/auth/oidc/repository_auth_method_update_test.go b/internal/auth/oidc/repository_auth_method_update_test.go index 435c0e825..7801c8c05 100644 --- a/internal/auth/oidc/repository_auth_method_update_test.go +++ b/internal/auth/oidc/repository_auth_method_update_test.go @@ -68,6 +68,7 @@ func Test_UpdateAuthMethod(t *testing.T) { WithCertificates(tpCert[0]), WithSigningAlgs(Alg(tpAlg)), WithClaimsScopes("email", "profile"), + WithAccountClaimMap(map[string]AccountToClaim{"display_name": "name"}), WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), ) }, @@ -78,9 +79,10 @@ func Test_UpdateAuthMethod(t *testing.T) { am.Description = "the best place to eat" am.AudClaims = []string{"www.alice.com", "www.alice.com/admin"} am.ClientSecret = "This is a new secret" + am.AccountClaimMaps = []string{"preferred_name=name"} return &am }, - fieldMasks: []string{NameField, DescriptionField, AudClaimsField, ClientSecretField}, + fieldMasks: []string{NameField, DescriptionField, AudClaimsField, ClientSecretField, AccountClaimMapsField}, version: 1, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.Clone() @@ -90,6 +92,7 @@ func Test_UpdateAuthMethod(t *testing.T) { am.ClientSecret = updateWith.ClientSecret am.CtClientSecret = updateWith.CtClientSecret am.ClientSecretHmac = updateWith.ClientSecretHmac + am.AccountClaimMaps = updateWith.AccountClaimMaps return am }, }, @@ -368,6 +371,34 @@ func Test_UpdateAuthMethod(t *testing.T) { return am }, }, + { + name: "attempt-to-update-sub-account-claim-map", + setup: func() *AuthMethod { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + InactiveState, + "alice-rp", "alice-secret", + WithCertificates(tpCert[0]), + WithSigningAlgs(Alg(tpAlg)), + WithClaimsScopes("email", "profile"), + WithAccountClaimMap(map[string]AccountToClaim{"oid": "sub"}), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + }, + updateWith: func(orig *AuthMethod) *AuthMethod { + am := AllocAuthMethod() + am.PublicId = orig.PublicId + am.AccountClaimMaps = []string{"uid=sub"} + return &am + }, + fieldMasks: []string{AccountClaimMapsField}, + version: 1, + wantErrMatch: errors.T(errors.InvalidParameter), + }, { name: "nil-authMethod", setup: func() *AuthMethod { return nil }, @@ -548,6 +579,10 @@ func Test_UpdateAuthMethod(t *testing.T) { err = db.TestVerifyOplog(t, rw, updateWith.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) require.NoErrorf(err, "unexpected error verifying oplog entry: %s", err) } + found, err := repo.LookupAuthMethod(ctx, want.PublicId) + require.NoError(err) + TestSortAuthMethods(t, []*AuthMethod{found}) + assert.Empty(cmp.Diff(found.AuthMethod, want.AuthMethod, protocmp.Transform())) } }) } diff --git a/internal/auth/oidc/store/oidc.pb.go b/internal/auth/oidc/store/oidc.pb.go index b33b0de74..b588273d6 100644 --- a/internal/auth/oidc/store/oidc.pb.go +++ b/internal/auth/oidc/store/oidc.pb.go @@ -117,6 +117,12 @@ type AuthMethod struct { // are operatated on as a complete set. // @inject_tag: `gorm:"-"` ClaimsScopes []string `protobuf:"bytes,200,rep,name=claims_scopes,json=claimsScopes,proto3" json:"claims_scopes,omitempty" gorm:"-"` + // account_claim_maps are optional claim maps from custom claims to the + // standard claims of sub, name and email. These maps are represented as + // key=value where the key equals the from_claim and the value equals the + // to_claim. For example "oid=sub". + // @inject_tag: `gorm:"-"` + AccountClaimMaps []string `protobuf:"bytes,210,rep,name=account_claim_maps,json=accountClaimMaps,proto3" json:"account_claim_maps,omitempty" gorm:"-"` } func (x *AuthMethod) Reset() { @@ -305,6 +311,13 @@ func (x *AuthMethod) GetClaimsScopes() []string { return nil } +func (x *AuthMethod) GetAccountClaimMaps() []string { + if x != nil { + return x.AccountClaimMaps + } + return nil +} + // 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. @@ -735,6 +748,87 @@ func (x *ClaimsScope) GetCreateTime() *timestamp.Timestamp { return nil } +// AccountClaimMap entries are optional from/to account claim maps. +type AccountClaimMap struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // @inject_tag: `gorm:"primary_key"` + OidcMethodId string `protobuf:"bytes,10,opt,name=oidc_method_id,json=oidcMethodId,proto3" json:"oidc_method_id,omitempty" gorm:"primary_key"` + // from_claim is the claim from the id_token that you need to map to a + // standard account claim. + // @inject_tag: `gorm:"not_null"` + FromClaim string `protobuf:"bytes,20,opt,name=from_claim,json=fromClaim,proto3" json:"from_claim,omitempty" gorm:"not_null"` + // to_claim is the standard account claim to map the from_claim to. Valid + // values are: sub, name, email + // @inject_tag: `gorm:"column:to_claim;primary_key"` + ToClaim string `protobuf:"bytes,30,opt,name=to_claim,json=toClaim,proto3" json:"to_claim,omitempty" gorm:"column:to_claim;primary_key"` + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,40,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` +} + +func (x *AccountClaimMap) Reset() { + *x = AccountClaimMap{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AccountClaimMap) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AccountClaimMap) ProtoMessage() {} + +func (x *AccountClaimMap) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AccountClaimMap.ProtoReflect.Descriptor instead. +func (*AccountClaimMap) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDescGZIP(), []int{6} +} + +func (x *AccountClaimMap) GetOidcMethodId() string { + if x != nil { + return x.OidcMethodId + } + return "" +} + +func (x *AccountClaimMap) GetFromClaim() string { + if x != nil { + return x.FromClaim + } + return "" +} + +func (x *AccountClaimMap) GetToClaim() string { + if x != nil { + return x.ToClaim + } + return "" +} + +func (x *AccountClaimMap) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + var File_controller_storage_auth_oidc_store_v1_oidc_proto protoreflect.FileDescriptor var file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDesc = []byte{ @@ -751,7 +845,7 @@ var file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDesc = []byte{ 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x9d, 0x0a, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x4d, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x83, 0x0b, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, @@ -833,78 +927,96 @@ var file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDesc = []byte{ 0x0a, 0x0c, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0x18, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x52, 0x0c, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, - 0x53, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x22, 0xcd, 0x03, 0x0a, 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, - 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, - 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x14, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, - 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, - 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, + 0x53, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0x64, 0x0a, 0x12, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x5f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x5f, 0x6d, 0x61, 0x70, 0x73, 0x18, 0xd2, 0x01, 0x20, + 0x03, 0x28, 0x09, 0x42, 0x35, 0xc2, 0xdd, 0x29, 0x31, 0x0a, 0x10, 0x41, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x4d, 0x61, 0x70, 0x73, 0x12, 0x1d, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, + 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x5f, 0x6d, 0x61, 0x70, 0x73, 0x52, 0x10, 0x61, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x4d, 0x61, 0x70, 0x73, 0x22, 0xcd, 0x03, 0x0a, + 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, + 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, + 0x74, 0x69, 0x6d, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, + 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, + 0x24, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x42, 0x10, 0xc2, + 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0xc2, 0xdd, 0x29, 0x1a, + 0x0a, 0x0b, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x5f, 0x69, 0x64, 0x18, 0x46, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x75, 0x74, 0x68, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, + 0x72, 0x18, 0x50, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, + 0x18, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x75, 0x6c, + 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x75, + 0x6c, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, + 0x6e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x91, 0x01, 0x0a, + 0x0a, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x41, 0x6c, 0x67, 0x12, 0x24, 0x0a, 0x0e, 0x6f, + 0x69, 0x64, 0x63, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x69, 0x64, 0x63, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, + 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x67, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x61, 0x6c, 0x67, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, + 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, + 0x22, 0x8f, 0x01, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x24, 0x0a, + 0x0e, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x69, 0x64, 0x63, 0x4d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x75, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x61, 0x75, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, + 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, + 0x6d, 0x65, 0x22, 0x94, 0x01, 0x0a, 0x0b, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x69, 0x64, 0x63, + 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x65, 0x72, 0x74, + 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x65, 0x72, 0x74, 0x12, 0x4b, 0x0a, 0x0b, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x42, 0x10, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x4e, - 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x40, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x32, - 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x44, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x3c, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x61, - 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x46, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, - 0x64, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x18, 0x50, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, - 0x65, 0x63, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x75, 0x6c, 0x6c, 0x4e, 0x61, 0x6d, 0x65, - 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x91, 0x01, 0x0a, 0x0a, 0x53, 0x69, 0x67, 0x6e, 0x69, - 0x6e, 0x67, 0x41, 0x6c, 0x67, 0x12, 0x24, 0x0a, 0x0e, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x6d, 0x65, - 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, - 0x69, 0x64, 0x63, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, - 0x6c, 0x67, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x61, 0x6c, 0x67, 0x12, 0x4b, 0x0a, - 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, - 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, - 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x8f, 0x01, 0x0a, 0x08, 0x41, - 0x75, 0x64, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x24, 0x0a, 0x0e, 0x6f, 0x69, 0x64, 0x63, 0x5f, - 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0c, 0x6f, 0x69, 0x64, 0x63, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x10, 0x0a, - 0x03, 0x61, 0x75, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x61, 0x75, 0x64, 0x12, - 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, - 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x94, 0x01, 0x0a, - 0x0b, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x24, 0x0a, 0x0e, - 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0a, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x69, 0x64, 0x63, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, - 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x65, 0x72, 0x74, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x63, 0x65, 0x72, 0x74, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, + 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x96, 0x01, 0x0a, 0x0b, 0x43, 0x6c, + 0x61, 0x69, 0x6d, 0x73, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x6f, 0x69, 0x64, + 0x63, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x6f, 0x69, 0x64, 0x63, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, + 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x73, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, + 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, + 0x6d, 0x65, 0x22, 0xbe, 0x01, 0x0a, 0x0f, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x43, 0x6c, + 0x61, 0x69, 0x6d, 0x4d, 0x61, 0x70, 0x12, 0x24, 0x0a, 0x0e, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x6d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, + 0x6f, 0x69, 0x64, 0x63, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, + 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x66, 0x72, 0x6f, 0x6d, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x19, 0x0a, 0x08, 0x74, + 0x6f, 0x5f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x74, + 0x6f, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, - 0x69, 0x6d, 0x65, 0x22, 0x96, 0x01, 0x0a, 0x0b, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x53, 0x63, - 0x6f, 0x70, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x6d, 0x65, 0x74, 0x68, - 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x69, 0x64, - 0x63, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, - 0x70, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x12, - 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, - 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x42, 0x3e, 0x5a, 0x3c, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, - 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x6f, 0x69, 0x64, 0x63, - 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x69, 0x6d, 0x65, 0x42, 0x3e, 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, + 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x75, + 0x74, 0x68, 0x2f, 0x6f, 0x69, 0x64, 0x63, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, + 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -919,7 +1031,7 @@ func file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDescGZIP() []byte return file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDescData } -var file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_controller_storage_auth_oidc_store_v1_oidc_proto_goTypes = []interface{}{ (*AuthMethod)(nil), // 0: controller.storage.auth.oidc.store.v1.AuthMethod (*Account)(nil), // 1: controller.storage.auth.oidc.store.v1.Account @@ -927,22 +1039,24 @@ var file_controller_storage_auth_oidc_store_v1_oidc_proto_goTypes = []interface{ (*AudClaim)(nil), // 3: controller.storage.auth.oidc.store.v1.AudClaim (*Certificate)(nil), // 4: controller.storage.auth.oidc.store.v1.Certificate (*ClaimsScope)(nil), // 5: controller.storage.auth.oidc.store.v1.ClaimsScope - (*timestamp.Timestamp)(nil), // 6: controller.storage.timestamp.v1.Timestamp + (*AccountClaimMap)(nil), // 6: controller.storage.auth.oidc.store.v1.AccountClaimMap + (*timestamp.Timestamp)(nil), // 7: controller.storage.timestamp.v1.Timestamp } var file_controller_storage_auth_oidc_store_v1_oidc_proto_depIdxs = []int32{ - 6, // 0: controller.storage.auth.oidc.store.v1.AuthMethod.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 1: controller.storage.auth.oidc.store.v1.AuthMethod.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 2: controller.storage.auth.oidc.store.v1.Account.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 3: controller.storage.auth.oidc.store.v1.Account.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 4: controller.storage.auth.oidc.store.v1.SigningAlg.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 5: controller.storage.auth.oidc.store.v1.AudClaim.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 6: controller.storage.auth.oidc.store.v1.Certificate.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // 7: controller.storage.auth.oidc.store.v1.ClaimsScope.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 8, // [8:8] is the sub-list for method output_type - 8, // [8:8] is the sub-list for method input_type - 8, // [8:8] is the sub-list for extension type_name - 8, // [8:8] is the sub-list for extension extendee - 0, // [0:8] is the sub-list for field type_name + 7, // 0: controller.storage.auth.oidc.store.v1.AuthMethod.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 7, // 1: controller.storage.auth.oidc.store.v1.AuthMethod.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 7, // 2: controller.storage.auth.oidc.store.v1.Account.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 7, // 3: controller.storage.auth.oidc.store.v1.Account.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 7, // 4: controller.storage.auth.oidc.store.v1.SigningAlg.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 7, // 5: controller.storage.auth.oidc.store.v1.AudClaim.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 7, // 6: controller.storage.auth.oidc.store.v1.Certificate.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 7, // 7: controller.storage.auth.oidc.store.v1.ClaimsScope.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 7, // 8: controller.storage.auth.oidc.store.v1.AccountClaimMap.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 9, // [9:9] is the sub-list for method output_type + 9, // [9:9] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name } func init() { file_controller_storage_auth_oidc_store_v1_oidc_proto_init() } @@ -1023,6 +1137,18 @@ func file_controller_storage_auth_oidc_store_v1_oidc_proto_init() { return nil } } + file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AccountClaimMap); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -1030,7 +1156,7 @@ func file_controller_storage_auth_oidc_store_v1_oidc_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDesc, NumEnums: 0, - NumMessages: 6, + NumMessages: 7, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/auth/oidc/testing.go b/internal/auth/oidc/testing.go index dc9a45f05..62622083a 100644 --- a/internal/auth/oidc/testing.go +++ b/internal/auth/oidc/testing.go @@ -108,7 +108,16 @@ func TestAuthMethod( require.NoError(err) require.Equal(len(opts.withClaimsScopes), len(authMethod.ClaimsScopes)) } - + if len(opts.withAccountClaimMap) > 0 { + newAccountClaimMaps := make([]interface{}, 0, len(opts.withAccountClaimMap)) + for k, v := range opts.withAccountClaimMap { + acm, err := NewAccountClaimMap(authMethod.PublicId, k, v) + require.NoError(err) + newAccountClaimMaps = append(newAccountClaimMaps, acm) + } + require.NoError(rw.CreateItems(ctx, newAccountClaimMaps)) + require.Equal(len(opts.withAccountClaimMap), len(authMethod.AccountClaimMaps)) + } authMethod.OperationalState = string(state) rowsUpdated, err := rw.Update(ctx, authMethod, []string{OperationalStateField}, nil) require.NoError(err) @@ -140,6 +149,9 @@ func TestSortAuthMethods(t *testing.T, methods []*AuthMethod) { sort.Slice(am.ClaimsScopes, func(a, b int) bool { return am.ClaimsScopes[a] < am.ClaimsScopes[b] }) + sort.Slice(am.AccountClaimMaps, func(a, b int) bool { + return am.AccountClaimMaps[a] < am.AccountClaimMaps[b] + }) } } diff --git a/internal/cmd/commands/authmethodscmd/oidc_funcs.go b/internal/cmd/commands/authmethodscmd/oidc_funcs.go index 43ad54535..c1c842fe9 100644 --- a/internal/cmd/commands/authmethodscmd/oidc_funcs.go +++ b/internal/cmd/commands/authmethodscmd/oidc_funcs.go @@ -27,6 +27,7 @@ type extraOidcCmdVars struct { flagIdpCaCerts []string flagAllowedAudiences []string flagClaimsScopes []string + flagAccountClaimMaps []string flagDisableDiscoveredConfigValidation bool flagDryRun bool } @@ -42,6 +43,7 @@ const ( caCertFlagName = "idp-ca-cert" allowedAudienceFlagName = "allowed-audience" claimsScopes = "claims-scopes" + accountClaimMaps = "account-claim-maps" stateFlagName = "state" disableDiscoveredConfigValidationFlagName = "disable-discovered-config-validation" dryRunFlagName = "dry-run" @@ -59,6 +61,7 @@ func extraOidcActionsFlagsMapFuncImpl() map[string][]string { caCertFlagName, allowedAudienceFlagName, claimsScopes, + accountClaimMaps, }, "change-state": { idFlagName, @@ -129,6 +132,12 @@ func extraOidcFlagsFuncImpl(c *OidcCommand, set *base.FlagSets, _ *base.FlagSet) Target: &c.flagClaimsScopes, Usage: `The optional claims scope requested. May be specified multiple times.`, }) + case accountClaimMaps: + f.StringSliceVar(&base.StringSliceVar{ + Name: accountClaimMaps, + Target: &c.flagAccountClaimMaps, + Usage: `The optional account claim maps from custom claims to the standard claims of sub, name and email. These maps are represented as key=value where the key equals the Provider from-claim and the value equals the Boundary to-claim. For example "oid=sub". May be specified multiple times for different to-claims.`, + }) case stateFlagName: f.StringVar(&base.StringVar{ Name: stateFlagName, @@ -258,6 +267,13 @@ func extraOidcFlagHandlingFuncImpl(c *OidcCommand, f *base.FlagSets, opts *[]aut default: *opts = append(*opts, authmethods.WithOidcAuthMethodClaimsScopes(c.flagClaimsScopes)) } + switch { + case len(c.flagAccountClaimMaps) == 0: + case len(c.flagAccountClaimMaps) == 1 && c.flagAccountClaimMaps[0] == "null": + *opts = append(*opts, authmethods.DefaultOidcAuthMethodAccountClaimMaps()) + default: + *opts = append(*opts, authmethods.WithOidcAuthMethodAccountClaimMaps(c.flagAccountClaimMaps)) + } if c.flagDisableDiscoveredConfigValidation { *opts = append(*opts, authmethods.WithOidcAuthMethodDisableDiscoveredConfigValidation(c.flagDisableDiscoveredConfigValidation)) } diff --git a/internal/db/schema/migrations/postgres/6/01_oidc.up.sql b/internal/db/schema/migrations/postgres/6/01_oidc.up.sql new file mode 100644 index 000000000..aff56dddc --- /dev/null +++ b/internal/db/schema/migrations/postgres/6/01_oidc.up.sql @@ -0,0 +1,94 @@ +begin; + +-- auth_oidc_account_claim_map entries are the optional claim maps from custom +-- claims to the standard claims of sub, name and email. There can be 0 or more +-- for each parent oidc auth method. +create table auth_oidc_account_claim_map ( + create_time wt_timestamp, + oidc_method_id wt_public_id + constraint auth_oidc_method_fkey + references auth_oidc_method(public_id) + on delete cascade + on update cascade, + from_claim text not null + constraint from_claim_must_not_be_empty + check(length(trim(from_claim)) > 0) + constraint from_claim_must_be_less_than_1024_chars + check(length(trim(from_claim)) < 1024), + to_claim text not null + constraint to_claim_valid_values + check (to_claim in ('sub', 'name', 'email')), -- intentionally case-sensitive matching + primary key(oidc_method_id, to_claim) +); +comment on table auth_oidc_account_claim_map is +'auth_oidc_account_claim_map entries are the optional claim maps from custom claims to the standard claims of sub, name and email. There can be 0 or more for each parent oidc auth method.'; + + +create trigger + default_create_time_column +before +insert on auth_oidc_account_claim_map + for each row execute procedure default_create_time(); + + +create trigger + immutable_columns +before +update on auth_oidc_account_claim_map + for each row execute procedure immutable_columns('oidc_method_id', 'from_claim', 'to_claim', 'create_time'); + + +-- we will drop the oidc_auth_method_with_value_obj view, so we can recreate it +-- and add the oidc claim's scopes to the returned set. +drop view oidc_auth_method_with_value_obj; + +-- oidc_auth_method_with_value_obj is useful for reading an oidc auth method +-- with its associated value objects (algs, auds, certs, claims scopes and +-- account claim maps) 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, + -- 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 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 +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) as columns with | delimited values'; + + + commit; diff --git a/internal/db/schema/postgres_migration.gen.go b/internal/db/schema/postgres_migration.gen.go index 82c7ce708..2e338b4ad 100644 --- a/internal/db/schema/postgres_migration.gen.go +++ b/internal/db/schema/postgres_migration.gen.go @@ -4,7 +4,7 @@ package schema func init() { migrationStates["postgres"] = migrationState{ - binarySchemaVersion: 5001, + binarySchemaVersion: 6001, upMigrations: map[int][]byte{ 1: []byte(` create domain wt_public_id as text @@ -6087,6 +6087,97 @@ from 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) as columns with | delimited values'; +`), + 6001: []byte(` +-- auth_oidc_account_claim_map entries are the optional claim maps from custom +-- claims to the standard claims of sub, name and email. There can be 0 or more +-- for each parent oidc auth method. +create table auth_oidc_account_claim_map ( + create_time wt_timestamp, + oidc_method_id wt_public_id + constraint auth_oidc_method_fkey + references auth_oidc_method(public_id) + on delete cascade + on update cascade, + from_claim text not null + constraint from_claim_must_not_be_empty + check(length(trim(from_claim)) > 0) + constraint from_claim_must_be_less_than_1024_chars + check(length(trim(from_claim)) < 1024), + to_claim text not null + constraint to_claim_valid_values + CHECK (to_claim IN ('sub', 'name', 'email')), -- intentionally case-sensitive matching + primary key(oidc_method_id, to_claim) +); +comment on table auth_oidc_account_claim_map is +'auth_oidc_account_claim_map entries are the optional claim maps from custom claims to the standard claims of sub, name and email. There can be 0 or more for each parent oidc auth method.'; + + +create trigger + default_create_time_column +before +insert on auth_oidc_account_claim_map + for each row execute procedure default_create_time(); + + +create trigger + immutable_columns +before +update on auth_oidc_account_claim_map + for each row execute procedure immutable_columns('oidc_method_id', 'from_claim', 'to_claim', 'create_time'); + + + -- we will drop the oidc_auth_method_with_value_obj view, so we can recreate it +-- and add the oidc claim's scopes to the returned set. +drop view oidc_auth_method_with_value_obj; + +-- oidc_auth_method_with_value_obj is useful for reading an oidc auth method +-- with its associated value objects (algs, auds, certs, claims scopes and +-- account claim maps) 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, + -- 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 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 +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) as columns with | delimited values'; `), }, } diff --git a/internal/gen/controller/api/resources/authmethods/auth_method.pb.go b/internal/gen/controller/api/resources/authmethods/auth_method.pb.go index 71926c221..335435607 100644 --- a/internal/gen/controller/api/resources/authmethods/auth_method.pb.go +++ b/internal/gen/controller/api/resources/authmethods/auth_method.pb.go @@ -285,6 +285,11 @@ type OidcAuthMethodAttributes struct { // Optional claims scopes that will be requested during authentication. // see: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims ClaimsScopes []string `protobuf:"bytes,112,rep,name=claims_scopes,proto3" json:"claims_scopes,omitempty"` + // account_claim_maps are optional claim maps from custom claims to the + // standard claims of sub, name and email. These maps are represented as + // key=value where the key equals the from_claim and the value equals the + // to_claim. For example "oid=sub". + AccountClaimMaps []string `protobuf:"bytes,113,rep,name=account_claim_maps,proto3" json:"account_claim_maps,omitempty"` // If the Authorization Server's discovered configuration contains values // that do not match the configuration set on this auth method, this can be // set to force the local configuration to override the discovered values. @@ -419,6 +424,13 @@ func (x *OidcAuthMethodAttributes) GetClaimsScopes() []string { return nil } +func (x *OidcAuthMethodAttributes) GetAccountClaimMaps() []string { + if x != nil { + return x.AccountClaimMaps + } + return nil +} + func (x *OidcAuthMethodAttributes) GetDisableDiscoveredConfigValidation() bool { if x != nil { return x.DisableDiscoveredConfigValidation @@ -815,7 +827,7 @@ var file_controller_api_resources_authmethods_v1_auth_method_proto_rawDesc = []b 0x6e, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, 0x11, 0x4d, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x52, 0x13, 0x6d, 0x69, 0x6e, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, - 0x72, 0x64, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x22, 0xfb, 0x08, 0x0a, 0x18, 0x4f, 0x69, + 0x72, 0x64, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x22, 0xe6, 0x09, 0x0a, 0x18, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x59, 0x0a, 0x06, @@ -879,54 +891,60 @@ var file_controller_api_resources_authmethods_v1_auth_method_proto_rawDesc = []b 0x01, 0xc2, 0xdd, 0x29, 0x28, 0x0a, 0x18, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0x0c, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x52, 0x0d, 0x63, - 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0x58, 0x0a, 0x24, - 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, - 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x78, 0x20, 0x01, 0x28, 0x08, 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, - 0x52, 0x24, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, - 0x65, 0x72, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x69, - 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x07, 0x64, 0x72, 0x79, 0x5f, 0x72, 0x75, - 0x6e, 0x18, 0x82, 0x01, 0x20, 0x01, 0x28, 0x08, 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x07, - 0x64, 0x72, 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x22, 0x61, 0x0a, 0x27, 0x4f, 0x69, 0x64, 0x63, 0x41, - 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, - 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0a, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x75, 0x72, 0x6c, 0x12, 0x1a, - 0x0a, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x22, 0xb7, 0x01, 0x0a, 0x29, 0x4f, - 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, - 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, - 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, - 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x1e, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2c, 0x0a, 0x11, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x28, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x11, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, - 0x75, 0x72, 0x69, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x5f, 0x75, 0x72, 0x69, 0x22, 0x5c, 0x0a, 0x2a, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, - 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x64, 0x69, - 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, - 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, - 0x72, 0x6c, 0x22, 0x44, 0x0a, 0x26, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, + 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0x69, 0x0a, 0x12, + 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x5f, 0x6d, 0x61, + 0x70, 0x73, 0x18, 0x71, 0x20, 0x03, 0x28, 0x09, 0x42, 0x39, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, + 0x29, 0x31, 0x0a, 0x1d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x61, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x5f, 0x6d, 0x61, 0x70, + 0x73, 0x12, 0x10, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x4d, + 0x61, 0x70, 0x73, 0x52, 0x12, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x63, 0x6c, 0x61, + 0x69, 0x6d, 0x5f, 0x6d, 0x61, 0x70, 0x73, 0x12, 0x58, 0x0a, 0x24, 0x64, 0x69, 0x73, 0x61, 0x62, + 0x6c, 0x65, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x65, 0x64, 0x5f, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x78, 0x20, 0x01, 0x28, 0x08, 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x24, 0x64, 0x69, 0x73, + 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x65, 0x64, 0x5f, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x1f, 0x0a, 0x07, 0x64, 0x72, 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x18, 0x82, 0x01, 0x20, + 0x01, 0x28, 0x08, 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x07, 0x64, 0x72, 0x79, 0x5f, 0x72, + 0x75, 0x6e, 0x22, 0x61, 0x0a, 0x27, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x22, 0x41, 0x0a, 0x27, 0x4f, 0x69, 0x64, 0x63, - 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, - 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0a, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, 0x5d, 0x5a, 0x5b, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, - 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x3b, 0x61, - 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, + 0x08, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x75, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x22, 0xb7, 0x01, 0x0a, 0x29, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, + 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, + 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x12, 0x2c, 0x0a, 0x11, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x64, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x32, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x75, 0x72, 0x69, 0x22, + 0x5c, 0x0a, 0x2a, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x61, 0x6c, + 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, + 0x12, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, + 0x75, 0x72, 0x6c, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x66, 0x69, 0x6e, 0x61, 0x6c, + 0x5f, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x22, 0x44, 0x0a, + 0x26, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, + 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x5f, 0x69, 0x64, 0x22, 0x41, 0x0a, 0x27, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, + 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, 0x5d, 0x5a, 0x5b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, + 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, + 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x61, 0x75, + 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x3b, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/proto/local/controller/api/resources/authmethods/v1/auth_method.proto b/internal/proto/local/controller/api/resources/authmethods/v1/auth_method.proto index c96da96b1..7ad75536c 100644 --- a/internal/proto/local/controller/api/resources/authmethods/v1/auth_method.proto +++ b/internal/proto/local/controller/api/resources/authmethods/v1/auth_method.proto @@ -123,6 +123,13 @@ message OidcAuthMethodAttributes { repeated string claims_scopes = 112 [json_name = "claims_scopes", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = { this: "attributes.claims_scopes" that: "ClaimsScopes" }]; + // account_claim_maps are optional claim maps from custom claims to the + // standard claims of sub, name and email. These maps are represented as + // key=value where the key equals the from_claim and the value equals the + // to_claim. For example "oid=sub". + repeated string account_claim_maps = 113 + [json_name = "account_claim_maps", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = { this: "attributes.account_claim_maps" that: "AccountClaimMaps" }]; + // If the Authorization Server's discovered configuration contains values // that do not match the configuration set on this auth method, this can be // set to force the local configuration to override the discovered values. diff --git a/internal/proto/local/controller/storage/auth/oidc/store/v1/oidc.proto b/internal/proto/local/controller/storage/auth/oidc/store/v1/oidc.proto index 642295a40..d6234991c 100644 --- a/internal/proto/local/controller/storage/auth/oidc/store/v1/oidc.proto +++ b/internal/proto/local/controller/storage/auth/oidc/store/v1/oidc.proto @@ -117,6 +117,13 @@ message AuthMethod { // are operatated on as a complete set. // @inject_tag: `gorm:"-"` repeated string claims_scopes = 200 [(custom_options.v1.mask_mapping) = { this: "ClaimsScopes" that: "attributes.claims_scopes" }]; + + // account_claim_maps are optional claim maps from custom claims to the + // standard claims of sub, name and email. These maps are represented as + // key=value where the key equals the from_claim and the value equals the + // to_claim. For example "oid=sub". + // @inject_tag: `gorm:"-"` + repeated string account_claim_maps = 210 [(custom_options.v1.mask_mapping) = { this: "AccountClaimMaps" that: "attributes.account_claim_maps" }]; } // Account represents an OIDC account @@ -224,4 +231,24 @@ message ClaimsScope { // The create_time is set by the database. // @inject_tag: `gorm:"default:current_timestamp"` timestamp.v1.Timestamp create_time = 30; +} + +// AccountClaimMap entries are optional from/to account claim maps. +message AccountClaimMap { + // @inject_tag: `gorm:"primary_key"` + string oidc_method_id = 10; + + // from_claim is the claim from the id_token that you need to map to a + // standard account claim. + // @inject_tag: `gorm:"not_null"` + string from_claim = 20; + + // to_claim is the standard account claim to map the from_claim to. Valid + // values are: sub, name, email + // @inject_tag: `gorm:"column:to_claim;primary_key"` + string to_claim = 30; + + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 40; } \ No newline at end of file diff --git a/internal/servers/controller/handlers/authmethods/authmethod_service.go b/internal/servers/controller/handlers/authmethods/authmethod_service.go index 87a2dbf93..1d4f0ac71 100644 --- a/internal/servers/controller/handlers/authmethods/authmethod_service.go +++ b/internal/servers/controller/handlers/authmethods/authmethod_service.go @@ -678,6 +678,7 @@ func toAuthMethodProto(in auth.AuthMethod, opt ...handlers.Option) (*pb.AuthMeth SigningAlgorithms: i.GetSigningAlgs(), AllowedAudiences: i.GetAudClaims(), ClaimsScopes: i.GetClaimsScopes(), + AccountClaimMaps: i.GetAccountClaimMaps(), } if i.DisableDiscoveredConfigValidation { attrs.DisableDiscoveredConfigValidation = true @@ -814,6 +815,19 @@ func validateCreateRequest(req *pbs.CreateAuthMethodRequest) error { } } } + if len(attrs.GetAccountClaimMaps()) > 0 { + acm, err := oidc.ParseAccountClaimMaps(attrs.GetAccountClaimMaps()...) + if err != nil { + badFields[accountClaimMapsField] = fmt.Sprintf("Contains invalid map %q", err.Error()) + } + foundTo := make(map[string]bool, len(attrs.GetAccountClaimMaps())) + for from, rawTo := range acm { + if foundTo[rawTo] { + badFields[accountClaimMapsField] = fmt.Sprintf("%s=%s contains invalid map - multiple maps to the same Boundary to-claim %s", from, rawTo, rawTo) + } + foundTo[rawTo] = true + } + } } default: badFields[typeField] = fmt.Sprintf("This is a required field and must be %q.", auth.PasswordSubtype.String()) @@ -922,6 +936,30 @@ func validateUpdateRequest(req *pbs.UpdateAuthMethodRequest) error { } } } + if len(attrs.GetAccountClaimMaps()) > 0 { + acm, err := oidc.ParseAccountClaimMaps(attrs.GetAccountClaimMaps()...) + if err != nil { + badFields[accountClaimMapsField] = fmt.Sprintf("Contains invalid map %q", err.Error()) + } else { + foundTo := make(map[string]bool, len(attrs.GetAccountClaimMaps())) + for from, rawTo := range acm { + if foundTo[rawTo] { + badFields[accountClaimMapsField] = fmt.Sprintf("%s=%s contains invalid map - multiple maps to the same Boundary to-claim %s", from, rawTo, rawTo) + } + foundTo[rawTo] = true + + to, err := oidc.ConvertToAccountToClaim(rawTo) + if err != nil { + badFields[accountClaimMapsField] = fmt.Sprintf("%s=%s contains invalid map %q", from, rawTo, err.Error()) + break + } + if to == oidc.ToSubClaim { + badFields[accountClaimMapsField] = fmt.Sprintf("%s=%s contains invalid map: not allowed to update sub claim maps", from, rawTo) + break + } + } + } + } default: badFields["id"] = "Incorrectly formatted identifier." } diff --git a/internal/servers/controller/handlers/authmethods/authmethod_service_test.go b/internal/servers/controller/handlers/authmethods/authmethod_service_test.go index e48f1d1c9..dd0a87486 100644 --- a/internal/servers/controller/handlers/authmethods/authmethod_service_test.go +++ b/internal/servers/controller/handlers/authmethods/authmethod_service_test.go @@ -572,6 +572,10 @@ func TestCreate(t *testing.T) { lv, _ := structpb.NewList([]interface{}{"email", "profile"}) return structpb.NewListValue(lv) }(), + "account_claim_maps": func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{"display_name=name", "oid=sub"}) + return structpb.NewListValue(lv) + }(), }}, }}, idPrefix: oidc.AuthMethodPrefix + "_", @@ -600,6 +604,10 @@ func TestCreate(t *testing.T) { lv, _ := structpb.NewList([]interface{}{"email", "profile"}) return structpb.NewListValue(lv) }(), + "account_claim_maps": func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{"display_name=name", "oid=sub"}) + return structpb.NewListValue(lv) + }(), }}, AuthorizedActions: oidcAuthorizedActions, AuthorizedCollectionActions: authorizedCollectionActions, @@ -989,7 +997,7 @@ func TestCreate(t *testing.T) { delete(tc.res.Item.Attributes.Fields, "callback_url") } } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateAuthMethod(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform(), protocmp.SortRepeatedFields(got)), "CreateAuthMethod(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } diff --git a/internal/servers/controller/handlers/authmethods/oidc.go b/internal/servers/controller/handlers/authmethods/oidc.go index 2d9e48119..581bb516c 100644 --- a/internal/servers/controller/handlers/authmethods/oidc.go +++ b/internal/servers/controller/handlers/authmethods/oidc.go @@ -42,6 +42,7 @@ const ( roundtripPayloadAttributesField = "attributes.roundtrip_payload" codeField = "attributes.code" claimsScopesField = "attributes.claims_scopes" + accountClaimMapsField = "attributes.account_claim_maps" ) var oidcMaskManager handlers.MaskManager @@ -472,6 +473,28 @@ func toStorageOidcAuthMethod(scopeId string, in *pb.AuthMethod) (out *oidc.AuthM opts = append(opts, oidc.WithClaimsScopes(attrs.GetClaimsScopes()...)) } + if len(attrs.GetAccountClaimMaps()) > 0 { + claimsMap := make(map[string]oidc.AccountToClaim, len(attrs.GetAccountClaimMaps())) + for _, v := range attrs.GetAccountClaimMaps() { + acm, err := oidc.ParseAccountClaimMaps(v) + if err != nil { + return nil, false, false, errors.Wrap(err, op) + } + if len(acm) > 1 { + return nil, false, false, errors.New(errors.InvalidParameter, op, fmt.Sprintf("unable to parse account claim map %s", v)) + } + var from, rawTo string + for from, rawTo = range acm { + } + to, err := oidc.ConvertToAccountToClaim(rawTo) + if err != nil { + return nil, false, false, errors.Wrap(err, op) + } + claimsMap[from] = to + } + opts = append(opts, oidc.WithAccountClaimMap(claimsMap)) + } + u, err := oidc.NewAuthMethod(scopeId, clientId, clientSecret, opts...) if err != nil { return nil, false, false, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build auth method: %v.", err) diff --git a/internal/servers/controller/handlers/authmethods/oidc_test.go b/internal/servers/controller/handlers/authmethods/oidc_test.go index e212de7fc..e790fd54d 100644 --- a/internal/servers/controller/handlers/authmethods/oidc_test.go +++ b/internal/servers/controller/handlers/authmethods/oidc_test.go @@ -905,6 +905,64 @@ func TestUpdate_OIDC(t *testing.T) { }, }, }, + { + name: "Change Account Claim Maps", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.account_claim_maps"}, + }, + Item: &pb.AuthMethod{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "account_claim_maps": func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{"display_name=name"}) + return structpb.NewListValue(lv) + }(), + }, + }, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{ + Fields: func() map[string]*structpb.Value { + f := defaultReadAttributeFields() + f["account_claim_maps"] = func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{"display_name=name"}) + return structpb.NewListValue(lv) + }() + return f + }(), + }, + Scope: defaultScopeInfo, + AuthorizedActions: oidcAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "Attempt to Change Account Claim Maps for sub", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.account_claim_maps"}, + }, + Item: &pb.AuthMethod{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "account_claim_maps": func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{"oid=sub"}) + return structpb.NewListValue(lv) + }(), + }, + }, + }, + }, + wantErr: true, + }, { name: "Unset Issuer Is Incomplete", req: &pbs.UpdateAuthMethodRequest{ @@ -996,7 +1054,6 @@ func TestUpdate_OIDC(t *testing.T) { tc.res.Item.Id = am.GetId() tc.res.Item.CreatedTime = am.GetCreatedTime() } - got, gErr := tested.UpdateAuthMethod(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req) // TODO: When handlers move to domain errors remove wantErr and rely errors.Match here. if tc.err != nil || tc.wantErr {