mirror of
https://github.com/fleetdm/fleet
synced 2026-05-18 14:38:53 +00:00
Fixes #31897. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) ## Testing - [ ] Added/updated automated tests - [ ] QA'd all new/changed functionality manually ## New Fleet configuration settings - [ ] Verified that the setting is exported via `fleetctl generate-gitops` - [x] Verified the setting is documented in a separate PR to [the GitOps documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485) - [ ] Verified that the setting is cleared on the server if it is not supplied in a YAML file (or that it is documented as being optional) - [x] Verified that any relevant UI is disabled when GitOps mode is enabled <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - GitOps now supports software icons: generate and include icon files/paths in specs for packages and App Store apps. - CLI adds flags to control concurrent icon uploads/updates. - Icons are uploaded, updated, or deleted automatically during GitOps runs. - UI YAML modal now includes icon_url and offers icon download. - Improvements - Robust path resolution for icon assets across specs. - Non-YAML outputs handle both string and byte file contents. - Bug Fixes - Removes stale icons after App Store app re-association. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Scott Gress <scottmgress@gmail.com> Co-authored-by: Scott Gress <scott@fleetdm.com> Co-authored-by: Jahziel Villasana-Espinoza <jahziel@fleetdm.com>
638 lines
22 KiB
Go
638 lines
22 KiB
Go
package fleet
|
|
|
|
import (
|
|
"database/sql/driver"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"golang.org/x/text/unicode/norm"
|
|
)
|
|
|
|
const (
|
|
RoleAdmin = "admin"
|
|
RoleMaintainer = "maintainer"
|
|
RoleObserver = "observer"
|
|
RoleObserverPlus = "observer_plus"
|
|
RoleGitOps = "gitops"
|
|
TeamNameNoTeam = "No team"
|
|
TeamNameAllTeams = "All teams"
|
|
)
|
|
|
|
const (
|
|
ReservedNameAllTeams = "All teams"
|
|
ReservedNameNoTeam = "No team"
|
|
)
|
|
|
|
// IsReservedTeamName checks if the name provided is a reserved team name
|
|
func IsReservedTeamName(name string) bool {
|
|
normalizedName := norm.NFC.String(name)
|
|
return normalizedName == ReservedNameAllTeams || normalizedName == ReservedNameNoTeam
|
|
}
|
|
|
|
type TeamPayload struct {
|
|
Name *string `json:"name"`
|
|
Description *string `json:"description"`
|
|
Secrets []*EnrollSecret `json:"secrets"`
|
|
WebhookSettings *TeamWebhookSettings `json:"webhook_settings"`
|
|
Integrations *TeamIntegrations `json:"integrations"`
|
|
MDM *TeamPayloadMDM `json:"mdm"`
|
|
HostExpirySettings *HostExpirySettings `json:"host_expiry_settings"`
|
|
// Note AgentOptions must be set by a separate endpoint.
|
|
}
|
|
|
|
// TeamPayloadMDM is a distinct struct than TeamMDM because in ModifyTeam we
|
|
// need to be able which part of the MDM config was provided in the request,
|
|
// so the fields are pointers to structs.
|
|
type TeamPayloadMDM struct {
|
|
EnableDiskEncryption optjson.Bool `json:"enable_disk_encryption"`
|
|
// RequireBitLockerPIN indicates whether BitLocker PIN is required for Windows devices
|
|
// in order for Fleet to consider them compliant.
|
|
RequireBitLockerPIN optjson.Bool `json:"windows_require_bitlocker_pin"`
|
|
|
|
// MacOSUpdates defines the OS update settings for macOS devices.
|
|
MacOSUpdates *AppleOSUpdateSettings `json:"macos_updates"`
|
|
// IOSUpdates defines the OS update settings for iOS devices.
|
|
IOSUpdates *AppleOSUpdateSettings `json:"ios_updates"`
|
|
// IPadOSUpdates defines the OS update settings for iPadOS devices.
|
|
IPadOSUpdates *AppleOSUpdateSettings `json:"ipados_updates"`
|
|
// WindowsUpdates defines the OS update settings for Windows devices.
|
|
WindowsUpdates *WindowsUpdates `json:"windows_updates"`
|
|
|
|
MacOSSetup *MacOSSetup `json:"macos_setup"`
|
|
}
|
|
|
|
// Team is the data representation for the "Team" concept (group of hosts and
|
|
// group of users that can perform operations on those hosts).
|
|
type Team struct {
|
|
// Directly in DB
|
|
|
|
// ID is the database ID.
|
|
ID uint `json:"id" db:"id"`
|
|
Filename *string `json:"gitops_filename,omitempty" db:"filename"`
|
|
// CreatedAt is the timestamp of the label creation.
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
// Name is the human friendly name of the team.
|
|
Name string `json:"name" db:"name"`
|
|
// Description is an optional description for the team.
|
|
Description string `json:"description" db:"description"`
|
|
Config TeamConfig `json:"-" db:"config"` // see json.MarshalJSON/UnmarshalJSON implementations
|
|
|
|
// Derived from JOINs
|
|
|
|
// UserCount is the count of users with explicit roles on this team.
|
|
UserCount int `json:"user_count" db:"user_count"`
|
|
// Users is the users that have a role on this team.
|
|
Users []TeamUser `json:"users,omitempty"`
|
|
// UserCount is the count of hosts assigned to this team.
|
|
HostCount int `json:"host_count" db:"host_count"`
|
|
// Hosts are the hosts assigned to the team.
|
|
Hosts []Host `json:"hosts,omitempty"`
|
|
// Secrets is the enroll secrets valid for this team.
|
|
Secrets []*EnrollSecret `json:"secrets,omitempty"`
|
|
}
|
|
|
|
func (t Team) MarshalJSON() ([]byte, error) {
|
|
// The reason for not embedding TeamConfig above, is that it also implements sql.Scanner/Valuer.
|
|
// We do not want it be promoted to the parent struct, because it causes issues when using sqlx for scanning.
|
|
// Also need to implement json.Marshaler/Unmarshaler on each type that embeds Team so because it will be promoted
|
|
// to the parent struct.
|
|
x := struct {
|
|
ID uint `json:"id"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
TeamConfig // inline this using struct embedding
|
|
UserCount int `json:"user_count"`
|
|
Users []TeamUser `json:"users,omitempty"`
|
|
HostCount int `json:"host_count"`
|
|
Hosts []HostResponse `json:"hosts,omitempty"`
|
|
Secrets []*EnrollSecret `json:"secrets,omitempty"`
|
|
}{
|
|
ID: t.ID,
|
|
CreatedAt: t.CreatedAt,
|
|
Name: t.Name,
|
|
Description: t.Description,
|
|
TeamConfig: t.Config,
|
|
UserCount: t.UserCount,
|
|
Users: t.Users,
|
|
HostCount: t.HostCount,
|
|
Hosts: HostResponsesForHostsCheap(t.Hosts),
|
|
Secrets: t.Secrets,
|
|
}
|
|
|
|
return json.Marshal(x)
|
|
}
|
|
|
|
func (t *Team) UnmarshalJSON(b []byte) error {
|
|
var x struct {
|
|
ID uint `json:"id"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
TeamConfig // inline this using struct embedding
|
|
UserCount int `json:"user_count"`
|
|
Users []TeamUser `json:"users,omitempty"`
|
|
HostCount int `json:"host_count"`
|
|
Hosts []Host `json:"hosts,omitempty"`
|
|
Secrets []*EnrollSecret `json:"secrets,omitempty"`
|
|
}
|
|
|
|
if err := json.Unmarshal(b, &x); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !x.MDM.MacOSSetup.EnableReleaseDeviceManually.Valid {
|
|
x.MDM.MacOSSetup.EnableReleaseDeviceManually = optjson.SetBool(false)
|
|
}
|
|
*t = Team{
|
|
ID: x.ID,
|
|
CreatedAt: x.CreatedAt,
|
|
Name: x.Name,
|
|
Description: x.Description,
|
|
Config: x.TeamConfig,
|
|
UserCount: x.UserCount,
|
|
Users: x.Users,
|
|
HostCount: x.HostCount,
|
|
Hosts: x.Hosts,
|
|
Secrets: x.Secrets,
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type TeamConfig struct {
|
|
// AgentOptions is the options for osquery and Orbit.
|
|
AgentOptions *json.RawMessage `json:"agent_options,omitempty"`
|
|
HostExpirySettings HostExpirySettings `json:"host_expiry_settings"`
|
|
WebhookSettings TeamWebhookSettings `json:"webhook_settings"`
|
|
Integrations TeamIntegrations `json:"integrations"`
|
|
Features Features `json:"features"`
|
|
MDM TeamMDM `json:"mdm"`
|
|
Scripts optjson.Slice[string] `json:"scripts,omitempty"`
|
|
Software *SoftwareSpec `json:"software,omitempty"`
|
|
}
|
|
|
|
type TeamWebhookSettings struct {
|
|
// HostStatusWebhook can be nil to match the TeamSpec webhook settings
|
|
HostStatusWebhook *HostStatusWebhookSettings `json:"host_status_webhook"`
|
|
FailingPoliciesWebhook FailingPoliciesWebhookSettings `json:"failing_policies_webhook"`
|
|
}
|
|
|
|
// DefaultTeam represents the limited team information returned for team ID 0
|
|
type DefaultTeam struct {
|
|
ID uint `json:"id"`
|
|
Name string `json:"name"`
|
|
DefaultTeamConfig // Embedded struct - fields appear at top level in JSON
|
|
}
|
|
|
|
type DefaultTeamConfig struct {
|
|
WebhookSettings DefaultTeamWebhookSettings `json:"webhook_settings"`
|
|
Integrations DefaultTeamIntegrations `json:"integrations"`
|
|
}
|
|
|
|
// DefaultTeamWebhookSettings contains webhook settings for team ID 0
|
|
type DefaultTeamWebhookSettings struct {
|
|
FailingPoliciesWebhook FailingPoliciesWebhookSettings `json:"failing_policies_webhook"`
|
|
}
|
|
|
|
// DefaultTeamIntegrations contains only the integrations supported for team ID 0
|
|
type DefaultTeamIntegrations struct {
|
|
Jira []*TeamJiraIntegration `json:"jira"`
|
|
Zendesk []*TeamZendeskIntegration `json:"zendesk"`
|
|
}
|
|
|
|
type TeamSpecSoftwareAsset struct {
|
|
Path string `json:"path"`
|
|
}
|
|
|
|
type TeamSpecAppStoreApp struct {
|
|
AppStoreID string `json:"app_store_id"`
|
|
SelfService bool `json:"self_service"`
|
|
LabelsIncludeAny []string `json:"labels_include_any"`
|
|
LabelsExcludeAny []string `json:"labels_exclude_any"`
|
|
// Categories is the list of names of software categories associated with this VPP app.
|
|
Categories []string `json:"categories"`
|
|
// InstallDuringSetup indicates whether a package should be incorporated into setup experience;
|
|
// if not supplied (Valid field is false) then the server-side value for setup experience membership
|
|
// is not changed, for compatibility with the old fleetctl apply format
|
|
InstallDuringSetup optjson.Bool `json:"setup_experience"`
|
|
Icon TeamSpecSoftwareAsset `json:"icon"`
|
|
}
|
|
|
|
func (spec TeamSpecAppStoreApp) ResolvePaths(baseDir string) TeamSpecAppStoreApp {
|
|
spec.Icon.Path = resolveApplyRelativePath(baseDir, spec.Icon.Path)
|
|
|
|
return spec
|
|
}
|
|
|
|
type TeamMDM struct {
|
|
EnableDiskEncryption bool `json:"enable_disk_encryption"`
|
|
RequireBitLockerPIN bool `json:"windows_require_bitlocker_pin"`
|
|
MacOSUpdates AppleOSUpdateSettings `json:"macos_updates"`
|
|
IOSUpdates AppleOSUpdateSettings `json:"ios_updates"`
|
|
IPadOSUpdates AppleOSUpdateSettings `json:"ipados_updates"`
|
|
WindowsUpdates WindowsUpdates `json:"windows_updates"`
|
|
MacOSSettings MacOSSettings `json:"macos_settings"`
|
|
MacOSSetup MacOSSetup `json:"macos_setup"`
|
|
|
|
WindowsSettings WindowsSettings `json:"windows_settings"`
|
|
|
|
AndroidSettings AndroidSettings `json:"android_settings"`
|
|
// NOTE: TeamSpecMDM must be kept in sync with TeamMDM.
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
// WARNING: If you add to this struct make sure it's taken into
|
|
// account in the TeamMDM Clone implementation!
|
|
/////////////////////////////////////////////////////////////////
|
|
}
|
|
|
|
// Clone implements cloner for TeamMDM.
|
|
func (t *TeamMDM) Clone() (Cloner, error) {
|
|
return t.Copy(), nil
|
|
}
|
|
|
|
// Copy returns a deep copy of the TeamMDM.
|
|
func (t *TeamMDM) Copy() *TeamMDM {
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
|
|
clone := *t
|
|
|
|
// EnableDiskEncryption, MacOSUpdates and MacOSSetup don't have fields that
|
|
// require cloning (all fields are basic value types, no
|
|
// pointers/slices/maps).
|
|
|
|
if t.MacOSSettings.CustomSettings != nil {
|
|
clone.MacOSSettings.CustomSettings = make([]MDMProfileSpec, len(t.MacOSSettings.CustomSettings))
|
|
for i, mps := range t.MacOSSettings.CustomSettings {
|
|
clone.MacOSSettings.CustomSettings[i] = *mps.Copy()
|
|
}
|
|
}
|
|
if t.MacOSSettings.DeprecatedEnableDiskEncryption != nil {
|
|
clone.MacOSSettings.DeprecatedEnableDiskEncryption = ptr.Bool(*t.MacOSSettings.DeprecatedEnableDiskEncryption)
|
|
}
|
|
if t.WindowsSettings.CustomSettings.Set {
|
|
windowsSettings := make([]MDMProfileSpec, len(t.WindowsSettings.CustomSettings.Value))
|
|
for i, mps := range t.WindowsSettings.CustomSettings.Value {
|
|
windowsSettings[i] = *mps.Copy()
|
|
}
|
|
clone.WindowsSettings.CustomSettings = optjson.SetSlice(windowsSettings)
|
|
}
|
|
if t.AndroidSettings.CustomSettings.Set {
|
|
androidSettings := make([]MDMProfileSpec, len(t.AndroidSettings.CustomSettings.Value))
|
|
for i, mps := range t.AndroidSettings.CustomSettings.Value {
|
|
androidSettings[i] = *mps.Copy()
|
|
}
|
|
clone.AndroidSettings.CustomSettings = optjson.SetSlice(androidSettings)
|
|
}
|
|
if t.MacOSSetup.Software.Set {
|
|
sw := make([]*MacOSSetupSoftware, len(t.MacOSSetup.Software.Value))
|
|
for i, s := range t.MacOSSetup.Software.Value {
|
|
s := *s
|
|
sw[i] = &s
|
|
}
|
|
clone.MacOSSetup.Software = optjson.SetSlice(sw)
|
|
}
|
|
return &clone
|
|
}
|
|
|
|
type TeamSpecMDM struct {
|
|
EnableDiskEncryption optjson.Bool `json:"enable_disk_encryption"`
|
|
// RequireBitLockerPIN indicates whether BitLocker PIN is required for Windows devices
|
|
// in order for Fleet to consider them compliant.
|
|
RequireBitLockerPIN optjson.Bool `json:"windows_require_bitlocker_pin"`
|
|
|
|
// MacOSUpdates defines the OS update settings for macOS devices.
|
|
MacOSUpdates AppleOSUpdateSettings `json:"macos_updates"`
|
|
// IOSUpdates defines the OS update settings for iOS devices.
|
|
IOSUpdates AppleOSUpdateSettings `json:"ios_updates"`
|
|
// IPadOSUpdates defines the OS update settings for iPadOS devices.
|
|
IPadOSUpdates AppleOSUpdateSettings `json:"ipados_updates"`
|
|
// WindowsUpdates defines the OS update settings for Windows devices.
|
|
WindowsUpdates WindowsUpdates `json:"windows_updates"`
|
|
|
|
// A map is used for the macos settings so that we can easily detect if its
|
|
// sub-keys were provided or not in an "apply" call. E.g. if the
|
|
// custom_settings key is specified but empty, then we need to clear the
|
|
// value, but if it isn't provided, we need to leave the existing value
|
|
// unmodified.
|
|
MacOSSettings map[string]interface{} `json:"macos_settings"`
|
|
MacOSSetup MacOSSetup `json:"macos_setup"`
|
|
|
|
WindowsSettings WindowsSettings `json:"windows_settings"`
|
|
|
|
AndroidSettings AndroidSettings `json:"android_settings"`
|
|
|
|
// NOTE: TeamMDM must be kept in sync with TeamSpecMDM.
|
|
}
|
|
|
|
// Scan implements the sql.Scanner interface
|
|
func (t *TeamConfig) Scan(val interface{}) error {
|
|
switch v := val.(type) {
|
|
case []byte:
|
|
return json.Unmarshal(v, t)
|
|
case string:
|
|
return json.Unmarshal([]byte(v), t)
|
|
case nil: // sql NULL
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unsupported type: %T", v)
|
|
}
|
|
}
|
|
|
|
// Value implements the sql.Valuer interface
|
|
func (t TeamConfig) Value() (driver.Value, error) {
|
|
// force-save as the default `false` value if not set
|
|
if !t.MDM.MacOSSetup.EnableReleaseDeviceManually.Valid {
|
|
t.MDM.MacOSSetup.EnableReleaseDeviceManually = optjson.SetBool(false)
|
|
}
|
|
return json.Marshal(t)
|
|
}
|
|
|
|
// Copy creates a deep copy of the TeamConfig
|
|
func (t *TeamConfig) Copy() *TeamConfig {
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
|
|
clone := *t
|
|
|
|
// Deep copy AgentOptions if present
|
|
if t.AgentOptions != nil {
|
|
agentOptionsCopy := make(json.RawMessage, len(*t.AgentOptions))
|
|
copy(agentOptionsCopy, *t.AgentOptions)
|
|
clone.AgentOptions = &agentOptionsCopy
|
|
}
|
|
|
|
// Deep copy WebhookSettings
|
|
if t.WebhookSettings.HostStatusWebhook != nil {
|
|
hostStatusCopy := *t.WebhookSettings.HostStatusWebhook
|
|
clone.WebhookSettings.HostStatusWebhook = &hostStatusCopy
|
|
}
|
|
if len(t.WebhookSettings.FailingPoliciesWebhook.PolicyIDs) > 0 {
|
|
clone.WebhookSettings.FailingPoliciesWebhook.PolicyIDs = make([]uint, len(t.WebhookSettings.FailingPoliciesWebhook.PolicyIDs))
|
|
copy(clone.WebhookSettings.FailingPoliciesWebhook.PolicyIDs, t.WebhookSettings.FailingPoliciesWebhook.PolicyIDs)
|
|
}
|
|
|
|
// Deep copy integrations
|
|
clone.Integrations = t.Integrations.Copy()
|
|
|
|
// Deep copy Features
|
|
clone.Features = *t.Features.Copy()
|
|
|
|
// Deep copy all MDM fields (includes macOS/windows custom settings and setup software)
|
|
clone.MDM = *t.MDM.Copy()
|
|
|
|
// Do not copy script and software since they will not be stored/cached in the database.
|
|
clone.Scripts = optjson.Slice[string]{}
|
|
clone.Software = nil
|
|
|
|
return &clone
|
|
}
|
|
|
|
// Clone implements the Cloner interface for cache support
|
|
func (t *TeamConfig) Clone() (Cloner, error) {
|
|
return t.Copy(), nil
|
|
}
|
|
|
|
type TeamSummary struct {
|
|
ID uint `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
func (t Team) AuthzType() string {
|
|
return "team"
|
|
}
|
|
|
|
// TeamUser is a user mapped to a team with a role.
|
|
type TeamUser struct {
|
|
// User is the user object. At least ID must be specified for most uses.
|
|
User
|
|
// Role is the role the user has for the team.
|
|
Role string `json:"role" db:"role"`
|
|
}
|
|
|
|
var teamRoles = map[string]struct{}{
|
|
RoleAdmin: {},
|
|
RoleObserver: {},
|
|
RoleMaintainer: {},
|
|
RoleObserverPlus: {},
|
|
RoleGitOps: {},
|
|
}
|
|
|
|
var premiumTeamRoles = map[string]struct{}{
|
|
RoleObserverPlus: {},
|
|
RoleGitOps: {},
|
|
}
|
|
|
|
// ValidTeamRole returns whether the role provided is valid for a team user.
|
|
func ValidTeamRole(role string) bool {
|
|
_, ok := teamRoles[role]
|
|
return ok
|
|
}
|
|
|
|
var globalRoles = map[string]struct{}{
|
|
RoleObserver: {},
|
|
RoleMaintainer: {},
|
|
RoleAdmin: {},
|
|
RoleObserverPlus: {},
|
|
RoleGitOps: {},
|
|
}
|
|
|
|
var premiumGlobalRoles = map[string]struct{}{
|
|
RoleObserverPlus: {},
|
|
RoleGitOps: {},
|
|
}
|
|
|
|
// ValidGlobalRole returns whether the role provided is valid for a global user.
|
|
func ValidGlobalRole(role string) bool {
|
|
_, ok := globalRoles[role]
|
|
return ok
|
|
}
|
|
|
|
// ValidateRole returns nil if the global and team roles combination is a valid
|
|
// one within fleet, or a fleet Error otherwise.
|
|
func ValidateRole(globalRole *string, teamUsers []UserTeam) error {
|
|
if globalRole == nil || *globalRole == "" {
|
|
if len(teamUsers) == 0 {
|
|
return NewError(ErrNoRoleNeeded, "either global role or team role needs to be defined")
|
|
}
|
|
for _, t := range teamUsers {
|
|
if !ValidTeamRole(t.Role) {
|
|
return NewErrorf(ErrNoRoleNeeded, "invalid team role: %s", t.Role)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if len(teamUsers) > 0 {
|
|
return NewError(ErrNoRoleNeeded, "Cannot specify both Global Role and Team Roles")
|
|
}
|
|
|
|
if !ValidGlobalRole(*globalRole) {
|
|
return NewErrorf(ErrNoRoleNeeded, "invalid global role: %s", *globalRole)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateUserRoles verifies the roles to be applied to a new or existing user.
|
|
//
|
|
// Argument createNew sets whether the user is being created (true) or is being modified (false).
|
|
func ValidateUserRoles(createNew bool, payload UserPayload, license LicenseInfo) error {
|
|
var teamUsers_ []UserTeam
|
|
if payload.Teams != nil {
|
|
teamUsers_ = *payload.Teams
|
|
}
|
|
if err := ValidateRole(payload.GlobalRole, teamUsers_); err != nil {
|
|
return err
|
|
}
|
|
premiumRolesPresent := false
|
|
gitOpsRolePresent := false
|
|
if payload.GlobalRole != nil {
|
|
if *payload.GlobalRole == RoleGitOps {
|
|
gitOpsRolePresent = true
|
|
}
|
|
if _, ok := premiumGlobalRoles[*payload.GlobalRole]; ok {
|
|
premiumRolesPresent = true
|
|
}
|
|
}
|
|
for _, teamUser := range teamUsers_ {
|
|
if teamUser.Role == RoleGitOps {
|
|
gitOpsRolePresent = true
|
|
}
|
|
if _, ok := premiumTeamRoles[teamUser.Role]; ok {
|
|
premiumRolesPresent = true
|
|
}
|
|
}
|
|
if !license.IsPremium() && premiumRolesPresent {
|
|
return ErrMissingLicense
|
|
}
|
|
if gitOpsRolePresent &&
|
|
// New user is not API only.
|
|
((createNew && (payload.APIOnly == nil || !*payload.APIOnly)) ||
|
|
// Removing API only status from existing user.
|
|
(!createNew && payload.APIOnly != nil && !*payload.APIOnly)) {
|
|
return NewErrorf(ErrAPIOnlyRole, "role GitOps can only be set for API only users")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// TeamFilter is the filtering information passed to the datastore for queries
|
|
// that may be filtered by team.
|
|
type TeamFilter struct {
|
|
// User is the user to filter by.
|
|
User *User
|
|
// IncludeObserver determines whether to include teams the user is an observer on.
|
|
IncludeObserver bool
|
|
// TeamID is the specific team id to filter by. If other criteria are
|
|
// specified, they must met too (e.g. if a User is provided, that team ID
|
|
// must be part of their teams).
|
|
TeamID *uint
|
|
}
|
|
|
|
const (
|
|
TeamKind = "team"
|
|
)
|
|
|
|
type TeamSpec struct {
|
|
Name string `json:"name"`
|
|
Filename *string `json:"gitops_filename,omitempty"`
|
|
|
|
// We need to distinguish between the agent_options key being present but
|
|
// "empty" or being absent, as we leave the existing agent options unmodified
|
|
// if it is absent, and we clear it if present but empty.
|
|
//
|
|
// If the agent_options key is not provided, the field will be nil (Go nil).
|
|
// If the agent_options key is present but empty in the YAML, will be set to
|
|
// "null" (JSON null). Otherwise, if the key is present and set, it will be
|
|
// set to the agent options JSON object.
|
|
AgentOptions json.RawMessage `json:"agent_options,omitempty"` // marshals as "null" if omitempty is not set
|
|
HostExpirySettings *HostExpirySettings `json:"host_expiry_settings,omitempty"`
|
|
Secrets *[]EnrollSecret `json:"secrets,omitempty"`
|
|
Features *json.RawMessage `json:"features"`
|
|
MDM TeamSpecMDM `json:"mdm"`
|
|
Scripts optjson.Slice[string] `json:"scripts"`
|
|
WebhookSettings TeamSpecWebhookSettings `json:"webhook_settings"`
|
|
Integrations TeamSpecIntegrations `json:"integrations"`
|
|
Software *SoftwareSpec `json:"software,omitempty"`
|
|
}
|
|
|
|
type TeamSpecWebhookSettings struct {
|
|
HostStatusWebhook *HostStatusWebhookSettings `json:"host_status_webhook"`
|
|
FailingPoliciesWebhook *FailingPoliciesWebhookSettings `json:"failing_policies_webhook"`
|
|
}
|
|
|
|
// TeamSpecIntegrations contains the configuration for external services'
|
|
// integrations for a specific team.
|
|
type TeamSpecIntegrations struct {
|
|
// If value is nil, we don't want to change the existing value.
|
|
GoogleCalendar *TeamGoogleCalendarIntegration `json:"google_calendar"`
|
|
// ConditionalAccessEnabled indicates whether "Conditional access" is enabled/disabled for the team.
|
|
ConditionalAccessEnabled *bool `json:"conditional_access_enabled"`
|
|
}
|
|
|
|
// TeamSpecsDryRunAssumptions holds the assumptions that are made when applying team specs in dry-run mode.
|
|
type TeamSpecsDryRunAssumptions struct {
|
|
WindowsEnabledAndConfigured optjson.Bool `json:"windows_enabled_and_configured,omitempty"`
|
|
AndroidEnabledAndConfigured optjson.Bool `json:"android_enabled_and_configured,omitempty"`
|
|
}
|
|
|
|
// TeamSpecFromTeam returns a TeamSpec constructed from the given Team.
|
|
func TeamSpecFromTeam(t *Team) (*TeamSpec, error) {
|
|
features, err := json.Marshal(t.Config.Features)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
featuresJSON := json.RawMessage(features)
|
|
var secrets []EnrollSecret
|
|
if len(t.Secrets) > 0 {
|
|
secrets = make([]EnrollSecret, 0, len(t.Secrets))
|
|
for _, secret := range t.Secrets {
|
|
secrets = append(secrets, *secret)
|
|
}
|
|
}
|
|
var agentOptions json.RawMessage
|
|
if t.Config.AgentOptions != nil {
|
|
agentOptions = *t.Config.AgentOptions
|
|
}
|
|
|
|
var mdmSpec TeamSpecMDM
|
|
mdmSpec.MacOSUpdates = t.Config.MDM.MacOSUpdates
|
|
mdmSpec.WindowsUpdates = t.Config.MDM.WindowsUpdates
|
|
mdmSpec.MacOSSettings = t.Config.MDM.MacOSSettings.ToMap()
|
|
delete(mdmSpec.MacOSSettings, "enable_disk_encryption")
|
|
mdmSpec.MacOSSetup = t.Config.MDM.MacOSSetup
|
|
mdmSpec.EnableDiskEncryption = optjson.SetBool(t.Config.MDM.EnableDiskEncryption)
|
|
mdmSpec.WindowsSettings = t.Config.MDM.WindowsSettings
|
|
mdmSpec.AndroidSettings = t.Config.MDM.AndroidSettings
|
|
|
|
var webhookSettings TeamSpecWebhookSettings
|
|
if t.Config.WebhookSettings.HostStatusWebhook != nil {
|
|
webhookSettings.HostStatusWebhook = t.Config.WebhookSettings.HostStatusWebhook
|
|
}
|
|
|
|
var integrations TeamSpecIntegrations
|
|
if t.Config.Integrations.GoogleCalendar != nil {
|
|
integrations.GoogleCalendar = t.Config.Integrations.GoogleCalendar
|
|
}
|
|
|
|
return &TeamSpec{
|
|
Name: t.Name,
|
|
AgentOptions: agentOptions,
|
|
Features: &featuresJSON,
|
|
Secrets: &secrets,
|
|
MDM: mdmSpec,
|
|
HostExpirySettings: &t.Config.HostExpirySettings,
|
|
WebhookSettings: webhookSettings,
|
|
Integrations: integrations,
|
|
Scripts: t.Config.Scripts,
|
|
Software: t.Config.Software,
|
|
}, nil
|
|
}
|