mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-30 20:00:44 +01:00
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat(authdomain): support custom roles in SSO group mapping Role mappings now reference SigNoz roles by name (custom or managed) instead of the legacy ADMIN/EDITOR/VIEWER enum. Legacy values sent by a client are normalized to their managed names (signoz-admin/editor/viewer), and a migration normalizes existing stored mappings. - normalize legacy role names on unmarshal; resolve all matched roles on SSO callback (union of matched groups, else default, else viewer) - validate referenced roles exist on auth domain create/update - block deleting a role referenced by any auth domain mapping, naming the referencing domains (OnBeforeRoleDelete now also passes the role name) - migration 096 to rewrite legacy names in existing auth_domain mappings * refactor(sqlmigration): decode SSO role mapping into a typed struct Decode the roleMapping object into a small frozen struct instead of poking at json.RawMessage per field. The top-level config stays a raw map so the other config fields are preserved untouched. * fix(authdomain): validate SSO role attribute claim against existing roles When the role mapping uses the IDP role attribute, the claimed role is assigned only if it exists in the org (resolved case-insensitively, with legacy ADMIN/EDITOR/VIEWER mapped to their managed names); otherwise the mapping falls through to group mappings and the default role. This lets custom roles be assigned via the attribute and avoids failing login on an unknown claim. Also normalize legacy role names case-insensitively and fold the single-use managed-role lookup into NormalizeRoleName.
238 lines
7.9 KiB
Go
238 lines
7.9 KiB
Go
package implsession
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"net/url"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/SigNoz/signoz/pkg/authn"
|
|
"github.com/SigNoz/signoz/pkg/authz"
|
|
"github.com/SigNoz/signoz/pkg/errors"
|
|
"github.com/SigNoz/signoz/pkg/factory"
|
|
"github.com/SigNoz/signoz/pkg/modules/authdomain"
|
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
|
"github.com/SigNoz/signoz/pkg/modules/session"
|
|
"github.com/SigNoz/signoz/pkg/modules/user"
|
|
"github.com/SigNoz/signoz/pkg/tokenizer"
|
|
"github.com/SigNoz/signoz/pkg/types"
|
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
|
"github.com/SigNoz/signoz/pkg/valuer"
|
|
)
|
|
|
|
type module struct {
|
|
settings factory.ScopedProviderSettings
|
|
authNs map[authtypes.AuthNProvider]authn.AuthN
|
|
userSetter user.Setter
|
|
userGetter user.Getter
|
|
authDomain authdomain.Module
|
|
tokenizer tokenizer.Tokenizer
|
|
orgGetter organization.Getter
|
|
authz authz.AuthZ
|
|
}
|
|
|
|
func NewModule(providerSettings factory.ProviderSettings, authNs map[authtypes.AuthNProvider]authn.AuthN, userSetter user.Setter, userGetter user.Getter, authDomain authdomain.Module, tokenizer tokenizer.Tokenizer, orgGetter organization.Getter, authz authz.AuthZ) session.Module {
|
|
return &module{
|
|
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/session/implsession"),
|
|
authNs: authNs,
|
|
userSetter: userSetter,
|
|
userGetter: userGetter,
|
|
authDomain: authDomain,
|
|
tokenizer: tokenizer,
|
|
orgGetter: orgGetter,
|
|
authz: authz,
|
|
}
|
|
}
|
|
|
|
func (module *module) GetSessionContext(ctx context.Context, email valuer.Email, siteURL *url.URL) (*authtypes.SessionContext, error) {
|
|
context := authtypes.NewSessionContext()
|
|
|
|
orgs, err := module.orgGetter.ListByOwnedKeyRange(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(orgs) == 0 {
|
|
context.Exists = false
|
|
return context, nil
|
|
}
|
|
|
|
var orgIDs []valuer.UUID
|
|
for _, org := range orgs {
|
|
orgIDs = append(orgIDs, org.ID)
|
|
}
|
|
|
|
users, err := module.userGetter.ListUsersByEmailAndOrgIDs(ctx, email, orgIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// filter out deleted users
|
|
users = slices.DeleteFunc(users, func(user *types.User) bool { return user.ErrIfDeleted() != nil })
|
|
|
|
// Since email is a valuer, we can be sure that it is a valid email and we can split it to get the domain name.
|
|
name := strings.Split(email.String(), "@")[1]
|
|
|
|
if len(users) == 0 {
|
|
context.Exists = false
|
|
|
|
for _, org := range orgs {
|
|
orgContext, err := module.getOrgSessionContext(ctx, org, name, siteURL)
|
|
if err != nil {
|
|
// For some reason, there was an error in getting the org session context. Instead of failing the context call, we create a PasswordAuthNSupport for the org and add a warning.
|
|
orgContext = authtypes.NewOrgSessionContext(org.ID, org.Name).AddPasswordAuthNSupport(authtypes.AuthNProviderEmailPassword).AddWarning(err)
|
|
}
|
|
|
|
context = context.AddOrgContext(orgContext)
|
|
}
|
|
|
|
return context, nil
|
|
}
|
|
|
|
context.Exists = true
|
|
for _, user := range users {
|
|
idx := slices.IndexFunc(orgs, func(org *types.Organization) bool {
|
|
return org.ID == user.OrgID
|
|
})
|
|
|
|
if idx == -1 {
|
|
continue
|
|
}
|
|
|
|
org := orgs[idx]
|
|
orgContext, err := module.getOrgSessionContext(ctx, org, name, siteURL)
|
|
if err != nil {
|
|
// For some reason, there was an error in getting the org session context. Instead of failing the context call, we create a PasswordAuthNSupport for the org and add a warning.
|
|
orgContext = authtypes.NewOrgSessionContext(org.ID, org.Name).AddPasswordAuthNSupport(authtypes.AuthNProviderEmailPassword).AddWarning(err)
|
|
}
|
|
|
|
context = context.AddOrgContext(orgContext)
|
|
}
|
|
|
|
return context, nil
|
|
}
|
|
|
|
func (module *module) CreatePasswordAuthNSession(ctx context.Context, authNProvider authtypes.AuthNProvider, email valuer.Email, password string, orgID valuer.UUID) (*authtypes.Token, error) {
|
|
passwordAuthN, err := getProvider[authn.PasswordAuthN](authNProvider, module.authNs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
identity, err := passwordAuthN.Authenticate(ctx, email.String(), password, orgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return module.tokenizer.CreateToken(ctx, identity, map[string]string{})
|
|
}
|
|
|
|
func (module *module) CreateCallbackAuthNSession(ctx context.Context, authNProvider authtypes.AuthNProvider, values url.Values) (string, error) {
|
|
callbackAuthN, err := getProvider[authn.CallbackAuthN](authNProvider, module.authNs)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
callbackIdentity, err := callbackAuthN.HandleCallback(ctx, values)
|
|
if err != nil {
|
|
module.settings.Logger().ErrorContext(ctx, "failed to handle callback", errors.Attr(err), slog.Any("authn_provider", authNProvider))
|
|
return "", err
|
|
}
|
|
|
|
authDomain, err := module.authDomain.GetByOrgIDAndID(ctx, callbackIdentity.OrgID, callbackIdentity.State.DomainID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
roleMapping := authDomain.AuthDomainConfig().RoleMapping
|
|
|
|
roleAttributeExists := false
|
|
if roleMapping != nil && roleMapping.UseRoleAttribute && callbackIdentity.Role != "" {
|
|
_, err := module.authz.GetByOrgIDAndName(ctx, callbackIdentity.OrgID, authtypes.NormalizeRoleName(callbackIdentity.Role))
|
|
if err == nil {
|
|
roleAttributeExists = true
|
|
}
|
|
}
|
|
|
|
roleNames := roleMapping.NewRolesFromCallbackIdentity(callbackIdentity, roleAttributeExists)
|
|
|
|
newUser, err := types.NewUser(callbackIdentity.Name, callbackIdentity.Email, callbackIdentity.OrgID, types.UserStatusActive)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
newUser, err = module.userSetter.GetOrCreateUser(ctx, newUser, user.WithRoleNames(roleNames))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if err := newUser.ErrIfRoot(); err != nil {
|
|
return "", errors.WithAdditionalf(err, "root user can only authenticate via password")
|
|
}
|
|
|
|
token, err := module.tokenizer.CreateToken(ctx, authtypes.NewPrincipalUserIdentity(newUser.ID, newUser.OrgID, newUser.Email, authtypes.IdentNProviderTokenizer), map[string]string{})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
redirectURL := &url.URL{
|
|
Scheme: callbackIdentity.State.URL.Scheme,
|
|
Host: callbackIdentity.State.URL.Host,
|
|
Path: callbackIdentity.State.URL.Path,
|
|
RawQuery: authtypes.NewURLValuesFromToken(token, module.GetRotationInterval(ctx)).Encode(),
|
|
}
|
|
|
|
return redirectURL.String(), nil
|
|
}
|
|
|
|
func (module *module) RotateSession(ctx context.Context, accessToken string, refreshToken string) (*authtypes.Token, error) {
|
|
return module.tokenizer.RotateToken(ctx, accessToken, refreshToken)
|
|
}
|
|
|
|
func (module *module) DeleteSession(ctx context.Context, accessToken string) error {
|
|
return module.tokenizer.DeleteToken(ctx, accessToken)
|
|
}
|
|
|
|
func (module *module) GetRotationInterval(context.Context) time.Duration {
|
|
return module.tokenizer.Config().Rotation.Interval
|
|
}
|
|
|
|
func (module *module) getOrgSessionContext(ctx context.Context, org *types.Organization, name string, siteURL *url.URL) (*authtypes.OrgSessionContext, error) {
|
|
authDomain, err := module.authDomain.GetByNameAndOrgID(ctx, name, org.ID)
|
|
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
|
return nil, err
|
|
}
|
|
|
|
if authDomain == nil {
|
|
return authtypes.NewOrgSessionContext(org.ID, org.Name).AddPasswordAuthNSupport(authtypes.AuthNProviderEmailPassword), nil
|
|
}
|
|
|
|
if !authDomain.AuthDomainConfig().SSOEnabled {
|
|
return authtypes.NewOrgSessionContext(org.ID, org.Name).AddPasswordAuthNSupport(authtypes.AuthNProviderEmailPassword), nil
|
|
}
|
|
|
|
provider, err := getProvider[authn.CallbackAuthN](authDomain.AuthDomainConfig().AuthNProvider, module.authNs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
loginURL, err := provider.LoginURL(ctx, siteURL, authDomain)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return authtypes.NewOrgSessionContext(org.ID, org.Name).AddCallbackAuthNSupport(authDomain.AuthDomainConfig().AuthNProvider, loginURL), nil
|
|
}
|
|
|
|
func getProvider[T authn.AuthN](authNProvider authtypes.AuthNProvider, authNs map[authtypes.AuthNProvider]authn.AuthN) (T, error) {
|
|
var provider T
|
|
|
|
provider, ok := authNs[authNProvider].(T)
|
|
if !ok {
|
|
return provider, errors.New(errors.TypeNotFound, errors.CodeNotFound, "authn provider not found")
|
|
}
|
|
|
|
return provider, nil
|
|
}
|