mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 21:47:20 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #37192 - Move /server/service/middleware/endpoint_utils to /server/platform/endpointer - Move /server/service/middleware/authzcheck to /server/platform/middleware/authzcheck - Move /server/service/middleware/ratelimit to /server/platform/middleware/ratelimit # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **Refactor** * Reorganized internal endpoint utilities to a centralized platform location for improved code organization and maintainability. No functional changes to existing features or APIs. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
224 lines
7.4 KiB
Go
224 lines
7.4 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/authz"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
|
|
"github.com/fleetdm/fleet/v4/server/platform/endpointer"
|
|
"github.com/fleetdm/fleet/v4/server/variables"
|
|
)
|
|
|
|
func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, profileName string, data []byte, labels []string, labelsMembershipMode fleet.MDMLabelsMode) (*fleet.MDMWindowsConfigProfile, error) {
|
|
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: &teamID}, fleet.ActionWrite); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err)
|
|
}
|
|
|
|
// check that Windows MDM is enabled - the middleware of that endpoint checks
|
|
// only that any MDM is enabled, maybe it's just macOS
|
|
if err := svc.VerifyMDMWindowsConfigured(ctx); err != nil {
|
|
err := fleet.NewInvalidArgumentError("profile", fleet.WindowsMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest)
|
|
return nil, ctxerr.Wrap(ctx, err, "check windows MDM enabled")
|
|
}
|
|
|
|
var teamName string
|
|
if teamID > 0 {
|
|
tm, err := svc.EnterpriseOverrides.TeamByIDOrName(ctx, &teamID, nil)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err)
|
|
}
|
|
teamName = tm.Name
|
|
}
|
|
|
|
cp := fleet.MDMWindowsConfigProfile{
|
|
TeamID: &teamID,
|
|
Name: profileName,
|
|
SyncML: data,
|
|
}
|
|
if err := cp.ValidateUserProvided(svc.config.MDM.EnableCustomOSUpdatesAndFileVault); err != nil {
|
|
msg := err.Error()
|
|
if strings.Contains(msg, syncml.DiskEncryptionProfileRestrictionErrMsg) {
|
|
return nil, ctxerr.Wrap(ctx,
|
|
&fleet.BadRequestError{Message: msg + " To control these settings use disk encryption endpoint."})
|
|
}
|
|
|
|
// this is not great, but since the validations are shared between the CLI
|
|
// and the API, we must make some changes to error message here.
|
|
if ix := strings.Index(msg, "To control these settings,"); ix >= 0 {
|
|
msg = strings.TrimSpace(msg[:ix])
|
|
}
|
|
err := &fleet.BadRequestError{Message: "Couldn't add. " + msg}
|
|
return nil, ctxerr.Wrap(ctx, err, "validate profile")
|
|
}
|
|
|
|
labelMap, err := svc.validateProfileLabels(ctx, &teamID, labels)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "validating labels")
|
|
}
|
|
switch labelsMembershipMode {
|
|
case fleet.LabelsIncludeAny:
|
|
cp.LabelsIncludeAny = labelMap
|
|
case fleet.LabelsExcludeAny:
|
|
cp.LabelsExcludeAny = labelMap
|
|
default:
|
|
// default include all
|
|
cp.LabelsIncludeAll = labelMap
|
|
}
|
|
|
|
if err := svc.ds.ValidateEmbeddedSecrets(ctx, []string{string(cp.SyncML)}); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("profile", err.Error()))
|
|
}
|
|
|
|
// Get license for validation
|
|
lic, err := svc.License(ctx)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "checking license")
|
|
}
|
|
|
|
groupedCAs, err := svc.ds.GetGroupedCertificateAuthorities(ctx, true)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "getting grouped certificate authorities")
|
|
}
|
|
|
|
foundVars, err := validateWindowsProfileFleetVariables(string(cp.SyncML), lic, groupedCAs)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err)
|
|
}
|
|
|
|
// Collect Fleet variables used in the profile
|
|
var usesFleetVars []fleet.FleetVarName
|
|
for _, varName := range foundVars {
|
|
usesFleetVars = append(usesFleetVars, fleet.FleetVarName(varName))
|
|
}
|
|
|
|
newCP, err := svc.ds.NewMDMWindowsConfigProfile(ctx, cp, usesFleetVars)
|
|
if err != nil {
|
|
var existsErr endpointer.ExistsErrorInterface
|
|
if errors.As(err, &existsErr) {
|
|
err = fleet.NewInvalidArgumentError("profile", SameProfileNameUploadErrorMsg).
|
|
WithStatus(http.StatusConflict)
|
|
}
|
|
return nil, ctxerr.Wrap(ctx, err)
|
|
}
|
|
|
|
if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{newCP.ProfileUUID}, nil); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "bulk set pending host profiles")
|
|
}
|
|
|
|
var (
|
|
actTeamID *uint
|
|
actTeamName *string
|
|
)
|
|
if teamID > 0 {
|
|
actTeamID = &teamID
|
|
actTeamName = &teamName
|
|
}
|
|
if err := svc.NewActivity(
|
|
ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeCreatedWindowsProfile{
|
|
TeamID: actTeamID,
|
|
TeamName: actTeamName,
|
|
ProfileName: newCP.Name,
|
|
}); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "logging activity for create mdm windows config profile")
|
|
}
|
|
|
|
return newCP, nil
|
|
}
|
|
|
|
// fleetVarsSupportedInWindowsProfiles lists the Fleet variables that are
|
|
// supported in Windows configuration profiles.
|
|
// except prefix variables
|
|
var fleetVarsSupportedInWindowsProfiles = []fleet.FleetVarName{
|
|
fleet.FleetVarHostUUID,
|
|
fleet.FleetVarHostHardwareSerial,
|
|
fleet.FleetVarSCEPWindowsCertificateID,
|
|
fleet.FleetVarSCEPRenewalID,
|
|
fleet.FleetVarHostEndUserIDPUsername,
|
|
fleet.FleetVarHostEndUserIDPUsernameLocalPart,
|
|
fleet.FleetVarHostEndUserIDPFullname,
|
|
fleet.FleetVarHostEndUserIDPDepartment,
|
|
fleet.FleetVarHostEndUserIDPGroups,
|
|
fleet.FleetVarHostPlatform,
|
|
}
|
|
|
|
func validateWindowsProfileFleetVariables(contents string, lic *fleet.LicenseInfo, groupedCAs *fleet.GroupedCertificateAuthorities) ([]string, error) {
|
|
foundVars := variables.Find(contents)
|
|
if len(foundVars) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Check for premium license if the profile contains Fleet variables
|
|
if lic == nil || !lic.IsPremium() {
|
|
return nil, fleet.ErrMissingLicense
|
|
}
|
|
|
|
// Check if all found variables are supported
|
|
for _, varName := range foundVars {
|
|
if !slices.Contains(fleetVarsSupportedInWindowsProfiles, fleet.FleetVarName(varName)) &&
|
|
!strings.HasPrefix(varName, string(fleet.FleetVarCustomSCEPChallengePrefix)) &&
|
|
!strings.HasPrefix(varName, string(fleet.FleetVarCustomSCEPProxyURLPrefix)) {
|
|
return nil, fleet.NewInvalidArgumentError("profile", fmt.Sprintf("Fleet variable $FLEET_VAR_%s is not supported in Windows profiles.", varName))
|
|
}
|
|
}
|
|
|
|
err := validateProfileCertificateAuthorityVariables(contents, lic, groupedCAs, nil, additionalCustomSCEPValidationForWindowsProfiles, nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Do additional validation that both custom SCEP URL and challenge vars are provided and not using different CA names etc.
|
|
|
|
return foundVars, nil
|
|
}
|
|
|
|
func additionalCustomSCEPValidationForWindowsProfiles(contents string, customSCEPVars *CustomSCEPVarsFound) error {
|
|
if customSCEPVars == nil {
|
|
return nil
|
|
}
|
|
|
|
var cmdMsg *fleet.SyncMLCmd
|
|
dec := xml.NewDecoder(bytes.NewReader(bytes.TrimSpace([]byte(contents))))
|
|
for {
|
|
if err := dec.Decode(&cmdMsg); err != nil { // EOF is fine in this case
|
|
if errors.Is(err, io.EOF) {
|
|
break
|
|
}
|
|
return fmt.Errorf("The payload isn't valid XML: %w", err)
|
|
}
|
|
if cmdMsg == nil {
|
|
break
|
|
}
|
|
|
|
// cmdMsg represents a top-level command (<Replace>...</Replace>)
|
|
// so we look through all items, often there is only one item per command.
|
|
for _, cmd := range cmdMsg.Items {
|
|
if cmd.Target == nil {
|
|
continue
|
|
}
|
|
|
|
if strings.HasSuffix(*cmd.Target, "/Install/SubjectName") {
|
|
// SubjectName item found, check that it contains the expected renewal ID variable
|
|
if cmd.Data == nil {
|
|
return errors.New("SubjectName item is missing data")
|
|
}
|
|
|
|
if !strings.Contains(cmd.Data.Content, "OU="+fleet.FleetVarSCEPRenewalID.WithPrefix()) && !strings.Contains(cmd.Data.Content, "OU="+fleet.FleetVarSCEPRenewalID.WithBraces()) {
|
|
// Does not contain the renewal ID in any of it's two fleet var forms as the OU field
|
|
return fmt.Errorf("SubjectName item must contain the %s variable in the OU field", fleet.FleetVarSCEPRenewalID.WithPrefix())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|