mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #36313 The database columns have the default set as `CURRENT_TIMESTAMP`, so even if we are not initializing these values in code, the DB still populates them correctly. I'm explicitly adding these to the insert statements as well as updating the pointers to the label and team structs. # Checklist for submitter - [x] 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. ## Testing - [x] QA'd all new/changed functionality manually <img width="928" height="291" alt="Screenshot 2026-01-27 at 1 41 24 PM" src="https://github.com/user-attachments/assets/d4a6c8b1-e2f2-4d70-9f50-7e741eb2ae25" /> <img width="907" height="347" alt="Screenshot 2026-01-27 at 1 40 30 PM" src="https://github.com/user-attachments/assets/e5bb3a0f-1313-4a00-9193-740029d5491b" />
625 lines
19 KiB
Go
625 lines
19 KiB
Go
package mysql
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/text/unicode/norm"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/jmoiron/sqlx"
|
|
)
|
|
|
|
var teamSearchColumns = []string{"name"}
|
|
|
|
const teamColumns = `id, created_at, name, filename, description, config`
|
|
|
|
func (ds *Datastore) NewTeam(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
// We must normalize the name for full Unicode support (Unicode equivalence).
|
|
team.Name = norm.NFC.String(team.Name)
|
|
err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
|
query := `
|
|
INSERT INTO teams (
|
|
name,
|
|
filename,
|
|
description,
|
|
config
|
|
) VALUES (?, ?, ?, ?)
|
|
`
|
|
result, err := tx.ExecContext(
|
|
ctx,
|
|
query,
|
|
team.Name,
|
|
team.Filename,
|
|
team.Description,
|
|
team.Config,
|
|
)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "insert team")
|
|
}
|
|
|
|
id, _ := result.LastInsertId()
|
|
team.ID = uint(id) //nolint:gosec // dismiss G115
|
|
team.CreatedAt = time.Now().UTC().Truncate(time.Second)
|
|
|
|
return saveTeamSecretsDB(ctx, tx, team)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return team, nil
|
|
}
|
|
|
|
func (ds *Datastore) TeamWithExtras(ctx context.Context, tid uint) (*fleet.Team, error) {
|
|
return teamDB(ctx, ds.reader(ctx), tid, true)
|
|
}
|
|
|
|
func (ds *Datastore) TeamLite(ctx context.Context, tid uint) (*fleet.TeamLite, error) {
|
|
team, err := teamDB(ctx, ds.reader(ctx), tid, false)
|
|
if team == nil {
|
|
return nil, err
|
|
}
|
|
|
|
return team.ToTeamLite(), err // re-marshaling this way to avoid more code duplication
|
|
}
|
|
|
|
func teamDB(ctx context.Context, q sqlx.QueryerContext, tid uint, withExtras bool) (*fleet.Team, error) {
|
|
if tid == 0 {
|
|
if withExtras {
|
|
return nil, ctxerr.Errorf(ctx, "withExtras argument not supported for team ID 0")
|
|
}
|
|
config, err := defaultTeamConfigDB(ctx, q)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "default team config")
|
|
}
|
|
return &fleet.Team{
|
|
ID: 0,
|
|
Name: fleet.ReservedNameNoTeam,
|
|
Config: *config,
|
|
}, nil
|
|
}
|
|
|
|
stmt := `
|
|
SELECT ` + teamColumns + ` FROM teams
|
|
WHERE id = ?
|
|
`
|
|
team := &fleet.Team{}
|
|
|
|
if err := sqlx.GetContext(ctx, q, team, stmt, tid); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, ctxerr.Wrap(ctx, notFound("Team").WithID(tid))
|
|
}
|
|
return nil, ctxerr.Wrap(ctx, err, "select team")
|
|
}
|
|
|
|
if withExtras {
|
|
if err := loadSecretsForTeamsDB(ctx, q, []*fleet.Team{team}); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "getting secrets for teams")
|
|
}
|
|
|
|
if err := loadUsersForTeamDB(ctx, q, team); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := loadHostCountForTeamDB(ctx, q, team); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := loadFeaturesForTeamDB(ctx, q, team); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return team, nil
|
|
}
|
|
|
|
func saveTeamSecretsDB(ctx context.Context, q sqlx.ExtContext, team *fleet.Team) error {
|
|
if team.Secrets == nil {
|
|
return nil
|
|
}
|
|
|
|
return applyEnrollSecretsDB(ctx, q, &team.ID, team.Secrets)
|
|
}
|
|
|
|
// teamRefs are the tables referenced by teams.
|
|
// These tables are cleared when the team is deleted.
|
|
// Analogous to hostRefs.
|
|
var teamRefs = []string{
|
|
"mdm_apple_configuration_profiles",
|
|
"mdm_windows_configuration_profiles",
|
|
"mdm_apple_declarations",
|
|
"mdm_android_configuration_profiles",
|
|
"certificate_templates",
|
|
"software_title_icons",
|
|
"software_title_display_names",
|
|
}
|
|
|
|
// teamLabelsRefs are the tables that could be referenced by team labels that
|
|
// have ON DELETE RESTRICT so they have to be deleted before the team labels are deleted.
|
|
// These tables are cleared when the team is deleted.
|
|
// Analogous to hostRefs.
|
|
var teamLabelsRefs = []string{
|
|
"in_house_app_labels",
|
|
"software_installer_labels",
|
|
"vpp_app_team_labels",
|
|
// `mdm_configuration_profile_labels` and `mdm_declaration_labels` are defined with `ON DELETE SET NULL`, so not necessary.
|
|
// `policy_labels` and `query_labels` are defined with `ON DELETE CASCADE`, so not necessary.
|
|
}
|
|
|
|
func (ds *Datastore) DeleteTeam(ctx context.Context, tid uint) error {
|
|
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
|
// Delete team policies first, because policies can have associated installers and scripts
|
|
// which may be deleted on cascade before deleting the policies (which are also deleted on cascade).
|
|
_, err := tx.ExecContext(ctx, `DELETE FROM policies WHERE team_id = ?`, tid)
|
|
if err != nil {
|
|
return ctxerr.Wrapf(ctx, err, "deleting policies for team %d", tid)
|
|
}
|
|
|
|
// Delete related records from teamRefs tables before deleting the team itself
|
|
// to avoid foreign key constraint violations
|
|
for _, table := range teamRefs {
|
|
_, err = tx.ExecContext(ctx, fmt.Sprintf(`DELETE FROM %s WHERE team_id = ?`, table), tid)
|
|
if err != nil {
|
|
return ctxerr.Wrapf(ctx, err, "deleting %s for team %d", table, tid)
|
|
}
|
|
}
|
|
|
|
// Get labels that belong to this team.
|
|
getTeamLabelIDsStmt := `SELECT id FROM labels WHERE team_id = ?`
|
|
teamLabelIDs := []uint{}
|
|
if err := sqlx.SelectContext(ctx, tx, &teamLabelIDs, getTeamLabelIDsStmt, tid); err != nil {
|
|
return ctxerr.Wrapf(ctx, err, "load labels for team %d", tid)
|
|
}
|
|
|
|
if len(teamLabelIDs) > 0 {
|
|
// Delete related records from teamLabelRefs tables before deleting labels on this team
|
|
// to avoid foreign key constraint violations.
|
|
for _, table := range teamLabelsRefs {
|
|
query, args, err := sqlx.In(fmt.Sprintf(`DELETE FROM %s WHERE label_id IN (?)`, table), teamLabelIDs)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "build sqlx.In statement for labels")
|
|
}
|
|
if _, err = tx.ExecContext(ctx, query, args...); err != nil {
|
|
return ctxerr.Wrapf(ctx, err, "delete %s label associated tables", table)
|
|
}
|
|
}
|
|
|
|
if err := deleteLabelsInTx(ctx, tx, teamLabelIDs); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "delete labels")
|
|
}
|
|
}
|
|
|
|
_, err = tx.ExecContext(ctx, `DELETE FROM teams WHERE id = ?`, tid)
|
|
if err != nil {
|
|
return ctxerr.Wrapf(ctx, err, "delete team %d", tid)
|
|
}
|
|
|
|
_, err = tx.ExecContext(ctx, `DELETE FROM pack_targets WHERE type = ? AND target_id = ?`, fleet.TargetTeam, tid)
|
|
if err != nil {
|
|
return ctxerr.Wrapf(ctx, err, "deleting pack_targets for team %d", tid)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (ds *Datastore) TeamByName(ctx context.Context, name string) (*fleet.Team, error) {
|
|
// We must normalize the name for full Unicode support (Unicode equivalence).
|
|
nameUnicode := norm.NFC.String(name)
|
|
stmt := `
|
|
SELECT ` + teamColumns + ` FROM teams
|
|
WHERE name = ?
|
|
`
|
|
team := &fleet.Team{}
|
|
|
|
if err := sqlx.GetContext(ctx, ds.reader(ctx), team, stmt, nameUnicode); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, ctxerr.Wrap(ctx, notFound("Team").WithName(nameUnicode))
|
|
}
|
|
return nil, ctxerr.Wrap(ctx, err, "select team")
|
|
}
|
|
|
|
return ds.loadExtrasForTeam(ctx, team)
|
|
}
|
|
|
|
func (ds *Datastore) loadExtrasForTeam(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
if err := loadSecretsForTeamsDB(ctx, ds.reader(ctx), []*fleet.Team{team}); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "getting secrets for teams")
|
|
}
|
|
|
|
if err := loadUsersForTeamDB(ctx, ds.reader(ctx), team); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := loadHostCountForTeamDB(ctx, ds.reader(ctx), team); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := loadFeaturesForTeamDB(ctx, ds.reader(ctx), team); err != nil {
|
|
return nil, err
|
|
}
|
|
return team, nil
|
|
}
|
|
|
|
func (ds *Datastore) TeamByFilename(ctx context.Context, filename string) (*fleet.Team, error) {
|
|
stmt := `
|
|
SELECT ` + teamColumns + ` FROM teams
|
|
WHERE filename = ?
|
|
`
|
|
team := &fleet.Team{}
|
|
|
|
if err := sqlx.GetContext(ctx, ds.reader(ctx), team, stmt, filename); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ctxerr.Wrap(ctx, notFound("Team").WithMessage("filename not found"))
|
|
}
|
|
return nil, ctxerr.Wrap(ctx, err, "select team")
|
|
}
|
|
|
|
return ds.loadExtrasForTeam(ctx, team)
|
|
}
|
|
|
|
func loadUsersForTeamDB(ctx context.Context, q sqlx.QueryerContext, team *fleet.Team) error {
|
|
sql := `
|
|
SELECT u.name, u.id, u.email, ut.role
|
|
FROM user_teams ut JOIN users u ON (ut.user_id = u.id)
|
|
WHERE ut.team_id = ?
|
|
`
|
|
rows := []fleet.TeamUser{}
|
|
if err := sqlx.SelectContext(ctx, q, &rows, sql, team.ID); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "load users for team")
|
|
}
|
|
|
|
team.Users = rows
|
|
team.UserCount = len(rows)
|
|
|
|
return nil
|
|
}
|
|
|
|
func loadHostCountForTeamDB(ctx context.Context, q sqlx.QueryerContext, team *fleet.Team) error {
|
|
stmt := `
|
|
SELECT COUNT(*) FROM hosts h
|
|
WHERE h.team_id = ?
|
|
`
|
|
if err := q.QueryRowxContext(ctx, stmt, team.ID).Scan(&team.HostCount); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "load HostsCount for team")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func loadFeaturesForTeamDB(ctx context.Context, q sqlx.QueryerContext, team *fleet.Team) error {
|
|
features, err := teamFeaturesDB(ctx, q, team.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
team.Config.Features = *features
|
|
return nil
|
|
}
|
|
|
|
func saveUsersForTeamDB(ctx context.Context, exec sqlx.ExecerContext, team *fleet.Team) error {
|
|
// Do a full user update by deleting existing users and then inserting all
|
|
// the current users in a single transaction.
|
|
// Delete before insert
|
|
sql := `DELETE FROM user_teams WHERE team_id = ?`
|
|
if _, err := exec.ExecContext(ctx, sql, team.ID); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "delete existing users")
|
|
}
|
|
|
|
if len(team.Users) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Bulk insert
|
|
const valueStr = "(?,?,?),"
|
|
var args []interface{}
|
|
for _, teamUser := range team.Users {
|
|
args = append(args, teamUser.User.ID, team.ID, teamUser.Role)
|
|
}
|
|
sql = "INSERT INTO user_teams (user_id, team_id, role) VALUES " +
|
|
strings.Repeat(valueStr, len(team.Users))
|
|
sql = strings.TrimSuffix(sql, ",")
|
|
if _, err := exec.ExecContext(ctx, sql, args...); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "insert users")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ds *Datastore) SaveTeam(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
// We must normalize the name for full Unicode support (Unicode equivalence).
|
|
team.Name = norm.NFC.String(team.Name)
|
|
err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
|
query := `
|
|
UPDATE teams
|
|
SET
|
|
name = ?,
|
|
filename = ?,
|
|
description = ?,
|
|
config = ?
|
|
WHERE
|
|
id = ?
|
|
`
|
|
_, err := tx.ExecContext(ctx, query, team.Name, team.Filename, team.Description, team.Config, team.ID)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "saving team")
|
|
}
|
|
|
|
if err := saveUsersForTeamDB(ctx, tx, team); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return team, nil
|
|
}
|
|
|
|
// ListTeams lists all teams with limit, sort and offset passed in with
|
|
// fleet.ListOptions
|
|
func (ds *Datastore) ListTeams(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions) ([]*fleet.Team, error) {
|
|
query := fmt.Sprintf(`
|
|
SELECT `+teamColumns+`,
|
|
(SELECT count(*) FROM user_teams WHERE team_id = t.id) AS user_count,
|
|
(SELECT count(*) FROM hosts WHERE team_id = t.id) AS host_count
|
|
FROM teams t
|
|
WHERE %s
|
|
`,
|
|
ds.whereFilterTeams(filter, "t"),
|
|
)
|
|
// We must normalize the name for full Unicode support (Unicode equivalence).
|
|
matchQuery := norm.NFC.String(opt.MatchQuery)
|
|
query, params := searchLike(query, nil, matchQuery, teamSearchColumns...)
|
|
query, params = appendListOptionsWithCursorToSQL(query, params, &opt)
|
|
teams := []*fleet.Team{}
|
|
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &teams, query, params...); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "list teams")
|
|
}
|
|
if err := loadSecretsForTeamsDB(ctx, ds.reader(ctx), teams); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "getting secrets for teams")
|
|
}
|
|
return teams, nil
|
|
}
|
|
|
|
func loadSecretsForTeamsDB(ctx context.Context, q sqlx.QueryerContext, teams []*fleet.Team) error {
|
|
for _, team := range teams {
|
|
secrets, err := getEnrollSecretsDB(ctx, q, ptr.Uint(team.ID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
team.Secrets = secrets
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ds *Datastore) TeamsSummary(ctx context.Context) ([]*fleet.TeamSummary, error) {
|
|
teamsSummary := []*fleet.TeamSummary{}
|
|
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &teamsSummary, "SELECT id, name, description FROM teams"); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "teams summary")
|
|
}
|
|
return teamsSummary, nil
|
|
}
|
|
|
|
func (ds *Datastore) TeamExists(ctx context.Context, teamID uint) (bool, error) {
|
|
var exists bool
|
|
err := ds.writer(ctx).GetContext(ctx, &exists, "SELECT EXISTS(SELECT 1 FROM teams WHERE id = ?)", teamID)
|
|
if err != nil {
|
|
return false, ctxerr.Wrap(ctx, err, "team exists")
|
|
}
|
|
return exists, nil
|
|
}
|
|
|
|
func (ds *Datastore) SearchTeams(ctx context.Context, filter fleet.TeamFilter, matchQuery string, omit ...uint) ([]*fleet.Team, error) {
|
|
sql := fmt.Sprintf(`
|
|
SELECT %s,
|
|
(SELECT count(*) FROM user_teams WHERE team_id = t.id) AS user_count,
|
|
(SELECT count(*) FROM hosts WHERE team_id = t.id) AS host_count
|
|
FROM teams t
|
|
WHERE %s AND %s
|
|
`,
|
|
teamColumns,
|
|
ds.whereOmitIDs("t.id", omit),
|
|
ds.whereFilterTeams(filter, "t"),
|
|
)
|
|
// We must normalize the name for full Unicode support (Unicode equivalence).
|
|
matchQuery = norm.NFC.String(matchQuery)
|
|
sql, params := searchLike(sql, nil, matchQuery, teamSearchColumns...)
|
|
teams := []*fleet.Team{}
|
|
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &teams, sql, params...); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "search teams")
|
|
}
|
|
if err := loadSecretsForTeamsDB(ctx, ds.reader(ctx), teams); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "getting secrets for teams")
|
|
}
|
|
return teams, nil
|
|
}
|
|
|
|
func (ds *Datastore) TeamEnrollSecrets(ctx context.Context, teamID uint) ([]*fleet.EnrollSecret, error) {
|
|
sql := `
|
|
SELECT secret, team_id, created_at FROM enroll_secrets
|
|
WHERE team_id = ?
|
|
`
|
|
var secrets []*fleet.EnrollSecret
|
|
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &secrets, sql, teamID); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "get secrets")
|
|
}
|
|
return secrets, nil
|
|
}
|
|
|
|
func amountTeamsDB(ctx context.Context, db sqlx.QueryerContext) (int, error) {
|
|
var amount int
|
|
err := sqlx.GetContext(ctx, db, &amount, `SELECT count(*) FROM teams`)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return amount, nil
|
|
}
|
|
|
|
// TeamAgentOptions loads the agents options of a team.
|
|
func (ds *Datastore) TeamAgentOptions(ctx context.Context, tid uint) (*json.RawMessage, error) {
|
|
stmt := fmt.Sprintf(`SELECT config->'$.agent_options' FROM teams WHERE id = %d`, tid) // safe because uint
|
|
var agentOptions *json.RawMessage
|
|
if err := sqlx.GetContext(ctx, ds.reader(ctx), &agentOptions, stmt); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "select team")
|
|
}
|
|
return agentOptions, nil
|
|
}
|
|
|
|
// TeamFeatures loads the features enabled for a team.
|
|
func (ds *Datastore) TeamFeatures(ctx context.Context, tid uint) (*fleet.Features, error) {
|
|
return teamFeaturesDB(ctx, ds.reader(ctx), tid)
|
|
}
|
|
|
|
func teamFeaturesDB(ctx context.Context, q sqlx.QueryerContext, tid uint) (*fleet.Features, error) {
|
|
stmt := fmt.Sprintf(`SELECT config->'$.features' as features FROM teams WHERE id = %d`, tid) // safe due to uint
|
|
var raw *json.RawMessage
|
|
if err := sqlx.GetContext(ctx, q, &raw, stmt); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "get team config features")
|
|
}
|
|
|
|
var features fleet.Features
|
|
features.ApplyDefaultsForNewInstalls()
|
|
if raw != nil {
|
|
if err := json.Unmarshal(*raw, &features); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "unmarshal team config features")
|
|
}
|
|
}
|
|
return &features, nil
|
|
}
|
|
|
|
func (ds *Datastore) TeamMDMConfig(ctx context.Context, tid uint) (*fleet.TeamMDM, error) {
|
|
sql := `SELECT config->'$.mdm' AS mdm FROM teams WHERE id = ?`
|
|
var raw *json.RawMessage
|
|
if err := sqlx.GetContext(ctx, ds.reader(ctx), &raw, sql, tid); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "select team MDM config")
|
|
}
|
|
var mdmConfig *fleet.TeamMDM
|
|
if raw != nil {
|
|
if err := json.Unmarshal(*raw, &mdmConfig); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "unmarshal team MDM config")
|
|
}
|
|
}
|
|
return mdmConfig, nil
|
|
}
|
|
|
|
// DeleteIntegrationsFromTeams removes the deleted integrations from any team
|
|
// that uses it.
|
|
func (ds *Datastore) DeleteIntegrationsFromTeams(ctx context.Context, deletedIntgs fleet.Integrations) error {
|
|
const (
|
|
listTeams = `SELECT id, config FROM teams WHERE config IS NOT NULL`
|
|
updateTeam = `UPDATE teams SET config = ? WHERE id = ?`
|
|
)
|
|
|
|
rows, err := ds.writer(ctx).QueryxContext(ctx, listTeams)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "query teams")
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var tm fleet.Team
|
|
if err := rows.StructScan(&tm); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "scan team row in struct")
|
|
}
|
|
|
|
// ignore errors, it's ok for some integrations to not match with the
|
|
// batch of deleted integrations, we're only interested in knowing if
|
|
// some did match.
|
|
if matches, _ := tm.Config.Integrations.MatchWithIntegrations(deletedIntgs); len(matches.Jira)+len(matches.Zendesk) > 0 {
|
|
delJira, _ := fleet.IndexJiraIntegrations(matches.Jira)
|
|
delZendesk, _ := fleet.IndexZendeskIntegrations(matches.Zendesk)
|
|
|
|
var keepJira []*fleet.TeamJiraIntegration
|
|
for _, tmIntg := range tm.Config.Integrations.Jira {
|
|
if _, ok := delJira[tmIntg.UniqueKey()]; !ok {
|
|
keepJira = append(keepJira, tmIntg)
|
|
}
|
|
}
|
|
|
|
var keepZendesk []*fleet.TeamZendeskIntegration
|
|
for _, tmIntg := range tm.Config.Integrations.Zendesk {
|
|
if _, ok := delZendesk[tmIntg.UniqueKey()]; !ok {
|
|
keepZendesk = append(keepZendesk, tmIntg)
|
|
}
|
|
}
|
|
|
|
tm.Config.Integrations.Jira = keepJira
|
|
tm.Config.Integrations.Zendesk = keepZendesk
|
|
if _, err := ds.writer(ctx).ExecContext(ctx, updateTeam, tm.Config, tm.ID); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "update team config")
|
|
}
|
|
}
|
|
}
|
|
return rows.Err()
|
|
}
|
|
|
|
func (ds *Datastore) TeamIDsWithSetupExperienceIdPEnabled(ctx context.Context) ([]uint, error) {
|
|
const stmt = `
|
|
SELECT
|
|
id
|
|
FROM
|
|
teams
|
|
WHERE
|
|
config IS NOT NULL AND
|
|
config->'$.mdm.macos_setup.enable_end_user_authentication' = TRUE
|
|
|
|
UNION
|
|
|
|
SELECT
|
|
0
|
|
FROM
|
|
app_config_json
|
|
WHERE
|
|
json_value->'$.mdm.macos_setup.enable_end_user_authentication' = TRUE
|
|
`
|
|
var teamIDs []uint
|
|
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &teamIDs, stmt); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "select team IDs with setup experience IdP enabled")
|
|
}
|
|
return teamIDs, nil
|
|
}
|
|
|
|
// DefaultTeamConfig returns the configuration for "No Team" hosts.
|
|
func (ds *Datastore) DefaultTeamConfig(ctx context.Context) (*fleet.TeamConfig, error) {
|
|
return defaultTeamConfigDB(ctx, ds.reader(ctx))
|
|
}
|
|
|
|
func defaultTeamConfigDB(ctx context.Context, q sqlx.QueryerContext) (*fleet.TeamConfig, error) {
|
|
config := &fleet.TeamConfig{}
|
|
var bytes []byte
|
|
err := sqlx.GetContext(ctx, q, &bytes,
|
|
`SELECT json_value FROM default_team_config_json LIMIT 1`)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
// Return empty config if no record exists (shouldn't happen after migration)
|
|
return &fleet.TeamConfig{}, nil
|
|
}
|
|
return nil, ctxerr.Wrap(ctx, err, "selecting default team config")
|
|
}
|
|
|
|
err = json.Unmarshal(bytes, config)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "unmarshaling default team config")
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
// SaveDefaultTeamConfig saves the configuration for "No Team" hosts.
|
|
func (ds *Datastore) SaveDefaultTeamConfig(ctx context.Context, config *fleet.TeamConfig) error {
|
|
// Create a copy to avoid saving unsupported fields such as scripts and software
|
|
configCopy := config.Copy()
|
|
configBytes, err := json.Marshal(&configCopy)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "marshaling config")
|
|
}
|
|
|
|
_, err = ds.writer(ctx).ExecContext(ctx,
|
|
`INSERT INTO default_team_config_json(id, json_value) VALUES(1, ?)
|
|
ON DUPLICATE KEY UPDATE json_value = VALUES(json_value)`,
|
|
configBytes,
|
|
)
|
|
return ctxerr.Wrap(ctx, err, "save default team config")
|
|
}
|