mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 08:58:41 +00:00
#10784 The removal of the now deprecated `sso_settings.enable_jit_role_sync` config will be tackled in: #10688. - [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)).~
184 lines
5.6 KiB
Go
184 lines
5.6 KiB
Go
package fleet
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
)
|
|
|
|
// Auth contains methods to fetch information from a valid SSO auth response
|
|
type Auth interface {
|
|
// UserID returns the Subject Name Identifier associated with the request,
|
|
// this can be an email address, an entity identifier, or any other valid
|
|
// Name Identifier as described in the spec:
|
|
// http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
|
|
//
|
|
// Fleet requires users to configure this value to be the email of the Subject
|
|
UserID() string
|
|
// UserDisplayName finds a display name in the SSO response Attributes, there
|
|
// isn't a defined spec for this, so the return value is in a best-effort
|
|
// basis
|
|
UserDisplayName() string
|
|
// RequestID returns the request id associated with this SSO session
|
|
RequestID() string
|
|
// AssertionAttributes returns the attributes of the SAML response.
|
|
AssertionAttributes() []SAMLAttribute
|
|
}
|
|
|
|
// SAMLAttribute holds the name and values of a custom attribute.
|
|
type SAMLAttribute struct {
|
|
Name string
|
|
Values []SAMLAttributeValue
|
|
}
|
|
|
|
// SAMLAttributeValue holds the type and value of a custom attribute.
|
|
type SAMLAttributeValue struct {
|
|
// Type is the type of attribute value.
|
|
Type string
|
|
// Value is the actual value of the attribute.
|
|
Value string
|
|
}
|
|
|
|
type SSOSession struct {
|
|
Token string
|
|
RedirectURL string
|
|
}
|
|
|
|
// SessionSSOSettings SSO information used prior to authentication.
|
|
type SessionSSOSettings struct {
|
|
// IDPName is a human readable name for the IDP
|
|
IDPName string `json:"idp_name"`
|
|
// IDPImageURL https link to a logo image for the IDP.
|
|
IDPImageURL string `json:"idp_image_url"`
|
|
// SSOEnabled true if single sign on is enabled.
|
|
SSOEnabled bool `json:"sso_enabled"`
|
|
}
|
|
|
|
// Session is the model object which represents what an active session is
|
|
type Session struct {
|
|
CreateTimestamp
|
|
ID uint
|
|
AccessedAt time.Time `db:"accessed_at"`
|
|
UserID uint `json:"user_id" db:"user_id"`
|
|
Key string
|
|
APIOnly *bool `json:"-" db:"api_only"`
|
|
}
|
|
|
|
func (s Session) AuthzType() string {
|
|
return "session"
|
|
}
|
|
|
|
// SSORolesInfo holds the configuration parsed from SAML custom attributes.
|
|
//
|
|
// `Global` and `Teams` are never both set (by design, users must be either global
|
|
// or member of teams).
|
|
type SSORolesInfo struct {
|
|
// Global holds the role for the Global domain.
|
|
Global *string
|
|
// Teams holds the roles for teams.
|
|
Teams []TeamRole
|
|
}
|
|
|
|
// TeamRole holds a user's role on a Team.
|
|
type TeamRole struct {
|
|
// ID is the unique identifier of the team.
|
|
ID uint
|
|
// Role is the role of the user in the team.
|
|
Role string
|
|
}
|
|
|
|
func (s SSORolesInfo) verify() error {
|
|
if s.Global != nil && len(s.Teams) > 0 {
|
|
return errors.New("cannot set both global and team roles")
|
|
}
|
|
// Check for duplicate entries for the same team.
|
|
// This is just in case some IdP allows duplicating attributes.
|
|
teamSet := make(map[uint]struct{})
|
|
for _, teamRole := range s.Teams {
|
|
if _, ok := teamSet[teamRole.ID]; ok {
|
|
return fmt.Errorf("duplicate team entry: %d", teamRole.ID)
|
|
}
|
|
teamSet[teamRole.ID] = struct{}{}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IsSet returns whether any role attributes were set.
|
|
func (s SSORolesInfo) IsSet() bool {
|
|
return s.Global != nil || len(s.Teams) != 0
|
|
}
|
|
|
|
const (
|
|
globalUserRoleSSOAttrName = "FLEET_JIT_USER_ROLE_GLOBAL"
|
|
teamUserRoleSSOAttrNamePrefix = "FLEET_JIT_USER_ROLE_TEAM_"
|
|
ssoAttrNullRoleValue = "null"
|
|
)
|
|
|
|
// RolesFromSSOAttributes loads Global and Team roles from SAML custom attributes.
|
|
// - Custom attribute `FLEET_JIT_USER_ROLE_GLOBAL` is used for setting global role.
|
|
// - Custom attributes of the form `FLEET_JIT_USER_ROLE_TEAM_<TEAM_ID>` are used
|
|
// for setting role for a team with ID <TEAM_ID>.
|
|
//
|
|
// For both attributes currently supported values are `admin`, `maintainer`, `observer`,
|
|
// `observer_plus` and `null`. A `null` value is used to ignore the attribute.
|
|
func RolesFromSSOAttributes(attributes []SAMLAttribute) (SSORolesInfo, error) {
|
|
ssoRolesInfo := SSORolesInfo{}
|
|
for _, attribute := range attributes {
|
|
switch {
|
|
case attribute.Name == globalUserRoleSSOAttrName:
|
|
role, err := parseRole(attribute.Values)
|
|
if err != nil {
|
|
return SSORolesInfo{}, fmt.Errorf("parse global role: %w", err)
|
|
}
|
|
if role == ssoAttrNullRoleValue {
|
|
// If the role is set to the null value then the attribute is ignored.
|
|
continue
|
|
}
|
|
ssoRolesInfo.Global = ptr.String(role)
|
|
case strings.HasPrefix(attribute.Name, teamUserRoleSSOAttrNamePrefix):
|
|
teamIDSuffix := strings.TrimPrefix(attribute.Name, teamUserRoleSSOAttrNamePrefix)
|
|
teamID, err := strconv.ParseUint(teamIDSuffix, 10, 32)
|
|
if err != nil {
|
|
return SSORolesInfo{}, fmt.Errorf("parse team ID: %w", err)
|
|
}
|
|
teamRole, err := parseRole(attribute.Values)
|
|
if err != nil {
|
|
return SSORolesInfo{}, fmt.Errorf("parse team role: %w", err)
|
|
}
|
|
if teamRole == ssoAttrNullRoleValue {
|
|
// If the role is set to the null value then the attribute is ignored.
|
|
continue
|
|
}
|
|
ssoRolesInfo.Teams = append(ssoRolesInfo.Teams, TeamRole{
|
|
ID: uint(teamID),
|
|
Role: teamRole,
|
|
})
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
if err := ssoRolesInfo.verify(); err != nil {
|
|
return SSORolesInfo{}, err
|
|
}
|
|
return ssoRolesInfo, nil
|
|
}
|
|
|
|
func parseRole(values []SAMLAttributeValue) (string, error) {
|
|
if len(values) == 0 {
|
|
return "", errors.New("empty role")
|
|
}
|
|
// Using last value by default.
|
|
value := values[len(values)-1].Value
|
|
if value != RoleAdmin &&
|
|
value != RoleMaintainer &&
|
|
value != RoleObserver &&
|
|
value != RoleObserverPlus &&
|
|
value != ssoAttrNullRoleValue {
|
|
return "", fmt.Errorf("invalid role: %s", value)
|
|
}
|
|
return value, nil
|
|
}
|