2022-08-15 17:42:33 +00:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
2022-10-03 16:29:01 +00:00
|
|
|
// 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.
|
2022-08-15 17:42:33 +00:00
|
|
|
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:
|
2023-03-01 23:18:40 +00:00
|
|
|
// If the user exists, we want to update the user roles from the attributes received
|
|
|
|
// in the SAMLResponse.
|
|
|
|
|
2023-05-30 20:49:59 +00:00
|
|
|
// If JIT provisioning is disabled, then Fleet does not attempt to change
|
|
|
|
// the role of the existing user.
|
2023-06-07 19:06:36 +00:00
|
|
|
if config.SSOSettings == nil || !config.SSOSettings.EnableJITProvisioning {
|
2023-03-01 23:18:40 +00:00
|
|
|
return user, nil
|
|
|
|
}
|
|
|
|
|
2023-05-30 20:49:59 +00:00
|
|
|
// 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)
|
2023-03-01 23:18:40 +00:00
|
|
|
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")
|
|
|
|
}
|
2022-08-15 17:42:33 +00:00
|
|
|
return user, nil
|
|
|
|
case errors.As(err, &nfe):
|
2023-06-07 19:06:36 +00:00
|
|
|
if config.SSOSettings == nil || !config.SSOSettings.EnableJITProvisioning {
|
2022-08-15 17:42:33 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
displayName := auth.UserDisplayName()
|
|
|
|
if displayName == "" {
|
|
|
|
displayName = auth.UserID()
|
|
|
|
}
|
|
|
|
|
2023-05-30 20:49:59 +00:00
|
|
|
var (
|
|
|
|
globalRole *string
|
|
|
|
teamRoles []fleet.UserTeam
|
|
|
|
)
|
|
|
|
// Attempt to retrieve user roles from SAML custom attributes.
|
|
|
|
ssoRolesInfo, err := fleet.RolesFromSSOAttributes(auth.AssertionAttributes())
|
2023-03-01 23:18:40 +00:00
|
|
|
if err != nil {
|
2023-05-30 20:49:59 +00:00
|
|
|
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)
|
2023-03-01 23:18:40 +00:00
|
|
|
}
|
|
|
|
|
2022-08-15 17:42:33 +00:00
|
|
|
user, err = svc.Service.NewUser(ctx, fleet.UserPayload{
|
|
|
|
Name: &displayName,
|
|
|
|
Email: ptr.String(auth.UserID()),
|
|
|
|
SSOEnabled: ptr.Bool(true),
|
2023-03-01 23:18:40 +00:00
|
|
|
GlobalRole: globalRole,
|
|
|
|
Teams: &teamRoles,
|
2022-08-15 17:42:33 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "creating new SSO user")
|
|
|
|
}
|
2022-12-23 22:04:13 +00:00
|
|
|
if err := svc.ds.NewActivity(
|
2022-08-15 17:42:33 +00:00
|
|
|
ctx,
|
|
|
|
user,
|
2022-12-23 16:05:16 +00:00
|
|
|
fleet.ActivityTypeUserAddedBySSO{},
|
2022-12-23 22:04:13 +00:00
|
|
|
); err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "create activity for SSO user creation")
|
2022-08-15 17:42:33 +00:00
|
|
|
}
|
|
|
|
return user, nil
|
|
|
|
}
|
2023-03-01 23:18:40 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2023-05-30 20:49:59 +00:00
|
|
|
// 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) {
|
2023-03-01 23:18:40 +00:00
|
|
|
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
|
|
|
|
}
|