zitadel/internal/query/userinfo_test.go
Tim Möhlmann 6a51c4b0f5
feat(oidc): optimize the userinfo endpoint (#7706)
* feat(oidc): optimize the userinfo endpoint

* store project ID in the access token

* query for projectID if not in token

* add scope based tests

* Revert "store project ID in the access token"

This reverts commit 5f0262f239.

* query project role assertion

* use project role assertion setting to return roles

* workaround eventual consistency and handle PAT

* do not append empty project id
2024-04-09 15:15:35 +02:00

388 lines
13 KiB
Go

package query
import (
"database/sql"
"database/sql/driver"
_ "embed"
"encoding/json"
"regexp"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
)
var (
//go:embed testdata/userinfo_not_found.json
testdataUserInfoNotFound string
//go:embed testdata/userinfo_human_no_md.json
testdataUserInfoHumanNoMD string
//go:embed testdata/userinfo_human.json
testdataUserInfoHuman string
//go:embed testdata/userinfo_human_grants.json
testdataUserInfoHumanGrants string
//go:embed testdata/userinfo_machine.json
testdataUserInfoMachine string
// timeLocation does a single parse of the testdata and extracts a time.Location,
// so that it may be used during test assertion.
// Because depending on the environment json.Unmarshal parses
// the 00:00 timezones differently.
// On my local system is parses to an empty timezone with 0 offset,
// but in github action is pares into UTC.
timeLocation = func() *time.Location {
referenceInfo := new(OIDCUserInfo)
err := json.Unmarshal([]byte(testdataUserInfoHumanNoMD), referenceInfo)
if err != nil {
panic(err)
}
return referenceInfo.User.CreationDate.Location()
}()
)
func TestQueries_GetOIDCUserInfo(t *testing.T) {
expQuery := regexp.QuoteMeta(oidcUserInfoQuery)
type args struct {
userID string
roleAudience []string
}
tests := []struct {
name string
args args
mock sqlExpectation
want *OIDCUserInfo
wantErr error
}{
{
name: "query error",
args: args{
userID: "231965491734773762",
},
mock: mockQueryErr(expQuery, sql.ErrConnDone, "231965491734773762", "instanceID", database.TextArray[string](nil)),
wantErr: sql.ErrConnDone,
},
{
name: "user not found",
args: args{
userID: "231965491734773762",
},
mock: mockQuery(expQuery, []string{"json_build_object"}, []driver.Value{testdataUserInfoNotFound}, "231965491734773762", "instanceID", database.TextArray[string](nil)),
wantErr: zerrors.ThrowNotFound(nil, "QUERY-ahs4S", "Errors.User.NotFound"),
},
{
name: "human without metadata",
args: args{
userID: "231965491734773762",
},
mock: mockQuery(expQuery, []string{"json_build_object"}, []driver.Value{testdataUserInfoHumanNoMD}, "231965491734773762", "instanceID", database.TextArray[string](nil)),
want: &OIDCUserInfo{
User: &User{
ID: "231965491734773762",
CreationDate: time.Date(2023, time.September, 15, 6, 10, 7, 434142000, timeLocation),
ChangeDate: time.Date(2023, time.November, 14, 13, 27, 2, 72318000, timeLocation),
Sequence: 1148,
State: 1,
ResourceOwner: "231848297847848962",
Username: "tim+tesmail@zitadel.com",
PreferredLoginName: "tim+tesmail@zitadel.com@demo.localhost",
Human: &Human{
FirstName: "Tim",
LastName: "Mohlmann",
NickName: "muhlemmer",
DisplayName: "Tim Mohlmann",
AvatarKey: "",
PreferredLanguage: language.English,
Gender: domain.GenderMale,
Email: "tim+tesmail@zitadel.com",
IsEmailVerified: true,
Phone: "+40123456789",
IsPhoneVerified: false,
},
Machine: nil,
},
Org: &UserInfoOrg{
ID: "231848297847848962",
Name: "demo",
PrimaryDomain: "demo.localhost",
},
Metadata: nil,
},
},
{
name: "human with metadata",
args: args{
userID: "231965491734773762",
},
mock: mockQuery(expQuery, []string{"json_build_object"}, []driver.Value{testdataUserInfoHuman}, "231965491734773762", "instanceID", database.TextArray[string](nil)),
want: &OIDCUserInfo{
User: &User{
ID: "231965491734773762",
CreationDate: time.Date(2023, time.September, 15, 6, 10, 7, 434142000, timeLocation),
ChangeDate: time.Date(2023, time.November, 14, 13, 27, 2, 72318000, timeLocation),
Sequence: 1148,
State: 1,
ResourceOwner: "231848297847848962",
Username: "tim+tesmail@zitadel.com",
PreferredLoginName: "tim+tesmail@zitadel.com@demo.localhost",
Human: &Human{
FirstName: "Tim",
LastName: "Mohlmann",
NickName: "muhlemmer",
DisplayName: "Tim Mohlmann",
AvatarKey: "",
PreferredLanguage: language.English,
Gender: domain.GenderMale,
Email: "tim+tesmail@zitadel.com",
IsEmailVerified: true,
Phone: "+40123456789",
IsPhoneVerified: false,
},
Machine: nil,
},
Org: &UserInfoOrg{
ID: "231848297847848962",
Name: "demo",
PrimaryDomain: "demo.localhost",
},
Metadata: []UserMetadata{
{
CreationDate: time.Date(2023, time.November, 14, 13, 26, 3, 553702000, timeLocation),
ChangeDate: time.Date(2023, time.November, 14, 13, 26, 3, 553702000, timeLocation),
Sequence: 1147,
ResourceOwner: "231848297847848962",
Key: "bar",
Value: []byte("foo"),
},
{
CreationDate: time.Date(2023, time.November, 14, 13, 25, 57, 171368000, timeLocation),
ChangeDate: time.Date(2023, time.November, 14, 13, 25, 57, 171368000, timeLocation),
Sequence: 1146,
ResourceOwner: "231848297847848962",
Key: "foo",
Value: []byte("bar"),
},
},
},
},
{
name: "human with metadata and grants",
args: args{
userID: "231965491734773762",
roleAudience: []string{"236645808328409090", "240762134579904514"},
},
mock: mockQuery(expQuery,
[]string{"json_build_object"},
[]driver.Value{testdataUserInfoHumanGrants},
"231965491734773762", "instanceID", database.TextArray[string]{"236645808328409090", "240762134579904514"},
),
want: &OIDCUserInfo{
User: &User{
ID: "231965491734773762",
CreationDate: time.Date(2023, time.September, 15, 6, 10, 7, 434142000, timeLocation),
ChangeDate: time.Date(2023, time.November, 14, 13, 27, 2, 72318000, timeLocation),
Sequence: 1148,
State: 1,
ResourceOwner: "231848297847848962",
Username: "tim+tesmail@zitadel.com",
PreferredLoginName: "tim+tesmail@zitadel.com@demo.localhost",
Human: &Human{
FirstName: "Tim",
LastName: "Mohlmann",
NickName: "muhlemmer",
DisplayName: "Tim Mohlmann",
AvatarKey: "",
PreferredLanguage: language.English,
Gender: domain.GenderMale,
Email: "tim+tesmail@zitadel.com",
IsEmailVerified: true,
Phone: "+40123456789",
IsPhoneVerified: false,
},
Machine: nil,
},
Org: &UserInfoOrg{
ID: "231848297847848962",
Name: "demo",
PrimaryDomain: "demo.localhost",
},
Metadata: []UserMetadata{
{
CreationDate: time.Date(2023, time.November, 14, 13, 26, 3, 553702000, timeLocation),
ChangeDate: time.Date(2023, time.November, 14, 13, 26, 3, 553702000, timeLocation),
Sequence: 1147,
ResourceOwner: "231848297847848962",
Key: "bar",
Value: []byte("foo"),
},
{
CreationDate: time.Date(2023, time.November, 14, 13, 25, 57, 171368000, timeLocation),
ChangeDate: time.Date(2023, time.November, 14, 13, 25, 57, 171368000, timeLocation),
Sequence: 1146,
ResourceOwner: "231848297847848962",
Key: "foo",
Value: []byte("bar"),
},
},
UserGrants: []UserGrant{
{
ID: "240749256523120642",
GrantID: "",
State: 1,
CreationDate: time.Date(2023, time.November, 14, 20, 28, 59, 168208000, timeLocation),
ChangeDate: time.Date(2023, time.November, 14, 20, 50, 58, 822391000, timeLocation),
Sequence: 2,
UserID: "231965491734773762",
Roles: []string{
"role1",
"role2",
},
ResourceOwner: "231848297847848962",
ProjectID: "236645808328409090",
OrgName: "demo",
OrgPrimaryDomain: "demo.localhost",
ProjectName: "tests",
UserResourceOwner: "231848297847848962",
},
{
ID: "240762315572510722",
GrantID: "",
State: 1,
CreationDate: time.Date(2023, time.November, 14, 22, 38, 42, 967317000, timeLocation),
ChangeDate: time.Date(2023, time.November, 14, 22, 38, 42, 967317000, timeLocation),
Sequence: 1,
UserID: "231965491734773762",
Roles: []string{
"role3",
"role4",
},
ResourceOwner: "231848297847848962",
ProjectID: "240762134579904514",
OrgName: "demo",
OrgPrimaryDomain: "demo.localhost",
ProjectName: "tests2",
UserResourceOwner: "231848297847848962",
},
},
},
},
{
name: "machine with metadata",
args: args{
userID: "240707570677841922",
},
mock: mockQuery(expQuery, []string{"json_build_object"}, []driver.Value{testdataUserInfoMachine}, "240707570677841922", "instanceID", database.TextArray[string](nil)),
want: &OIDCUserInfo{
User: &User{
ID: "240707570677841922",
CreationDate: time.Date(2023, time.November, 14, 13, 34, 52, 473732000, timeLocation),
ChangeDate: time.Date(2023, time.November, 14, 13, 35, 2, 861342000, timeLocation),
Sequence: 2,
State: 1,
ResourceOwner: "231848297847848962",
Username: "tests",
PreferredLoginName: "tests@demo.localhost",
Human: nil,
Machine: &Machine{
Name: "tests",
Description: "My test service user",
},
},
Org: &UserInfoOrg{
ID: "231848297847848962",
Name: "demo",
PrimaryDomain: "demo.localhost",
},
Metadata: []UserMetadata{
{
CreationDate: time.Date(2023, time.November, 14, 13, 35, 30, 126849000, timeLocation),
ChangeDate: time.Date(2023, time.November, 14, 13, 35, 30, 126849000, timeLocation),
Sequence: 3,
ResourceOwner: "231848297847848962",
Key: "first",
Value: []byte("Hello World!"),
},
{
CreationDate: time.Date(2023, time.November, 14, 13, 35, 44, 28343000, timeLocation),
ChangeDate: time.Date(2023, time.November, 14, 13, 35, 44, 28343000, timeLocation),
Sequence: 4,
ResourceOwner: "231848297847848962",
Key: "second",
Value: []byte("Bye World!"),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
execMock(t, tt.mock, func(db *sql.DB) {
q := &Queries{
client: &database.DB{
DB: db,
Database: &prepareDB{},
},
}
ctx := authz.NewMockContext("instanceID", "orgID", "loginClient")
got, err := q.GetOIDCUserInfo(ctx, tt.args.userID, tt.args.roleAudience)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
})
}
}
func TestQueries_GetOIDCUserinfoClientByID(t *testing.T) {
expQuery := regexp.QuoteMeta(oidcUserinfoClientQuery)
cols := []string{"project_id", "project_role_assertion"}
tests := []struct {
name string
mock sqlExpectation
wantProjectID string
wantProjectRoleAssertion bool
wantErr error
}{
{
name: "no rows",
mock: mockQueryErr(expQuery, sql.ErrNoRows, "instanceID", "clientID"),
wantErr: zerrors.ThrowNotFound(sql.ErrNoRows, "QUERY-beeW8", "Errors.App.NotFound"),
},
{
name: "internal error",
mock: mockQueryErr(expQuery, sql.ErrConnDone, "instanceID", "clientID"),
wantErr: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-Ais4r", "Errors.Internal"),
},
{
name: "found",
mock: mockQuery(expQuery, cols, []driver.Value{"projectID", true}, "instanceID", "clientID"),
wantProjectID: "projectID",
wantProjectRoleAssertion: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
execMock(t, tt.mock, func(db *sql.DB) {
q := &Queries{
client: &database.DB{
DB: db,
Database: &prepareDB{},
},
}
ctx := authz.NewMockContext("instanceID", "orgID", "loginClient")
gotProjectID, gotProjectRoleAssertion, err := q.GetOIDCUserinfoClientByID(ctx, "clientID")
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.wantProjectID, gotProjectID)
assert.Equal(t, tt.wantProjectRoleAssertion, gotProjectRoleAssertion)
})
})
}
}