mirror of
https://github.com/zitadel/zitadel
synced 2024-11-22 00:39:36 +00:00
120ed0af73
# 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
124 lines
3.9 KiB
Go
124 lines
3.9 KiB
Go
package query
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
_ "embed"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
"text/template"
|
|
|
|
"github.com/zitadel/zitadel/internal/api/authz"
|
|
"github.com/zitadel/zitadel/internal/database"
|
|
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
|
"github.com/zitadel/zitadel/internal/query/projection"
|
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
// oidcUserInfoTriggerHandlers slice can only be created after zitadel
|
|
// is fully initialized, otherwise the handlers are nil.
|
|
// OnceValue takes care of creating the slice on the first request
|
|
// and than will always return the same slice on subsequent requests.
|
|
var oidcUserInfoTriggerHandlers = sync.OnceValue(func() []*handler.Handler {
|
|
return []*handler.Handler{
|
|
projection.UserProjection,
|
|
projection.UserMetadataProjection,
|
|
projection.UserGrantProjection,
|
|
projection.OrgProjection,
|
|
projection.ProjectProjection,
|
|
}
|
|
})
|
|
|
|
// TriggerOIDCUserInfoProjections triggers all projections
|
|
// relevant to userinfo queries concurrently.
|
|
func TriggerOIDCUserInfoProjections(ctx context.Context) {
|
|
triggerBatch(ctx, oidcUserInfoTriggerHandlers()...)
|
|
}
|
|
|
|
var (
|
|
//go:embed userinfo_by_id.sql
|
|
oidcUserInfoQueryTmpl string
|
|
oidcUserInfoQuery string
|
|
oidcUserInfoWithRoleOrgIDsQuery string
|
|
)
|
|
|
|
// build the two variants of the userInfo query
|
|
func init() {
|
|
tmpl := template.Must(template.New("oidcUserInfoQuery").Parse(oidcUserInfoQueryTmpl))
|
|
var buf strings.Builder
|
|
if err := tmpl.Execute(&buf, false); err != nil {
|
|
panic(err)
|
|
}
|
|
oidcUserInfoQuery = buf.String()
|
|
buf.Reset()
|
|
|
|
if err := tmpl.Execute(&buf, true); err != nil {
|
|
panic(err)
|
|
}
|
|
oidcUserInfoWithRoleOrgIDsQuery = buf.String()
|
|
buf.Reset()
|
|
}
|
|
|
|
func (q *Queries) GetOIDCUserInfo(ctx context.Context, userID string, roleAudience []string, roleOrgIDs ...string) (userInfo *OIDCUserInfo, err error) {
|
|
ctx, span := tracing.NewSpan(ctx)
|
|
defer func() { span.EndWithError(err) }()
|
|
|
|
if len(roleOrgIDs) > 0 {
|
|
userInfo, err = database.QueryJSONObject[OIDCUserInfo](ctx, q.client, oidcUserInfoWithRoleOrgIDsQuery,
|
|
userID, authz.GetInstance(ctx).InstanceID(), database.TextArray[string](roleAudience), database.TextArray[string](roleOrgIDs),
|
|
)
|
|
} else {
|
|
userInfo, err = database.QueryJSONObject[OIDCUserInfo](ctx, q.client, oidcUserInfoQuery,
|
|
userID, authz.GetInstance(ctx).InstanceID(), database.TextArray[string](roleAudience),
|
|
)
|
|
}
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, zerrors.ThrowNotFound(err, "QUERY-Eey2a", "Errors.User.NotFound")
|
|
}
|
|
if err != nil {
|
|
return nil, zerrors.ThrowInternal(err, "QUERY-Oath6", "Errors.Internal")
|
|
}
|
|
if userInfo.User == nil {
|
|
return nil, zerrors.ThrowNotFound(nil, "QUERY-ahs4S", "Errors.User.NotFound")
|
|
}
|
|
|
|
return userInfo, nil
|
|
}
|
|
|
|
type OIDCUserInfo struct {
|
|
User *User `json:"user,omitempty"`
|
|
Metadata []UserMetadata `json:"metadata,omitempty"`
|
|
Org *UserInfoOrg `json:"org,omitempty"`
|
|
UserGrants []UserGrant `json:"user_grants,omitempty"`
|
|
}
|
|
|
|
type UserInfoOrg struct {
|
|
ID string `json:"id,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
PrimaryDomain string `json:"primary_domain,omitempty"`
|
|
}
|
|
|
|
//go:embed userinfo_client_by_id.sql
|
|
var oidcUserinfoClientQuery string
|
|
|
|
func (q *Queries) GetOIDCUserinfoClientByID(ctx context.Context, clientID string) (projectID string, projectRoleAssertion bool, err error) {
|
|
ctx, span := tracing.NewSpan(ctx)
|
|
defer func() { span.EndWithError(err) }()
|
|
|
|
scan := func(row *sql.Row) error {
|
|
err := row.Scan(&projectID, &projectRoleAssertion)
|
|
return err
|
|
}
|
|
|
|
err = q.client.QueryRowContext(ctx, scan, oidcUserinfoClientQuery, authz.GetInstance(ctx).InstanceID(), clientID)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return "", false, zerrors.ThrowNotFound(err, "QUERY-beeW8", "Errors.App.NotFound")
|
|
}
|
|
if err != nil {
|
|
return "", false, zerrors.ThrowInternal(err, "QUERY-Ais4r", "Errors.Internal")
|
|
}
|
|
return projectID, projectRoleAssertion, nil
|
|
}
|