mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
2a532ede94
#11266 PS: I first attempted a serialization trick by introducing a new `appConfigResponse` and implementing `json.Marshal` to exclude these fields but it was too hacky and hard to maintain moving forward, so I'm bitting the bullet now. Happy to hear other ideas. - [X] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - ~[ ] Documented any API changes (docs/Using-Fleet/REST-API.md or docs/Contributing/API-for-contributors.md)~ - ~[ ] Documented any permissions changes~ - ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements)~ - ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features.~ - [X] Added/updated tests - [X] Manual QA for all new/changed functionality - ~For Orbit and Fleet Desktop changes:~ - ~[ ] Manual QA must be performed in the three main OSs, macOS, Windows and Linux.~ - ~[ ] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
171 lines
5.4 KiB
Go
171 lines
5.4 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
)
|
|
|
|
// GetSSOUser is the premium implementation of svc.GetSSOUser, it allows to
|
|
// create users during the SSO flow the first time they log in if
|
|
// config.SSOSettings.EnableJITProvisioning is `true`
|
|
func (svc *Service) GetSSOUser(ctx context.Context, auth fleet.Auth) (*fleet.User, error) {
|
|
config, err := svc.ds.AppConfig(ctx)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "getting app config")
|
|
}
|
|
|
|
// despite the fact that svc.NewUser will also validate the
|
|
// email, we do it here to avoid hitting the database early if
|
|
// the email happens to be invalid.
|
|
if err := fleet.ValidateEmail(auth.UserID()); err != nil {
|
|
return nil, ctxerr.New(ctx, "validating SSO response")
|
|
}
|
|
|
|
user, err := svc.Service.GetSSOUser(ctx, auth)
|
|
var nfe fleet.NotFoundError
|
|
switch {
|
|
case err == nil:
|
|
// If the user exists, we want to update the user roles from the attributes received
|
|
// in the SAMLResponse.
|
|
|
|
// If JIT provisioning is disabled, then Fleet does not attempt to change
|
|
// the role of the existing user.
|
|
if config.SSOSettings == nil || !config.SSOSettings.EnableJITProvisioning {
|
|
return user, nil
|
|
}
|
|
|
|
// Load custom roles from SSO attributes.
|
|
ssoRolesInfo, err := fleet.RolesFromSSOAttributes(auth.AssertionAttributes())
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "invalid SSO attributes")
|
|
}
|
|
if !ssoRolesInfo.IsSet() {
|
|
// If role attributes were not set, then there's nothing to do here.
|
|
return user, nil
|
|
}
|
|
|
|
newGlobalRole, newTeamsRoles, err := svc.userRolesFromSSOAttributes(ctx, ssoRolesInfo)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "user roles from SSO attributes")
|
|
}
|
|
oldGlobalRole := user.GlobalRole
|
|
oldTeamsRoles := user.Teams
|
|
|
|
// rolesChanged assumes that there cannot be multiple role entries for the same team,
|
|
// which is ok because the "old" values comes from the database and the "new" values
|
|
// come from fleet.RolesFromSSOAttributes which already checks for duplicates.
|
|
if !rolesChanged(oldGlobalRole, oldTeamsRoles, newGlobalRole, newTeamsRoles) {
|
|
// Roles haven't changed, so nothing to do.
|
|
return user, nil
|
|
}
|
|
|
|
user.GlobalRole = newGlobalRole
|
|
user.Teams = newTeamsRoles
|
|
|
|
err = svc.ds.SaveUser(ctx, user)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "save user")
|
|
}
|
|
if err := fleet.LogRoleChangeActivities(ctx, svc.ds, user, oldGlobalRole, oldTeamsRoles, user); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "log activities for role change")
|
|
}
|
|
return user, nil
|
|
case errors.As(err, &nfe):
|
|
if config.SSOSettings == nil || !config.SSOSettings.EnableJITProvisioning {
|
|
return nil, err
|
|
}
|
|
default:
|
|
return nil, err
|
|
}
|
|
|
|
displayName := auth.UserDisplayName()
|
|
if displayName == "" {
|
|
displayName = auth.UserID()
|
|
}
|
|
|
|
var (
|
|
globalRole *string
|
|
teamRoles []fleet.UserTeam
|
|
)
|
|
// Attempt to retrieve user roles from SAML custom attributes.
|
|
ssoRolesInfo, err := fleet.RolesFromSSOAttributes(auth.AssertionAttributes())
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "invalid SSO attributes")
|
|
}
|
|
if ssoRolesInfo.IsSet() {
|
|
globalRole, teamRoles, err = svc.userRolesFromSSOAttributes(ctx, ssoRolesInfo)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "user roles from SSO attributes")
|
|
}
|
|
} else {
|
|
// If no roles are set in the SSO attributes, default to setting user as a global observer.
|
|
globalRole = ptr.String(fleet.RoleObserver)
|
|
}
|
|
|
|
user, err = svc.Service.NewUser(ctx, fleet.UserPayload{
|
|
Name: &displayName,
|
|
Email: ptr.String(auth.UserID()),
|
|
SSOEnabled: ptr.Bool(true),
|
|
GlobalRole: globalRole,
|
|
Teams: &teamRoles,
|
|
})
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "creating new SSO user")
|
|
}
|
|
if err := svc.ds.NewActivity(
|
|
ctx,
|
|
user,
|
|
fleet.ActivityTypeUserAddedBySSO{},
|
|
); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "create activity for SSO user creation")
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
// rolesChanged checks whether there was any change between the old and new roles.
|
|
//
|
|
// rolesChanged assumes that there cannot be multiple role entries for the same team.
|
|
func rolesChanged(oldGlobal *string, oldTeams []fleet.UserTeam, newGlobal *string, newTeams []fleet.UserTeam) bool {
|
|
if (newGlobal != nil && (oldGlobal == nil || *oldGlobal != *newGlobal)) || (newGlobal == nil && oldGlobal != nil) {
|
|
return true
|
|
}
|
|
if len(oldTeams) != len(newTeams) {
|
|
return true
|
|
}
|
|
oldTeamsMap := make(map[uint]fleet.UserTeam, len(oldTeams))
|
|
for _, oldTeam := range oldTeams {
|
|
oldTeamsMap[oldTeam.Team.ID] = oldTeam
|
|
}
|
|
for _, newTeam := range newTeams {
|
|
oldTeam, ok := oldTeamsMap[newTeam.Team.ID]
|
|
if !ok {
|
|
return true
|
|
}
|
|
if oldTeam.Role != newTeam.Role {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// userRolesFromSSOAttributes returns `globalRole` and `teamRoles` ready to be assigned
|
|
// to a `fleet.User` struct fields `GlobalRole` and `Teams` respectively.
|
|
func (svc *Service) userRolesFromSSOAttributes(ctx context.Context, ssoRolesInfo fleet.SSORolesInfo) (globalRole *string, teamsRoles []fleet.UserTeam, err error) {
|
|
for _, teamRole := range ssoRolesInfo.Teams {
|
|
team, err := svc.ds.Team(ctx, teamRole.ID)
|
|
if err != nil {
|
|
return nil, nil, ctxerr.Wrap(ctx, err, "invalid team")
|
|
}
|
|
teamsRoles = append(teamsRoles, fleet.UserTeam{
|
|
Team: *team,
|
|
Role: teamRole.Role,
|
|
})
|
|
}
|
|
|
|
return ssoRolesInfo.Global, teamsRoles, nil
|
|
}
|