mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Covers #36760, #36758. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [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. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [ ] QA'd all new/changed functionality manually
1634 lines
54 KiB
Go
1634 lines
54 KiB
Go
package service
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"crypto/sha256"
|
||
"crypto/x509"
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"encoding/pem"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"net/url"
|
||
"reflect"
|
||
"sort"
|
||
"strings"
|
||
|
||
"github.com/fleetdm/fleet/v4/pkg/file"
|
||
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
||
"github.com/fleetdm/fleet/v4/server/authz"
|
||
"github.com/fleetdm/fleet/v4/server/contexts/ctxdb"
|
||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||
"github.com/fleetdm/fleet/v4/server/contexts/logging"
|
||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||
"github.com/fleetdm/fleet/v4/server/mdm"
|
||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
||
"github.com/fleetdm/fleet/v4/server/mdm/assets"
|
||
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/client"
|
||
"github.com/fleetdm/fleet/v4/server/sso"
|
||
"github.com/fleetdm/fleet/v4/server/worker"
|
||
"github.com/go-kit/log/level"
|
||
"github.com/google/uuid"
|
||
)
|
||
|
||
// Deprecated: use the new GetABMTokens API endpoint.
|
||
func (svc *Service) GetAppleBM(ctx context.Context) (*fleet.AppleBM, error) {
|
||
if err := svc.authz.Authorize(ctx, &fleet.AppleBM{}, fleet.ActionRead); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
appCfg, err := svc.AppConfigObfuscated(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
mdmServerURL, err := apple_mdm.ResolveAppleMDMURL(appCfg.ServerSettings.ServerURL)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
tokens, err := svc.ds.ListABMTokens(ctx)
|
||
if err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "GetAppleBM: listing ABM tokens")
|
||
}
|
||
|
||
if len(tokens) == 0 {
|
||
return nil, notFoundError{}
|
||
}
|
||
|
||
if len(tokens) > 1 {
|
||
return nil, errors.New("This API endpoint has been deprecated. Please use the new GET /abm_tokens API endpoint documented here: https://fleetdm.com/learn-more-about/apple-business-manager-tokens-api")
|
||
}
|
||
|
||
abmToken := tokens[0]
|
||
|
||
var appleBM fleet.AppleBM
|
||
// transfer the abmToken metadata info to the appleBM struct
|
||
appleBM.AppleID = abmToken.AppleID
|
||
appleBM.OrgName = abmToken.OrganizationName
|
||
appleBM.RenewDate = abmToken.RenewAt
|
||
appleBM.DefaultTeam = appCfg.MDM.DeprecatedAppleBMDefaultTeam
|
||
appleBM.MDMServerURL = mdmServerURL
|
||
|
||
return &appleBM, nil
|
||
}
|
||
|
||
func (svc *Service) MDMAppleDeviceLock(ctx context.Context, hostID uint) error {
|
||
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
||
return err
|
||
}
|
||
|
||
host, err := svc.ds.HostLite(ctx, hostID)
|
||
if err != nil {
|
||
return ctxerr.Wrap(ctx, err, "host lite")
|
||
}
|
||
|
||
// TODO: define and use right permissions according to the spec.
|
||
if err := svc.authz.Authorize(ctx, host, fleet.ActionWrite); err != nil {
|
||
return err
|
||
}
|
||
|
||
_, err = svc.mdmAppleCommander.DeviceLock(ctx, host, uuid.New().String())
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (svc *Service) MDMAppleEraseDevice(ctx context.Context, hostID uint) error {
|
||
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
||
return err
|
||
}
|
||
|
||
host, err := svc.ds.HostLite(ctx, hostID)
|
||
if err != nil {
|
||
return ctxerr.Wrap(ctx, err, "host lite")
|
||
}
|
||
|
||
// TODO: define and use right permissions according to the spec.
|
||
if err := svc.authz.Authorize(ctx, host, fleet.ActionWrite); err != nil {
|
||
return err
|
||
}
|
||
|
||
err = svc.mdmAppleCommander.EraseDevice(ctx, host, uuid.New().String())
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (svc *Service) MDMListHostConfigurationProfiles(ctx context.Context, hostID uint) ([]*fleet.MDMAppleConfigProfile, error) {
|
||
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionSelectiveList); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
host, err := svc.ds.HostLite(ctx, hostID)
|
||
if err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "find host to list profiles")
|
||
}
|
||
|
||
var tmID uint
|
||
if host.TeamID != nil {
|
||
tmID = *host.TeamID
|
||
}
|
||
|
||
// NOTE: the service method also does all the right authorization checks
|
||
sums, err := svc.ListMDMAppleConfigProfiles(ctx, tmID)
|
||
if err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "list config profiles")
|
||
}
|
||
|
||
return sums, nil
|
||
}
|
||
|
||
func (svc *Service) MDMAppleEnableFileVaultAndEscrow(ctx context.Context, teamID *uint) error {
|
||
cert, err := assets.X509Cert(ctx, svc.ds, fleet.MDMAssetCACert)
|
||
if err != nil {
|
||
return ctxerr.Wrap(ctx, err, "retrieving CA cert")
|
||
}
|
||
|
||
var contents bytes.Buffer
|
||
params := fileVaultProfileOptions{
|
||
PayloadIdentifier: mobileconfig.FleetFileVaultPayloadIdentifier,
|
||
PayloadName: mdm.FleetFileVaultProfileName,
|
||
Base64DerCertificate: base64.StdEncoding.EncodeToString(cert.Raw),
|
||
}
|
||
if err := fileVaultProfileTemplate.Execute(&contents, params); err != nil {
|
||
return ctxerr.Wrap(ctx, err, "enabling FileVault")
|
||
}
|
||
|
||
cp, err := fleet.NewMDMAppleConfigProfile(contents.Bytes(), teamID)
|
||
if err != nil {
|
||
return ctxerr.Wrap(ctx, err, "enabling FileVault")
|
||
}
|
||
|
||
// filevault profile is a fleet-controlled profile that doesn't use any Fleet variables
|
||
_, err = svc.ds.NewMDMAppleConfigProfile(ctx, *cp, nil)
|
||
return ctxerr.Wrap(ctx, err, "enabling FileVault")
|
||
}
|
||
|
||
func (svc *Service) MDMAppleDisableFileVaultAndEscrow(ctx context.Context, teamID *uint) error {
|
||
err := svc.ds.DeleteMDMAppleConfigProfileByTeamAndIdentifier(ctx, teamID, mobileconfig.FleetFileVaultPayloadIdentifier)
|
||
return ctxerr.Wrap(ctx, err, "disabling FileVault")
|
||
}
|
||
|
||
func (svc *Service) UpdateMDMAppleSetup(ctx context.Context, payload fleet.MDMAppleSetupPayload) error {
|
||
if err := svc.authz.Authorize(ctx, payload, fleet.ActionWrite); err != nil {
|
||
return err
|
||
}
|
||
|
||
if err := svc.validateMDMAppleSetupPayload(ctx, payload); err != nil {
|
||
return err
|
||
}
|
||
|
||
if payload.TeamID != nil && *payload.TeamID != 0 {
|
||
tm, err := svc.teamByIDOrName(ctx, payload.TeamID, nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return svc.updateTeamMDMAppleSetup(ctx, tm, payload)
|
||
}
|
||
return svc.updateAppConfigMDMAppleSetup(ctx, payload)
|
||
}
|
||
|
||
func (svc *Service) updateAppConfigMDMAppleSetup(ctx context.Context, payload fleet.MDMAppleSetupPayload) error {
|
||
// appconfig is only used internally, it's fine to read it unobfuscated
|
||
// (svc.AppConfigObfuscated must not be used because the write-only users
|
||
// such as gitops will fail to access it).
|
||
ac, err := svc.ds.AppConfig(ctx)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
var didUpdate, didUpdateMacOSEndUserAuth bool
|
||
if payload.EnableEndUserAuthentication != nil {
|
||
if ac.MDM.MacOSSetup.EnableEndUserAuthentication != *payload.EnableEndUserAuthentication {
|
||
ac.MDM.MacOSSetup.EnableEndUserAuthentication = *payload.EnableEndUserAuthentication
|
||
didUpdate = true
|
||
didUpdateMacOSEndUserAuth = true
|
||
}
|
||
}
|
||
|
||
if payload.RequireAllSoftware != nil && ac.MDM.MacOSSetup.RequireAllSoftware != *payload.RequireAllSoftware {
|
||
ac.MDM.MacOSSetup.RequireAllSoftware = *payload.RequireAllSoftware
|
||
didUpdate = true
|
||
}
|
||
|
||
if payload.EnableReleaseDeviceManually != nil {
|
||
if ac.MDM.MacOSSetup.EnableReleaseDeviceManually.Value != *payload.EnableReleaseDeviceManually {
|
||
ac.MDM.MacOSSetup.EnableReleaseDeviceManually = optjson.SetBool(*payload.EnableReleaseDeviceManually)
|
||
didUpdate = true
|
||
}
|
||
}
|
||
|
||
if payload.ManualAgentInstall != nil {
|
||
if ac.MDM.MacOSSetup.ManualAgentInstall.Value != *payload.ManualAgentInstall {
|
||
// Try to load the bootstrap package to verify it exists.
|
||
_, err := svc.GetMDMAppleBootstrapPackageMetadata(ctx, 0, false)
|
||
// If we got an error other than not found, return it.
|
||
if err != nil && !fleet.IsNotFound(err) {
|
||
return ctxerr.Wrap(ctx, err, "checking bootstrap package")
|
||
}
|
||
// Otherwise if we got a not found error, we can't enable manual agent install.
|
||
if *payload.ManualAgentInstall && err != nil {
|
||
return fleet.NewUserMessageError(errors.New("Couldn’t enable manual_agent_install. To use this option, first specify a bootstrap_package."), http.StatusUnprocessableEntity)
|
||
}
|
||
sec, err := svc.ds.GetSetupExperienceCount(ctx, string(fleet.MacOSPlatform), nil)
|
||
if err != nil {
|
||
return ctxerr.Wrap(ctx, err, "getting setup experience information")
|
||
}
|
||
if sec.Installers != 0 || sec.VPP != 0 {
|
||
return fleet.NewUserMessageError(errors.New("Couldn’t enable manual_agent_install. To use this option, first disable setup experience software."), http.StatusUnprocessableEntity)
|
||
}
|
||
if sec.Scripts != 0 {
|
||
return fleet.NewUserMessageError(errors.New("Couldn’t enable manual_agent_install. To use this option, first remove your setup experience script."), http.StatusUnprocessableEntity)
|
||
}
|
||
ac.MDM.MacOSSetup.ManualAgentInstall = optjson.SetBool(*payload.ManualAgentInstall)
|
||
didUpdate = true
|
||
}
|
||
}
|
||
|
||
if didUpdate {
|
||
if err := svc.ds.SaveAppConfig(ctx, ac); err != nil {
|
||
return err
|
||
}
|
||
if didUpdateMacOSEndUserAuth {
|
||
if err := svc.updateMacOSSetupEnableEndUserAuth(ctx, ac.MDM.MacOSSetup.EnableEndUserAuthentication, nil, nil); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (svc *Service) updateMacOSSetupEnableEndUserAuth(ctx context.Context, enable bool, teamID *uint, teamName *string) error {
|
||
if _, err := worker.QueueMacosSetupAssistantJob(ctx, svc.ds, svc.logger, worker.MacosSetupAssistantUpdateProfile, teamID); err != nil {
|
||
return ctxerr.Wrap(ctx, err, "queue macos setup assistant update profile job")
|
||
}
|
||
|
||
var act fleet.ActivityDetails
|
||
if enable {
|
||
act = fleet.ActivityTypeEnabledMacosSetupEndUserAuth{TeamID: teamID, TeamName: teamName}
|
||
} else {
|
||
act = fleet.ActivityTypeDisabledMacosSetupEndUserAuth{TeamID: teamID, TeamName: teamName}
|
||
}
|
||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil {
|
||
return ctxerr.Wrap(ctx, err, "create activity for macos enable end user auth change")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (svc *Service) validateMDMAppleSetupPayload(ctx context.Context, payload fleet.MDMAppleSetupPayload) error {
|
||
// appconfig is only used internally, it's fine to read it unobfuscated
|
||
// (svc.AppConfigObfuscated must not be used because the write-only users
|
||
// such as gitops will fail to access it).
|
||
ac, err := svc.ds.AppConfig(ctx)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// If anything besides enable_end_user_authentication is being updated, ensure MDM is on.
|
||
if (payload.RequireAllSoftware != nil || payload.EnableReleaseDeviceManually != nil || payload.ManualAgentInstall != nil) && !ac.MDM.EnabledAndConfigured {
|
||
return fleet.ErrMDMNotConfigured
|
||
}
|
||
|
||
if payload.EnableEndUserAuthentication != nil && *payload.EnableEndUserAuthentication {
|
||
if ac.MDM.EndUserAuthentication.IsEmpty() {
|
||
// TODO: update this error message to include steps to resolve the issue once docs for IdP
|
||
// config are available
|
||
return fleet.NewInvalidArgumentError("enable_end_user_authentication",
|
||
`Couldn't enable macos_setup.enable_end_user_authentication because no IdP is configured for MDM features.`)
|
||
}
|
||
|
||
hasCustomConfigurationWebURL, err := svc.HasCustomSetupAssistantConfigurationWebURL(ctx, payload.TeamID)
|
||
if err != nil {
|
||
return ctxerr.Wrap(ctx, err, "checking setup assistant configuration web url")
|
||
}
|
||
|
||
if hasCustomConfigurationWebURL {
|
||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("macos_setup.enable_end_user_authentication", fleet.EndUserAuthDEPWebURLConfiguredErrMsg))
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (svc *Service) MDMAppleUploadBootstrapPackage(ctx context.Context, name string, pkg io.Reader, teamID uint, dryRun bool) error {
|
||
if err := svc.authz.Authorize(ctx, &fleet.MDMAppleBootstrapPackage{TeamID: teamID}, fleet.ActionWrite); err != nil {
|
||
return err
|
||
}
|
||
|
||
var ptrTeamName *string
|
||
var ptrTeamId *uint
|
||
if teamID >= 1 {
|
||
tm, err := svc.teamByIDOrName(ctx, &teamID, nil)
|
||
if err != nil {
|
||
return ctxerr.Wrap(ctx, err, "get team name for upload bootstrap package activity details")
|
||
}
|
||
ptrTeamName = &tm.Name
|
||
ptrTeamId = &teamID
|
||
}
|
||
|
||
// Read the pkg into a buffer
|
||
buff := bytes.NewBuffer(nil)
|
||
if _, err := io.Copy(buff, pkg); err != nil {
|
||
return err
|
||
}
|
||
buffReader := bytes.NewReader(buff.Bytes())
|
||
|
||
if err := file.CheckPKGSignature(buffReader); err != nil {
|
||
msg := "invalid package"
|
||
if errors.Is(err, file.ErrInvalidType) || errors.Is(err, file.ErrNotSigned) {
|
||
msg = err.Error()
|
||
}
|
||
|
||
return &fleet.BadRequestError{
|
||
Message: msg,
|
||
InternalErr: err,
|
||
}
|
||
}
|
||
|
||
buffReader.Reset(buff.Bytes())
|
||
hasDistribution, err := file.XARHasDistribution(buffReader)
|
||
if err != nil {
|
||
return &fleet.BadRequestError{
|
||
Message: err.Error(),
|
||
InternalErr: err,
|
||
}
|
||
}
|
||
if !hasDistribution {
|
||
return &fleet.BadRequestError{Message: fleet.BootstrapPkgNotDistributionErrMsg}
|
||
}
|
||
|
||
buffReader.Reset(buff.Bytes())
|
||
hash := sha256.New()
|
||
if _, err := io.Copy(hash, buffReader); err != nil {
|
||
return err
|
||
}
|
||
|
||
if dryRun {
|
||
return nil
|
||
}
|
||
|
||
bp := &fleet.MDMAppleBootstrapPackage{
|
||
TeamID: teamID,
|
||
Name: name,
|
||
Token: uuid.New().String(),
|
||
Sha256: hash.Sum(nil),
|
||
Bytes: buff.Bytes(),
|
||
}
|
||
if err := svc.ds.InsertMDMAppleBootstrapPackage(ctx, bp, svc.bootstrapPackageStore); err != nil {
|
||
return err
|
||
}
|
||
|
||
if err := svc.NewActivity(
|
||
ctx, authz.UserFromContext(ctx),
|
||
fleet.ActivityTypeAddedBootstrapPackage{BootstrapPackageName: name, TeamID: ptrTeamId, TeamName: ptrTeamName},
|
||
); err != nil {
|
||
return ctxerr.Wrap(ctx, err, "create activity for upload bootstrap package")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (svc *Service) GetMDMAppleBootstrapPackageBytes(ctx context.Context, token string) (*fleet.MDMAppleBootstrapPackage, error) {
|
||
// skipauth: bootstrap packages are gated by token
|
||
svc.authz.SkipAuthorization(ctx)
|
||
|
||
pkg, err := svc.ds.GetMDMAppleBootstrapPackageBytes(ctx, token, svc.bootstrapPackageStore)
|
||
if err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err)
|
||
}
|
||
return pkg, nil
|
||
}
|
||
|
||
func (svc *Service) GetMDMAppleBootstrapPackageMetadata(ctx context.Context, teamID uint, forUpdate bool) (*fleet.MDMAppleBootstrapPackage, error) {
|
||
act := fleet.ActionRead
|
||
if forUpdate {
|
||
act = fleet.ActionWrite
|
||
}
|
||
if err := svc.authz.Authorize(ctx, &fleet.MDMAppleBootstrapPackage{TeamID: teamID}, act); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
meta, err := svc.ds.GetMDMAppleBootstrapPackageMeta(ctx, teamID)
|
||
if err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "fetching bootstrap package metadata")
|
||
}
|
||
|
||
return meta, nil
|
||
}
|
||
|
||
func (svc *Service) DeleteMDMAppleBootstrapPackage(ctx context.Context, teamID *uint, dryRun bool) error {
|
||
var tmID uint
|
||
if teamID != nil {
|
||
tmID = *teamID
|
||
}
|
||
|
||
if err := svc.authz.Authorize(ctx, &fleet.MDMAppleBootstrapPackage{TeamID: tmID}, fleet.ActionWrite); err != nil {
|
||
return err
|
||
}
|
||
|
||
var ptrTeamID *uint
|
||
var ptrTeamName *string
|
||
if tmID >= 1 {
|
||
tm, err := svc.teamByIDOrName(ctx, &tmID, nil)
|
||
if err != nil {
|
||
return ctxerr.Wrap(ctx, err, "get team name for delete bootstrap package activity details")
|
||
}
|
||
ptrTeamID = &tm.ID
|
||
ptrTeamName = &tm.Name
|
||
}
|
||
|
||
meta, err := svc.ds.GetMDMAppleBootstrapPackageMeta(ctx, tmID)
|
||
if err != nil {
|
||
return ctxerr.Wrap(ctx, err, "fetching bootstrap package metadata")
|
||
}
|
||
|
||
if dryRun {
|
||
return nil
|
||
}
|
||
|
||
if err := svc.ds.DeleteMDMAppleBootstrapPackage(ctx, tmID); err != nil {
|
||
return ctxerr.Wrap(ctx, err, "deleting bootstrap package")
|
||
}
|
||
|
||
if err := svc.NewActivity(
|
||
ctx, authz.UserFromContext(ctx),
|
||
fleet.ActivityTypeDeletedBootstrapPackage{BootstrapPackageName: meta.Name, TeamID: ptrTeamID, TeamName: ptrTeamName},
|
||
); err != nil {
|
||
return ctxerr.Wrap(ctx, err, "create activity for delete bootstrap package")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (svc *Service) GetMDMAppleBootstrapPackageSummary(ctx context.Context, teamID *uint) (*fleet.MDMAppleBootstrapPackageSummary, error) {
|
||
var tmID uint
|
||
if teamID != nil {
|
||
tmID = *teamID
|
||
}
|
||
|
||
if err := svc.authz.Authorize(ctx, &fleet.MDMAppleBootstrapPackage{TeamID: tmID}, fleet.ActionRead); err != nil {
|
||
return &fleet.MDMAppleBootstrapPackageSummary{}, err
|
||
}
|
||
|
||
if teamID != nil {
|
||
_, err := svc.ds.TeamLite(ctx, tmID) // TODO see if we can use TeamExists here instead
|
||
if err != nil {
|
||
return &fleet.MDMAppleBootstrapPackageSummary{}, err
|
||
}
|
||
}
|
||
|
||
summary, err := svc.ds.GetMDMAppleBootstrapPackageSummary(ctx, tmID)
|
||
if err != nil {
|
||
return &fleet.MDMAppleBootstrapPackageSummary{}, ctxerr.Wrap(ctx, err, "getting bootstrap package summary")
|
||
}
|
||
|
||
return summary, nil
|
||
}
|
||
|
||
func (svc *Service) MDMCreateEULA(ctx context.Context, name string, f io.ReadSeeker, dryRun bool) error {
|
||
if err := svc.authz.Authorize(ctx, &fleet.MDMEULA{}, fleet.ActionWrite); err != nil {
|
||
return err
|
||
}
|
||
|
||
if err := file.CheckPDF(f); err != nil {
|
||
if errors.Is(err, file.ErrInvalidType) {
|
||
return &fleet.BadRequestError{
|
||
Message: err.Error(),
|
||
InternalErr: err,
|
||
}
|
||
}
|
||
|
||
return ctxerr.Wrap(ctx, err, "checking pdf")
|
||
}
|
||
|
||
// ensure we read the file from the start
|
||
_, err := f.Seek(0, io.SeekStart)
|
||
if err != nil {
|
||
return ctxerr.Wrap(ctx, err, "seeking start of PDF file")
|
||
}
|
||
|
||
bytes, err := io.ReadAll(f)
|
||
if err != nil {
|
||
return ctxerr.Wrap(ctx, err, "reading EULA bytes")
|
||
}
|
||
|
||
if dryRun {
|
||
return nil
|
||
}
|
||
|
||
hash := sha256.New()
|
||
_, _ = hash.Write(bytes)
|
||
|
||
eula := &fleet.MDMEULA{
|
||
Name: name,
|
||
Token: uuid.New().String(),
|
||
Sha256: hash.Sum(nil),
|
||
Bytes: bytes,
|
||
}
|
||
|
||
if err := svc.ds.MDMInsertEULA(ctx, eula); err != nil {
|
||
return ctxerr.Wrap(ctx, err, "inserting EULA")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (svc *Service) MDMGetEULABytes(ctx context.Context, token string) (*fleet.MDMEULA, error) {
|
||
// skipauth: this resource is authorized using the token provided in the
|
||
// request.
|
||
svc.authz.SkipAuthorization(ctx)
|
||
|
||
return svc.ds.MDMGetEULABytes(ctx, token)
|
||
}
|
||
|
||
func (svc *Service) MDMDeleteEULA(ctx context.Context, token string, dryRun bool) error {
|
||
if err := svc.authz.Authorize(ctx, &fleet.MDMEULA{}, fleet.ActionWrite); err != nil {
|
||
return err
|
||
}
|
||
|
||
if dryRun {
|
||
return nil
|
||
}
|
||
|
||
if err := svc.ds.MDMDeleteEULA(ctx, token); err != nil {
|
||
return ctxerr.Wrap(ctx, err, "deleting EULA")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (svc *Service) MDMGetEULAMetadata(ctx context.Context) (*fleet.MDMEULA, error) {
|
||
if err := svc.authz.Authorize(ctx, &fleet.MDMEULA{}, fleet.ActionRead); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
eula, err := svc.ds.MDMGetEULAMetadata(ctx)
|
||
if err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "getting EULA metadata")
|
||
}
|
||
|
||
return eula, nil
|
||
}
|
||
|
||
func (svc *Service) SetOrUpdateMDMAppleSetupAssistant(ctx context.Context, asst *fleet.MDMAppleSetupAssistant) (*fleet.MDMAppleSetupAssistant, error) {
|
||
if err := svc.authz.Authorize(ctx, asst, fleet.ActionWrite); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// In order to validate if a configuration_web_url can be set for this setup
|
||
// assistant configuration, we need to know if end user authentication is
|
||
// enabled (either globally or for a specific team, if provided)
|
||
var endUserAuthEnabled bool
|
||
var teamName *string
|
||
var tm *fleet.Team
|
||
if asst.TeamID != nil {
|
||
var err error
|
||
tm, err = svc.ds.TeamWithExtras(ctx, *asst.TeamID) // TODO see if we can convert to TeamLite
|
||
if err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "get team")
|
||
}
|
||
teamName = &tm.Name
|
||
endUserAuthEnabled = tm.Config.MDM.MacOSSetup.EnableEndUserAuthentication
|
||
} else {
|
||
appCfg, err := svc.ds.AppConfig(ctx)
|
||
if err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "getting app config")
|
||
}
|
||
endUserAuthEnabled = appCfg.MDM.MacOSSetup.EnableEndUserAuthentication
|
||
}
|
||
|
||
var m map[string]any
|
||
if err := json.Unmarshal(asst.Profile, &m); err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "json unmarshal setup assistant profile")
|
||
}
|
||
if _, ok := m["configuration_web_url"]; ok && endUserAuthEnabled {
|
||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("profile", `Couldn't edit macos_setup_assistant. First, disable end user authentication before adding an automatic enrollment (DEP) profile with a configuration_web_url.`))
|
||
}
|
||
|
||
if _, ok := m["url"]; ok {
|
||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("profile", `Couldn't edit macos_setup_assistant. The automatic enrollment profile can't include url.`))
|
||
}
|
||
if _, ok := m["await_device_configured"]; ok {
|
||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("profile", `Couldn't edit macos_setup_assistant. The profile can't include "await_device_configured" option.`))
|
||
}
|
||
|
||
// must read the existing setup assistant first to detect if it did change
|
||
// so that we can skip sending to Apple if it did not change and so that the
|
||
// changed activity is not created if the same assistant was uploaded).
|
||
prevAsst, err := svc.ds.GetMDMAppleSetupAssistant(ctx, asst.TeamID)
|
||
if err != nil && !fleet.IsNotFound(err) {
|
||
return nil, ctxerr.Wrap(ctx, err, "get previous setup assistant")
|
||
}
|
||
|
||
validateIncomingSetupAssistant := true
|
||
// If name and contents are the same, skip validation with Apple. Name is included to match
|
||
// the logic we use below for determining whether or not to post the activity
|
||
if prevAsst != nil && asst.Name == prevAsst.Name {
|
||
var m2 map[string]any
|
||
if err := json.Unmarshal(prevAsst.Profile, &m2); err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "json unmarshal previous setup assistant profile")
|
||
}
|
||
if reflect.DeepEqual(m, m2) {
|
||
validateIncomingSetupAssistant = false
|
||
}
|
||
}
|
||
|
||
if validateIncomingSetupAssistant {
|
||
// Validate the profile with Apple's API. Don't save the profile if it isn't valid.
|
||
err = svc.depService.ValidateSetupAssistant(ctx, tm, asst, "")
|
||
if err != nil {
|
||
return nil, fleet.NewInvalidArgumentError("profile", err.Error())
|
||
}
|
||
}
|
||
|
||
newAsst, err := svc.ds.SetOrUpdateMDMAppleSetupAssistant(ctx, asst)
|
||
if err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "set or update setup assistant")
|
||
}
|
||
|
||
// if the name is the same and the content did not change, uploaded at will stay the same
|
||
if prevAsst == nil || newAsst.Name != prevAsst.Name || newAsst.UploadedAt.After(prevAsst.UploadedAt) {
|
||
if _, err := worker.QueueMacosSetupAssistantJob(
|
||
ctx,
|
||
svc.ds,
|
||
svc.logger,
|
||
worker.MacosSetupAssistantProfileChanged,
|
||
newAsst.TeamID); err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "enqueue macos setup assistant profile changed job")
|
||
}
|
||
|
||
if err := svc.NewActivity(
|
||
ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeChangedMacosSetupAssistant{
|
||
TeamID: newAsst.TeamID,
|
||
TeamName: teamName,
|
||
Name: newAsst.Name,
|
||
}); err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "create activity for changed macos setup assistant")
|
||
}
|
||
}
|
||
return newAsst, nil
|
||
}
|
||
|
||
func (svc *Service) GetMDMAppleSetupAssistant(ctx context.Context, teamID *uint) (*fleet.MDMAppleSetupAssistant, error) {
|
||
if err := svc.authz.Authorize(ctx, &fleet.MDMAppleSetupAssistant{TeamID: teamID}, fleet.ActionRead); err != nil {
|
||
return nil, err
|
||
}
|
||
return svc.ds.GetMDMAppleSetupAssistant(ctx, teamID)
|
||
}
|
||
|
||
func (svc *Service) DeleteMDMAppleSetupAssistant(ctx context.Context, teamID *uint) error {
|
||
if err := svc.authz.Authorize(ctx, &fleet.MDMAppleSetupAssistant{TeamID: teamID}, fleet.ActionWrite); err != nil {
|
||
return err
|
||
}
|
||
|
||
// must read the existing setup assistant first to detect if it did delete
|
||
// and to get the name of the deleted assistant.
|
||
prevAsst, err := svc.ds.GetMDMAppleSetupAssistant(ctx, teamID)
|
||
if err != nil && !fleet.IsNotFound(err) {
|
||
return ctxerr.Wrap(ctx, err, "get previous setup assistant")
|
||
}
|
||
|
||
if err := svc.ds.DeleteMDMAppleSetupAssistant(ctx, teamID); err != nil {
|
||
return ctxerr.Wrap(ctx, err, "delete setup assistant")
|
||
}
|
||
|
||
if prevAsst != nil {
|
||
if _, err := worker.QueueMacosSetupAssistantJob(
|
||
ctx,
|
||
svc.ds,
|
||
svc.logger,
|
||
worker.MacosSetupAssistantProfileDeleted,
|
||
teamID); err != nil {
|
||
return ctxerr.Wrap(ctx, err, "enqueue macos setup assistant profile deleted job")
|
||
}
|
||
|
||
var teamName *string
|
||
if teamID != nil {
|
||
tm, err := svc.ds.TeamWithExtras(ctx, *teamID)
|
||
if err != nil {
|
||
return ctxerr.Wrap(ctx, err, "get team")
|
||
}
|
||
teamName = &tm.Name
|
||
}
|
||
if err := svc.NewActivity(
|
||
ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeDeletedMacosSetupAssistant{
|
||
TeamID: teamID,
|
||
TeamName: teamName,
|
||
Name: prevAsst.Name,
|
||
}); err != nil {
|
||
return ctxerr.Wrap(ctx, err, "create activity for deleted macos setup assistant")
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
const appleMDMAccountDrivenEnrollmentUrl = "/api/mdm/apple/account_driven_enroll"
|
||
|
||
func (svc *Service) InitiateMDMSSO(ctx context.Context, initiator, customOriginalURL string, hostUUID string) (sessionID string, sessionDurationSeconds int, idpURL string, err error) {
|
||
// skipauth: User context does not yet exist. Unauthenticated users may
|
||
// initiate SSO.
|
||
svc.authz.SkipAuthorization(ctx)
|
||
|
||
logging.WithLevel(logging.WithNoUser(ctx), level.Info)
|
||
|
||
appConfig, err := svc.ds.AppConfig(ctx)
|
||
if err != nil {
|
||
return "", 0, "", ctxerr.Wrap(ctx, err, "getting app config")
|
||
}
|
||
|
||
mdmSSOSettings := appConfig.MDM.EndUserAuthentication.SSOProviderSettings
|
||
|
||
// SSO is disabled if no settings are provided since end_user_authentication is a global setting.
|
||
//
|
||
// Note: enable_end_user_authentication is a team-specific setting,
|
||
// this means some teams may not use SSO even if it is configured.
|
||
if mdmSSOSettings.IsEmpty() {
|
||
err := &fleet.BadRequestError{Message: "organization not configured to use sso"}
|
||
return "", 0, "", ctxerr.Wrap(ctx, err, "initiate mdm sso")
|
||
}
|
||
|
||
serverURL := appConfig.MDMUrl()
|
||
// Parse the URL and use JoinPath to avoid double slashes
|
||
parsedURL, err := url.Parse(serverURL)
|
||
if err != nil {
|
||
return "", 0, "", ctxerr.Wrap(ctx, err, "invalid MDM URL")
|
||
}
|
||
parsedURL = parsedURL.JoinPath(svc.config.Server.URLPrefix, "/api/v1/fleet/mdm/sso/callback")
|
||
acsURL := parsedURL.String()
|
||
|
||
samlProvider, err := sso.SAMLProviderFromConfiguredMetadata(ctx,
|
||
mdmSSOSettings.EntityID,
|
||
acsURL,
|
||
&mdmSSOSettings,
|
||
)
|
||
if err != nil {
|
||
return "", 0, "", ctxerr.Wrap(ctx, err, "failed to create provider from metadata")
|
||
}
|
||
|
||
originalURL := "/"
|
||
switch initiator {
|
||
case "account_driven_enroll":
|
||
// originalURL is unused in the Setup Experience initiated MDM flow
|
||
// however because we need slightly different behavior for account driven
|
||
// enrollment we use it to signal proper behavior on the callback.
|
||
originalURL = appleMDMAccountDrivenEnrollmentUrl
|
||
case "ota_enroll":
|
||
// for ota_enroll, we support the custom original URL argument, as the
|
||
// enroll secret used to enroll varies. Other initiators do not support
|
||
// a custom original URL (and should receive an empty string).
|
||
originalURL = customOriginalURL
|
||
}
|
||
|
||
sessionDurationSeconds = int(svc.config.Auth.SsoSessionValidityPeriod.Seconds())
|
||
sessionID, idpURL, err = sso.CreateAuthorizationRequest(ctx,
|
||
samlProvider, svc.ssoSessionStore, originalURL,
|
||
uint(sessionDurationSeconds), //nolint:gosec // dismiss G115
|
||
sso.SSORequestData{
|
||
HostUUID: hostUUID,
|
||
Initiator: initiator,
|
||
},
|
||
)
|
||
if err != nil {
|
||
return "", 0, "", ctxerr.Wrap(ctx, err, "InitiateMDMSSO creating authorization")
|
||
}
|
||
|
||
return sessionID, sessionDurationSeconds, idpURL, nil
|
||
}
|
||
|
||
func (svc *Service) MDMSSOCallback(ctx context.Context, sessionID string, samlResponse []byte) (redirectURL, byodCookieValue string) {
|
||
// skipauth: User context does not yet exist. Unauthenticated users may
|
||
// hit the SSO callback.
|
||
svc.authz.SkipAuthorization(ctx)
|
||
|
||
logging.WithLevel(logging.WithNoUser(ctx), level.Info)
|
||
|
||
profileToken, enrollmentRef, eulaToken, originalURL, ssoRequestData, err := svc.mdmSSOHandleCallbackAuth(ctx, sessionID, samlResponse)
|
||
if err != nil {
|
||
logging.WithErr(ctx, err)
|
||
return apple_mdm.FleetUISSOCallbackPath + "?error=true", ""
|
||
}
|
||
|
||
if !strings.HasPrefix(originalURL, "/enroll?") && ssoRequestData.Initiator != "setup_experience" {
|
||
// for flows other than the /enroll BYOD, we have to ensure that Apple MDM
|
||
// is enabled (this was previously done in a middleware on the route, but
|
||
// we do it here now so the middleware is disabled for the BYOD flow, which
|
||
// handles the MDM not enabled differently, via a custom error page, as it
|
||
// supports not just Apple MDM).
|
||
if err := svc.VerifyMDMAppleConfigured(ctx); err != nil {
|
||
logging.WithErr(ctx, err)
|
||
return apple_mdm.FleetUISSOCallbackPath + "?error=true", ""
|
||
}
|
||
}
|
||
|
||
q := url.Values{
|
||
"profile_token": {profileToken},
|
||
"enrollment_reference": {enrollmentRef},
|
||
}
|
||
if eulaToken != "" {
|
||
q.Add("eula_token", eulaToken)
|
||
}
|
||
|
||
q.Add("initiator", ssoRequestData.Initiator)
|
||
|
||
switch {
|
||
case originalURL == appleMDMAccountDrivenEnrollmentUrl:
|
||
// For account driven enrollment we have to use this special protocol URL scheme to pass the
|
||
// access token back to Apple which it will then use to request the enrollment profile.
|
||
return fmt.Sprintf("apple-remotemanagement-user-login://authentication-results?access-token=%s", enrollmentRef), ""
|
||
|
||
case strings.HasPrefix(originalURL, "/enroll?"):
|
||
// redirect to the original URL with a cookie that identifies this device
|
||
// as authenticated and links it to the authenticated email (the enrollment
|
||
// reference).
|
||
u, err := url.Parse(originalURL)
|
||
if err != nil {
|
||
logging.WithErr(ctx, err)
|
||
return "/enroll?error=" + url.QueryEscape("An error occurred. : Failed to parse original URL."), ""
|
||
}
|
||
// port over the query string values, which will copy over the enrollment
|
||
// secret
|
||
for k, v := range u.Query() {
|
||
q.Add(k, v[0])
|
||
}
|
||
u.RawQuery = q.Encode()
|
||
return u.String(), enrollmentRef
|
||
|
||
default:
|
||
return fmt.Sprintf("%s?%s", apple_mdm.FleetUISSOCallbackPath, q.Encode()), ""
|
||
}
|
||
}
|
||
|
||
func (svc *Service) mdmSSOHandleCallbackAuth(
|
||
ctx context.Context,
|
||
sessionID string,
|
||
samlResponse []byte,
|
||
) (profileToken string, enrollmentReference string,
|
||
eulaToken string, originalURL string, ssoRequestData sso.SSORequestData, err error,
|
||
) {
|
||
appConfig, err := svc.ds.AppConfig(ctx)
|
||
if err != nil {
|
||
return "", "", "", "", sso.SSORequestData{}, ctxerr.Wrap(ctx, err, "get config for sso")
|
||
}
|
||
|
||
serverURL := appConfig.MDMUrl()
|
||
acsURL, err := url.Parse(serverURL + svc.config.Server.URLPrefix + "/api/v1/fleet/mdm/sso/callback")
|
||
if err != nil {
|
||
return "", "", "", "", sso.SSORequestData{}, ctxerr.Wrap(ctx, err, "failed to parse ACS URL")
|
||
}
|
||
|
||
mdmSSOSettings := appConfig.MDM.EndUserAuthentication.SSOProviderSettings
|
||
|
||
// SSO is disabled if no settings are provided since end_user_authentication is a global setting.
|
||
//
|
||
// Note: enable_end_user_authentication is a team-specific setting,
|
||
// this means some teams may not use SSO even if it is configured.
|
||
if mdmSSOSettings.IsEmpty() {
|
||
err := &fleet.BadRequestError{Message: "organization not configured to use sso"}
|
||
return "", "", "", "", sso.SSORequestData{}, ctxerr.Wrap(ctx, err, "get config for mdm sso callback")
|
||
}
|
||
|
||
expectedAudiences := []string{
|
||
mdmSSOSettings.EntityID,
|
||
appConfig.MDMUrl(),
|
||
appConfig.MDMUrl() + svc.config.Server.URLPrefix + "/api/v1/fleet/mdm/sso/callback",
|
||
}
|
||
samlProvider, requestID, originalURL, ssoRequestData, err := sso.SAMLProviderFromSession(
|
||
ctx, sessionID, svc.ssoSessionStore, acsURL, mdmSSOSettings.EntityID, expectedAudiences,
|
||
)
|
||
if err != nil {
|
||
return "", "", "", "", sso.SSORequestData{}, ctxerr.Wrap(ctx, err, "failed to create provider from metadata")
|
||
}
|
||
|
||
// Parse and verify SAMLResponse (verifies fields, expected IDs and signature).
|
||
auth, err := sso.ParseAndVerifySAMLResponse(samlProvider, samlResponse, requestID, acsURL)
|
||
if err != nil {
|
||
// We actually don't return 401 to clients and instead return an HTML page with /login?status=error,
|
||
// but to be consistent we will return fleet.AuthFailedError which is used for unauthorized access.
|
||
return "", "", "", "", sso.SSORequestData{}, ctxerr.Wrap(ctx, fleet.NewAuthFailedError(err.Error()))
|
||
}
|
||
|
||
// Store information for automatic account population/creation
|
||
//
|
||
// For now, we just grab whatever comes before the `@` in UserID, which
|
||
// must be an email.
|
||
//
|
||
// For more details, check https://github.com/fleetdm/fleet/issues/10744#issuecomment-1540605146
|
||
username, _, found := strings.Cut(auth.UserID(), "@")
|
||
if !found {
|
||
svc.logger.Log("mdm-sso-callback", "IdP UserID doesn't look like an email, using raw value")
|
||
username = auth.UserID()
|
||
}
|
||
|
||
err = svc.ds.InsertMDMIdPAccount(ctx, &fleet.MDMIdPAccount{
|
||
UUID: ssoRequestData.HostUUID,
|
||
Username: username,
|
||
Fullname: auth.UserDisplayName(),
|
||
Email: auth.UserID(),
|
||
})
|
||
if err != nil {
|
||
return "", "", "", "", sso.SSORequestData{}, ctxerr.Wrap(ctx, err, "saving account data from IdP")
|
||
}
|
||
|
||
idpAcc, err := svc.ds.GetMDMIdPAccountByEmail(
|
||
// use the primary db as the account might have been just
|
||
// inserted
|
||
ctxdb.RequirePrimary(ctx, true),
|
||
auth.UserID(),
|
||
)
|
||
if err != nil {
|
||
return "", "", "", "", sso.SSORequestData{}, ctxerr.Wrap(ctx, err, "retrieving new account data from IdP")
|
||
}
|
||
|
||
// If the initiator is "setup_experience", we can insert the host idp account record
|
||
// right away, as the host uuid is provided in the SSO request data.
|
||
if ssoRequestData.Initiator == "setup_experience" && ssoRequestData.HostUUID != "" {
|
||
err = svc.ds.AssociateHostMDMIdPAccountDB(ctx, ssoRequestData.HostUUID, idpAcc.UUID)
|
||
if err != nil {
|
||
return "", "", "", "", sso.SSORequestData{}, ctxerr.Wrap(ctx, err, "saving host-account link from IdP")
|
||
}
|
||
}
|
||
|
||
eula, err := svc.ds.MDMGetEULAMetadata(ctx)
|
||
if err != nil && !fleet.IsNotFound(err) {
|
||
return "", "", "", "", sso.SSORequestData{}, ctxerr.Wrap(ctx, err, "getting EULA metadata")
|
||
}
|
||
|
||
if eula != nil {
|
||
eulaToken = eula.Token
|
||
}
|
||
|
||
// If this is account driven enrollment there is no need to fetch the profile
|
||
if originalURL == appleMDMAccountDrivenEnrollmentUrl {
|
||
return "", idpAcc.UUID, eulaToken, originalURL, ssoRequestData, nil
|
||
}
|
||
|
||
var depProfToken string
|
||
// For automatic enrollments, get the automatic profile to access the authentication token.
|
||
if ssoRequestData.Initiator != "setup_experience" {
|
||
depProf, err := svc.getAutomaticEnrollmentProfile(ctx)
|
||
if err != nil {
|
||
return "", "", "", "", sso.SSORequestData{}, ctxerr.Wrap(ctx, err, "listing profiles")
|
||
}
|
||
if depProf == nil {
|
||
return "", "", "", "", sso.SSORequestData{}, ctxerr.Wrap(ctx, errors.New("missing profile"), "missing profile")
|
||
}
|
||
depProfToken = depProf.Token
|
||
}
|
||
|
||
// using the idp token as a reference just because that's the
|
||
// only thing we're referencing later on during enrollment.
|
||
return depProfToken, idpAcc.UUID, eulaToken, originalURL, ssoRequestData, nil
|
||
}
|
||
|
||
func (svc *Service) mdmAppleSyncDEPProfiles(ctx context.Context) error {
|
||
if _, err := worker.QueueMacosSetupAssistantJob(ctx, svc.ds, svc.logger, worker.MacosSetupAssistantUpdateAllProfiles, nil); err != nil {
|
||
return ctxerr.Wrap(ctx, err, "queue macos setup assistant update all profiles job")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// returns the default automatic enrollment profile, or nil (without error) if none exists.
|
||
func (svc *Service) getAutomaticEnrollmentProfile(ctx context.Context) (*fleet.MDMAppleEnrollmentProfile, error) {
|
||
prof, err := svc.ds.GetMDMAppleEnrollmentProfileByType(ctx, fleet.MDMAppleEnrollmentTypeAutomatic)
|
||
if err != nil && !fleet.IsNotFound(err) {
|
||
return nil, ctxerr.Wrap(ctx, err, "get automatic profile")
|
||
}
|
||
return prof, nil
|
||
}
|
||
|
||
func (svc *Service) MDMApplePreassignProfile(ctx context.Context, payload fleet.MDMApplePreassignProfilePayload) error {
|
||
// for the preassign and match features, we don't know yet what team(s) will
|
||
// be affected, so we authorize only users with write-access to the no-team
|
||
// config profiles and with team-write access.
|
||
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{}, fleet.ActionWrite); err != nil {
|
||
return ctxerr.Wrap(ctx, err)
|
||
}
|
||
if err := svc.authz.Authorize(ctx, &fleet.Team{}, fleet.ActionWrite); err != nil {
|
||
return ctxerr.Wrap(ctx, err)
|
||
}
|
||
if err := svc.profileMatcher.PreassignProfile(ctx, payload); err != nil {
|
||
return ctxerr.Wrap(ctx, err, "preassign profile")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (svc *Service) MDMAppleMatchPreassignment(ctx context.Context, externalHostIdentifier string) error {
|
||
// for the preassign and match features, we don't know yet what team(s) will
|
||
// be affected, so we authorize only users with write-access to the no-team
|
||
// config profiles and with team-write access.
|
||
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{}, fleet.ActionWrite); err != nil {
|
||
return ctxerr.Wrap(ctx, err)
|
||
}
|
||
if err := svc.authz.Authorize(ctx, &fleet.Team{}, fleet.ActionWrite); err != nil {
|
||
return ctxerr.Wrap(ctx, err)
|
||
}
|
||
|
||
profs, err := svc.profileMatcher.RetrieveProfiles(ctx, externalHostIdentifier)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if len(profs.Profiles) == 0 || profs.HostUUID == "" {
|
||
return nil // nothing to do
|
||
}
|
||
|
||
// force-use the primary DB instance for all the following reads/writes
|
||
// (we may create a team and need to read it back, but also to get latest
|
||
// team matching the profiles)
|
||
ctx = ctxdb.RequirePrimary(ctx, true)
|
||
|
||
// load the host and ensure it is enrolled in Fleet MDM
|
||
host, err := svc.ds.HostByIdentifier(ctx, profs.HostUUID)
|
||
if err != nil {
|
||
return err // will return a not found error if host does not exist
|
||
}
|
||
|
||
connected, err := svc.ds.IsHostConnectedToFleetMDM(ctx, host)
|
||
if err != nil {
|
||
return ctxerr.Wrap(ctx, err, "checking if host is connected to Fleet")
|
||
}
|
||
if !connected {
|
||
err = errors.New("host is not enrolled in Fleet MDM")
|
||
return ctxerr.Wrap(ctx, &fleet.BadRequestError{
|
||
Message: err.Error(),
|
||
InternalErr: err,
|
||
})
|
||
}
|
||
|
||
// Collect the profiles' groups in case we need to create a new team,
|
||
// and the list of raw profiles bytes.
|
||
groups, rawProfiles := make([]string, 0, len(profs.Profiles)),
|
||
make([][]byte, 0, len(profs.Profiles))
|
||
for _, prof := range profs.Profiles {
|
||
if prof.Group != "" {
|
||
groups = append(groups, prof.Group)
|
||
}
|
||
|
||
if !prof.Exclude {
|
||
rawProfiles = append(rawProfiles, prof.Profile)
|
||
}
|
||
}
|
||
|
||
team, err := svc.getOrCreatePreassignTeam(ctx, groups)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// create profiles for that team via the service call, so that uniqueness
|
||
// of profile identifier/name is verified, activity created, etc.
|
||
if err := svc.BatchSetMDMAppleProfiles(ctx, &team.ID, nil, rawProfiles, false, true); err != nil {
|
||
return err
|
||
}
|
||
|
||
// assign host to that team via the service call, which will trigger
|
||
// deployment of the profiles.
|
||
if err := svc.AddHostsToTeam(ctx, &team.ID, []uint{host.ID}, true); err != nil {
|
||
return err
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (svc *Service) getOrCreatePreassignTeam(ctx context.Context, groups []string) (*fleet.Team, error) {
|
||
teamName := teamNameFromPreassignGroups(groups)
|
||
team, err := svc.ds.TeamByName(ctx, teamName)
|
||
if err != nil {
|
||
if !fleet.IsNotFound(err) {
|
||
return nil, err
|
||
}
|
||
|
||
// Create a new team for this set of groups. Creating via the service
|
||
// call so that it properly assigns the agent options and creates audit
|
||
// activities, etc.
|
||
payload := fleet.TeamPayload{Name: &teamName}
|
||
team, err = svc.NewTeam(ctx, payload)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Get default bootstrap package and end user auth settings for no team.
|
||
ac, err := svc.ds.AppConfig(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
spec := &fleet.TeamSpec{
|
||
Name: teamName,
|
||
MDM: fleet.TeamSpecMDM{
|
||
EnableDiskEncryption: optjson.SetBool(true),
|
||
MacOSSetup: fleet.MacOSSetup{
|
||
MacOSSetupAssistant: ac.MDM.MacOSSetup.MacOSSetupAssistant,
|
||
// NOTE: BootstrapPackage gets set by
|
||
// CopyDefaultMDMAppleBootstrapPackage below
|
||
// BootstrapPackage: ac.MDM.MacOSSetup.BootstrapPackage,
|
||
EnableEndUserAuthentication: ac.MDM.MacOSSetup.EnableEndUserAuthentication,
|
||
EnableReleaseDeviceManually: ac.MDM.MacOSSetup.EnableReleaseDeviceManually,
|
||
},
|
||
},
|
||
}
|
||
if _, err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{spec}, fleet.ApplyTeamSpecOptions{}); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := svc.ds.CopyDefaultMDMAppleBootstrapPackage(ctx, ac, team.ID); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// get the global setup assistant contents (this is different
|
||
// from MDM.MacOSSetup.MacOSSetupAssistant we set above, the
|
||
// prior is the path to the file, this is the actual file
|
||
// contents.
|
||
asst, err := svc.ds.GetMDMAppleSetupAssistant(ctx, nil)
|
||
if err != nil {
|
||
// if "no team" doesn't have custom setup assistant
|
||
// settings configured, this team won't have either.
|
||
if fleet.IsNotFound(err) {
|
||
return team, nil
|
||
}
|
||
return nil, ctxerr.Wrap(ctx, err, "get global setup assistant")
|
||
|
||
}
|
||
_, err = svc.SetOrUpdateMDMAppleSetupAssistant(ctx, &fleet.MDMAppleSetupAssistant{
|
||
TeamID: &team.ID,
|
||
Name: asst.Name,
|
||
Profile: asst.Profile,
|
||
})
|
||
if err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "set setup assistant for new team")
|
||
}
|
||
}
|
||
return team, nil
|
||
}
|
||
|
||
// teamNameFromPreassignGroups returns the team name to use for a new team
|
||
// created to match the set of profiles preassigned to a host. The team name is
|
||
// derived from the "group" field provided with each request to pre-assign a
|
||
// profile to a host (in fleet.MDMApplePreassignProfilePayload). That field is
|
||
// optional, and empty groups are ignored.
|
||
func teamNameFromPreassignGroups(groups []string) string {
|
||
const defaultName = "default"
|
||
|
||
dedupeGroups := make(map[string]struct{}, len(groups))
|
||
for _, group := range groups {
|
||
if group != "" {
|
||
dedupeGroups[group] = struct{}{}
|
||
}
|
||
}
|
||
groups = groups[:0]
|
||
for group := range dedupeGroups {
|
||
groups = append(groups, group)
|
||
}
|
||
sort.Strings(groups)
|
||
|
||
if len(groups) == 0 {
|
||
groups = []string{defaultName}
|
||
}
|
||
|
||
return strings.Join(groups, " - ")
|
||
}
|
||
|
||
func (svc *Service) GetMDMDiskEncryptionSummary(ctx context.Context, teamID *uint) (*fleet.MDMDiskEncryptionSummary, error) {
|
||
if err := svc.authz.Authorize(ctx, fleet.MDMConfigProfileAuthz{TeamID: teamID}, fleet.ActionRead); err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err)
|
||
}
|
||
var macOS fleet.MDMAppleFileVaultSummary
|
||
if m, err := svc.ds.GetMDMAppleFileVaultSummary(ctx, teamID); err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "getting filevault summary")
|
||
} else if m != nil {
|
||
macOS = *m
|
||
}
|
||
|
||
var windows fleet.MDMWindowsBitLockerSummary
|
||
if w, err := svc.ds.GetMDMWindowsBitLockerSummary(ctx, teamID); err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "getting bitlocker summary")
|
||
} else if w != nil {
|
||
windows = *w
|
||
}
|
||
|
||
// Linux doesn't have configuration profiles, so if we aren't enforcing disk encryption we have nothing to report
|
||
diskEncryptionConfig, err := svc.ds.GetConfigEnableDiskEncryption(ctx, teamID)
|
||
if err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "check if disk encryption is enabled")
|
||
}
|
||
|
||
var linux fleet.MDMLinuxDiskEncryptionSummary
|
||
if diskEncryptionConfig.Enabled {
|
||
linux, err = svc.ds.GetLinuxDiskEncryptionSummary(ctx, teamID)
|
||
if err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "getting linux disk encryption summary")
|
||
}
|
||
}
|
||
|
||
return &fleet.MDMDiskEncryptionSummary{
|
||
Verified: fleet.MDMPlatformsCounts{
|
||
MacOS: macOS.Verified,
|
||
Windows: windows.Verified,
|
||
Linux: linux.Verified,
|
||
},
|
||
Verifying: fleet.MDMPlatformsCounts{
|
||
MacOS: macOS.Verifying,
|
||
Windows: windows.Verifying,
|
||
},
|
||
ActionRequired: fleet.MDMPlatformsCounts{
|
||
MacOS: macOS.ActionRequired,
|
||
Windows: windows.ActionRequired,
|
||
Linux: linux.ActionRequired,
|
||
},
|
||
Enforcing: fleet.MDMPlatformsCounts{
|
||
MacOS: macOS.Enforcing,
|
||
Windows: windows.Enforcing,
|
||
},
|
||
Failed: fleet.MDMPlatformsCounts{
|
||
MacOS: macOS.Failed,
|
||
Windows: windows.Failed,
|
||
Linux: linux.Failed,
|
||
},
|
||
RemovingEnforcement: fleet.MDMPlatformsCounts{
|
||
MacOS: macOS.RemovingEnforcement,
|
||
Windows: windows.RemovingEnforcement,
|
||
},
|
||
}, nil
|
||
}
|
||
|
||
func (svc *Service) mdmAppleEditedAppleOSUpdates(ctx context.Context, teamID *uint, appleDevice fleet.AppleDevice, updates fleet.AppleOSUpdateSettings) error {
|
||
const (
|
||
softwareUpdateType = `com.apple.configuration.softwareupdate.enforcement.specific`
|
||
softwareUpdateIdentSuffix = `-software-update-94f4bbdf-f439-4fb1-8d27-ae1bb793e105`
|
||
)
|
||
var (
|
||
softwareUpdateIdentifier string
|
||
osUpdatesProfileName string
|
||
labelName string
|
||
)
|
||
switch appleDevice {
|
||
case fleet.MacOS:
|
||
softwareUpdateIdentifier = "macos" + softwareUpdateIdentSuffix
|
||
osUpdatesProfileName = mdm.FleetMacOSUpdatesProfileName
|
||
labelName = fleet.BuiltinLabelMacOS14Plus // OS update DDMs are supported on macOS 14+ devices.
|
||
case fleet.IOS:
|
||
softwareUpdateIdentifier = "ios" + softwareUpdateIdentSuffix
|
||
osUpdatesProfileName = mdm.FleetIOSUpdatesProfileName
|
||
labelName = fleet.BuiltinLabelIOS
|
||
case fleet.IPadOS:
|
||
softwareUpdateIdentifier = "ipados" + softwareUpdateIdentSuffix
|
||
osUpdatesProfileName = mdm.FleetIPadOSUpdatesProfileName
|
||
labelName = fleet.BuiltinLabelIPadOS
|
||
default:
|
||
panic(fmt.Sprintf("invalid AppleDevice: %d", appleDevice))
|
||
}
|
||
|
||
if updates.MinimumVersion.Value == "" {
|
||
// OS updates disabled, remove the profile
|
||
if err := svc.ds.DeleteMDMAppleDeclarationByName(ctx, teamID, osUpdatesProfileName); err != nil {
|
||
return err
|
||
}
|
||
var globalOrTeamID uint
|
||
if teamID != nil {
|
||
globalOrTeamID = *teamID
|
||
}
|
||
// This only sets profiles that haven't been queued by the cron to 'pending' (both removes and installs, which includes
|
||
// the OS updates we just deleted). It doesn't have a functional difference because if you don't call this function
|
||
// the cron will catch up, but it's important for the UX to mark them as pending immediately so it's reflected in the UI.
|
||
if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{globalOrTeamID}, nil, nil); err != nil {
|
||
return ctxerr.Wrap(ctx, err, "bulk set pending host profiles")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// OS updates enabled, create or update the profile with the current settings.
|
||
|
||
rawDecl := []byte(fmt.Sprintf(`{
|
||
"Identifier": %q,
|
||
"Type": %q,
|
||
"Payload": {
|
||
"TargetOSVersion": %q,
|
||
"TargetLocalDateTime": "%sT12:00:00"
|
||
}
|
||
}`, softwareUpdateIdentifier, softwareUpdateType, updates.MinimumVersion.Value, updates.Deadline.Value))
|
||
|
||
d := fleet.NewMDMAppleDeclaration(rawDecl, teamID, osUpdatesProfileName, softwareUpdateType, softwareUpdateIdentifier)
|
||
|
||
// Associate the profile with the built-in label to ensure that the profile is applied to the targeted devices.
|
||
lblIDs, err := svc.ds.LabelIDsByName(ctx, []string{labelName}, fleet.TeamFilter{}) // built-in labels are global
|
||
if err != nil {
|
||
return err
|
||
}
|
||
d.LabelsIncludeAll = []fleet.ConfigurationProfileLabel{
|
||
{LabelName: labelName, LabelID: lblIDs[labelName]},
|
||
}
|
||
|
||
decl, err := svc.ds.SetOrUpdateMDMAppleDeclaration(ctx, d)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl.DeclarationUUID}, nil); err != nil {
|
||
return ctxerr.Wrap(ctx, err, "bulk set pending host declarations")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (svc *Service) mdmWindowsEnableOSUpdates(ctx context.Context, teamID *uint, updates fleet.WindowsUpdates) error {
|
||
var contents bytes.Buffer
|
||
params := windowsOSUpdatesProfileOptions{
|
||
Deadline: updates.DeadlineDays.Value,
|
||
GracePeriod: updates.GracePeriodDays.Value,
|
||
}
|
||
if err := windowsOSUpdatesProfileTemplate.Execute(&contents, params); err != nil {
|
||
return ctxerr.Wrap(ctx, err, "enabling Windows OS updates")
|
||
}
|
||
|
||
err := svc.ds.SetOrUpdateMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{
|
||
TeamID: teamID,
|
||
Name: mdm.FleetWindowsOSUpdatesProfileName,
|
||
SyncML: contents.Bytes(),
|
||
})
|
||
if err != nil {
|
||
return ctxerr.Wrap(ctx, err, "create Windows OS updates profile")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (svc *Service) mdmWindowsDisableOSUpdates(ctx context.Context, teamID *uint) error {
|
||
err := svc.ds.DeleteMDMWindowsConfigProfileByTeamAndName(ctx, teamID, mdm.FleetWindowsOSUpdatesProfileName)
|
||
return ctxerr.Wrap(ctx, err, "delete Windows OS updates profile")
|
||
}
|
||
|
||
func (svc *Service) GetMDMManualEnrollmentProfile(ctx context.Context) ([]byte, error) {
|
||
if err := svc.authz.Authorize(ctx, &fleet.MDMAppleManualEnrollmentProfile{}, fleet.ActionRead); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
appConfig, err := svc.ds.AppConfig(ctx)
|
||
if err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err)
|
||
}
|
||
|
||
topic, err := assets.APNSTopic(ctx, svc.ds)
|
||
if err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "extracting topic from APNs cert")
|
||
}
|
||
|
||
assets, err := svc.ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{
|
||
fleet.MDMAssetSCEPChallenge,
|
||
}, nil)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("loading SCEP challenge from the database: %w", err)
|
||
}
|
||
|
||
mobileConfig, err := apple_mdm.GenerateEnrollmentProfileMobileconfig(
|
||
appConfig.OrgInfo.OrgName,
|
||
appConfig.MDMUrl(),
|
||
string(assets[fleet.MDMAssetSCEPChallenge].Value),
|
||
topic,
|
||
)
|
||
if err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err)
|
||
}
|
||
|
||
// NOTE: the profile returned by this endpoint is intentionally not
|
||
// signed so it can be modified and signed by the IT admin with a
|
||
// custom certificate.
|
||
//
|
||
// Per @marko-lisica, we can add a parameter like `signed=true` if the
|
||
// need arises.
|
||
return mobileConfig, nil
|
||
}
|
||
|
||
func (svc *Service) UploadABMToken(ctx context.Context, token io.Reader) (*fleet.ABMToken, error) {
|
||
encryptedToken, decryptedToken, err := svc.decryptUploadedABMToken(ctx, token)
|
||
if err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "decrypting uploaded ABM token")
|
||
}
|
||
|
||
if err := svc.authz.Authorize(ctx, &fleet.AppleBM{}, fleet.ActionWrite); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
tok := &fleet.ABMToken{
|
||
EncryptedToken: encryptedToken,
|
||
}
|
||
|
||
if err := apple_mdm.SetDecryptedABMTokenMetadata(ctx, tok, decryptedToken, svc.depStorage, svc.ds, svc.logger, false); err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "setting ABM token metadata")
|
||
}
|
||
|
||
tok, err = svc.ds.InsertABMToken(ctx, tok)
|
||
if err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "save ABM token")
|
||
}
|
||
|
||
// flip the app config flag
|
||
appCfg, err := svc.ds.AppConfig(ctx)
|
||
if err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "retrieving app config")
|
||
}
|
||
|
||
appCfg.MDM.AppleBMEnabledAndConfigured = true
|
||
|
||
if err := svc.ds.SaveAppConfig(ctx, appCfg); err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "saving app config after ABM enablement")
|
||
}
|
||
|
||
return tok, nil
|
||
}
|
||
|
||
func (svc *Service) DeleteABMToken(ctx context.Context, tokenID uint) error {
|
||
if err := svc.authz.Authorize(ctx, &fleet.AppleBM{}, fleet.ActionWrite); err != nil {
|
||
return err
|
||
}
|
||
|
||
if err := svc.ds.DeleteABMToken(ctx, tokenID); err != nil {
|
||
return ctxerr.Wrap(ctx, err, "removing ABM token")
|
||
}
|
||
|
||
count, err := svc.ds.GetABMTokenCount(ctx)
|
||
if err != nil {
|
||
return ctxerr.Wrap(ctx, err, "getting ABM token count")
|
||
}
|
||
|
||
if count == 0 {
|
||
// flip the app config flag
|
||
appCfg, err := svc.ds.AppConfig(ctx)
|
||
if err != nil {
|
||
return ctxerr.Wrap(ctx, err, "retrieving app config")
|
||
}
|
||
|
||
appCfg.MDM.AppleBMEnabledAndConfigured = false
|
||
return svc.ds.SaveAppConfig(ctx, appCfg)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (svc *Service) ListABMTokens(ctx context.Context) ([]*fleet.ABMToken, error) {
|
||
if err := svc.authz.Authorize(ctx, &fleet.AppleBM{}, fleet.ActionList); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
tokens, err := svc.ds.ListABMTokens(ctx)
|
||
if err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "list ABM tokens")
|
||
}
|
||
|
||
return tokens, nil
|
||
}
|
||
|
||
func (svc *Service) CountABMTokens(ctx context.Context) (int, error) {
|
||
// Authorizing using the more general AppConfig object because:
|
||
// - this service method returns a count, which is not sensitive information
|
||
// - gitops role, which needs this info, is not authorized for AppleBM access (as of 2024/12/02)
|
||
if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionRead); err != nil {
|
||
return 0, err
|
||
}
|
||
|
||
tokens, err := svc.ds.GetABMTokenCount(ctx)
|
||
if err != nil {
|
||
return 0, ctxerr.Wrap(ctx, err, "count ABM tokens")
|
||
}
|
||
|
||
return tokens, nil
|
||
}
|
||
|
||
func (svc *Service) UpdateABMTokenTeams(ctx context.Context, tokenID uint, macOSTeamID, iOSTeamID, iPadOSTeamID *uint) (*fleet.ABMToken, error) {
|
||
if err := svc.authz.Authorize(ctx, &fleet.AppleBM{}, fleet.ActionWrite); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
token, err := svc.ds.GetABMTokenByID(ctx, tokenID)
|
||
if err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "get ABM token to update teams")
|
||
}
|
||
|
||
// validate the team IDs
|
||
token.MacOSTeam = fleet.ABMTokenTeam{Name: fleet.TeamNameNoTeam}
|
||
token.MacOSDefaultTeamID = nil
|
||
token.IOSTeam = fleet.ABMTokenTeam{Name: fleet.TeamNameNoTeam}
|
||
token.IOSDefaultTeamID = nil
|
||
token.IPadOSTeam = fleet.ABMTokenTeam{Name: fleet.TeamNameNoTeam}
|
||
token.IPadOSDefaultTeamID = nil
|
||
|
||
if macOSTeamID != nil && *macOSTeamID != 0 {
|
||
macOSTeam, err := svc.ds.TeamLite(ctx, *macOSTeamID)
|
||
if err != nil {
|
||
return nil, &fleet.BadRequestError{
|
||
Message: fmt.Sprintf("team with ID %d not found", *macOSTeamID),
|
||
InternalErr: ctxerr.Wrap(ctx, err, "checking existence of macOS team"),
|
||
}
|
||
}
|
||
|
||
token.MacOSTeam.Name = macOSTeam.Name
|
||
token.MacOSTeam.ID = *macOSTeamID
|
||
token.MacOSDefaultTeamID = macOSTeamID
|
||
}
|
||
|
||
if iOSTeamID != nil && *iOSTeamID != 0 {
|
||
iOSTeam, err := svc.ds.TeamLite(ctx, *iOSTeamID)
|
||
if err != nil {
|
||
return nil, &fleet.BadRequestError{
|
||
Message: fmt.Sprintf("team with ID %d not found", *iOSTeamID),
|
||
InternalErr: ctxerr.Wrap(ctx, err, "checking existence of iOS team"),
|
||
}
|
||
}
|
||
token.IOSTeam.Name = iOSTeam.Name
|
||
token.IOSTeam.ID = *iOSTeamID
|
||
token.IOSDefaultTeamID = iOSTeamID
|
||
}
|
||
|
||
if iPadOSTeamID != nil && *iPadOSTeamID != 0 {
|
||
iPadOSTeam, err := svc.ds.TeamLite(ctx, *iPadOSTeamID)
|
||
if err != nil {
|
||
return nil, &fleet.BadRequestError{
|
||
Message: fmt.Sprintf("team with ID %d not found", *iPadOSTeamID),
|
||
InternalErr: ctxerr.Wrap(ctx, err, "checking existence of iPadOS team"),
|
||
}
|
||
}
|
||
token.IPadOSTeam.Name = iPadOSTeam.Name
|
||
token.IPadOSTeam.ID = *iPadOSTeamID
|
||
token.IPadOSDefaultTeamID = iPadOSTeamID
|
||
}
|
||
|
||
if err := svc.ds.SaveABMToken(ctx, token); err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "updating token teams in db")
|
||
}
|
||
|
||
return token, nil
|
||
}
|
||
|
||
func (svc *Service) RenewABMToken(ctx context.Context, token io.Reader, tokenID uint) (*fleet.ABMToken, error) {
|
||
if err := svc.authz.Authorize(ctx, &fleet.AppleBM{}, fleet.ActionRead); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
oldTok, err := svc.ds.GetABMTokenByID(ctx, tokenID)
|
||
if err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "get ABM token by id")
|
||
}
|
||
|
||
encryptedToken, decryptedToken, err := svc.decryptUploadedABMToken(ctx, token)
|
||
if err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "decrypting ABM token for renewal")
|
||
}
|
||
|
||
if err := apple_mdm.SetDecryptedABMTokenMetadata(ctx, oldTok, decryptedToken, svc.depStorage, svc.ds, svc.logger, true); err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "setting ABM token metadata")
|
||
}
|
||
|
||
oldTok.EncryptedToken = encryptedToken
|
||
|
||
if err := svc.ds.SaveABMToken(ctx, oldTok); err != nil {
|
||
return nil, ctxerr.Wrap(ctx, err, "renewing ABM token")
|
||
}
|
||
|
||
return oldTok, nil
|
||
}
|
||
|
||
func (svc *Service) decryptUploadedABMToken(ctx context.Context, token io.Reader) (encryptedToken []byte, decryptedToken *client.OAuth1Tokens, err error) {
|
||
if err := svc.authz.Authorize(ctx, &fleet.AppleBM{}, fleet.ActionRead); err != nil {
|
||
return nil, nil, err
|
||
}
|
||
|
||
pair, err := svc.ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{
|
||
fleet.MDMAssetABMCert,
|
||
fleet.MDMAssetABMKey,
|
||
}, nil)
|
||
if err != nil {
|
||
if fleet.IsNotFound(err) {
|
||
return nil, nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{
|
||
Message: "Please generate a keypair first.",
|
||
}, "renewing ABM token")
|
||
}
|
||
|
||
return nil, nil, ctxerr.Wrap(ctx, err, "retrieving stored ABM assets")
|
||
}
|
||
|
||
encryptedToken, err = io.ReadAll(token)
|
||
if err != nil {
|
||
return nil, nil, ctxerr.Wrap(ctx, err, "reading token bytes")
|
||
}
|
||
|
||
derCert, _ := pem.Decode(pair[fleet.MDMAssetABMCert].Value)
|
||
if derCert == nil {
|
||
return nil, nil, ctxerr.New(ctx, "ABM certificate in the database cannot be parsed")
|
||
}
|
||
|
||
cert, err := x509.ParseCertificate(derCert.Bytes)
|
||
if err != nil {
|
||
return nil, nil, ctxerr.Wrap(ctx, err, "parsing ABM certificate")
|
||
}
|
||
|
||
decryptedToken, err = assets.DecryptRawABMToken(encryptedToken, cert, pair[fleet.MDMAssetABMKey].Value)
|
||
if err != nil {
|
||
return nil, nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{
|
||
Message: "Invalid token. Please provide a valid token from Apple Business Manager.",
|
||
InternalErr: err,
|
||
}, "validating ABM token")
|
||
}
|
||
return encryptedToken, decryptedToken, nil
|
||
}
|