zitadel/internal/query/userinfo_test.go
Tim Möhlmann 120ed0af73
feat(oidc): organization roles scope (#8120)
# Which Problems Are Solved

An admin / application might want to be able to reduce the amount of
roles returned in the token, for example if a user is granted to many
organizations or for specific cases where the application want to narrow
down the access for that token to a specific organization or multiple.
This can now be achieved by providing a scope with the id of the
organization, resp. multiple scopes for every organization, which should
be included.

```
urn:zitadel:iam:org:roles🆔{orgID}
```

**Note:** the new scope does not work when Introspection / Userinfo are
set to legacy mode.

# How the Problems Are Solved

The user info query now has two variants:

1. Variant that returns all organization authorization grants if the new
scope wasn't provided for backward compatibility.
2. Variant that filters the organizations based on the IDs passed in one
or more of the above scopes and returns only those authorization grants.

The query is defined as a `text/template` and both variants are rendered
once in package `init()`.

# Additional Changes

- In the integration tests `assertProjectRoleClaims` now also checks the
org IDs in the roles.

# Additional Context

- Closes #7996
2024-06-14 10:00:43 +02:00

492 lines
17 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) {
type args struct {
userID string
roleAudience []string
roleOrgIDs []string
}
tests := []struct {
name string
args args
mock sqlExpectation
want *OIDCUserInfo
wantErr error
}{
{
name: "query error",
args: args{
userID: "231965491734773762",
},
mock: mockQueryErr(regexp.QuoteMeta(oidcUserInfoQuery), sql.ErrConnDone, "231965491734773762", "instanceID", database.TextArray[string](nil)),
wantErr: sql.ErrConnDone,
},
{
name: "user not found",
args: args{
userID: "231965491734773762",
},
mock: mockQuery(regexp.QuoteMeta(oidcUserInfoQuery), []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(regexp.QuoteMeta(oidcUserInfoQuery), []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(regexp.QuoteMeta(oidcUserInfoQuery), []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(regexp.QuoteMeta(oidcUserInfoQuery),
[]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: "human with metadata and grants, role orgIDs",
args: args{
userID: "231965491734773762",
roleAudience: []string{"236645808328409090", "240762134579904514"},
roleOrgIDs: []string{"231848297847848962"},
},
mock: mockQuery(regexp.QuoteMeta(oidcUserInfoWithRoleOrgIDsQuery),
[]string{"json_build_object"},
[]driver.Value{testdataUserInfoHumanGrants},
"231965491734773762", "instanceID",
database.TextArray[string]{"236645808328409090", "240762134579904514"},
database.TextArray[string]{"231848297847848962"},
),
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(regexp.QuoteMeta(oidcUserInfoQuery), []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, tt.args.roleOrgIDs...)
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)
})
})
}
}