macOS managed local account foundations (#43381)

Implements both #42942 and #42943 

Co-authored-by: jkatz01 <yehonatankatz@gmail.com>
This commit is contained in:
Carlo
2026-04-22 09:05:07 -04:00
committed by GitHub
co-authored by jkatz01
parent 0d8c8978ea
commit 39e4f616ea
31 changed files with 1588 additions and 42 deletions
+50
View File
@@ -8,6 +8,7 @@ import (
"net/http"
"time"
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
@@ -705,3 +706,52 @@ var (
</Item>
</Exec>`
)
func (svc *Service) GetHostManagedAccountPassword(ctx context.Context, hostID uint) (*fleet.HostManagedLocalAccountPassword, error) {
// First ensure the user has access to list hosts, then check the specific
// host once team_id is loaded.
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
return nil, err
}
host, err := svc.ds.HostLite(ctx, hostID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get host lite")
}
if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
return nil, err
}
if !fleet.IsMacOSPlatform(host.Platform) {
return nil, &fleet.BadRequestError{
Message: "Host is not a macOS device.",
}
}
acct, err := svc.ds.GetHostManagedLocalAccountStatus(ctx, host.UUID)
if err != nil {
if fleet.IsNotFound(err) {
return nil, &fleet.BadRequestError{
Message: "Host does not have a managed account.",
}
}
return nil, ctxerr.Wrap(ctx, err, "get host managed account status")
}
if acct.Status == nil || *acct.Status != string(fleet.MDMDeliveryVerified) {
return nil, &fleet.BadRequestError{
Message: "Host's managed account password is not yet verified.",
}
}
pwd, err := svc.ds.GetHostManagedLocalAccountPassword(ctx, host.UUID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get host managed account password")
}
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityTypeViewedManagedLocalAccount{
HostID: host.ID,
HostDisplayName: host.DisplayName(),
}); err != nil {
return nil, ctxerr.Wrap(ctx, err, "create viewed managed local account activity")
}
return pwd, nil
}
+109
View File
@@ -0,0 +1,109 @@
package service
import (
"context"
"testing"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/ptr"
)
func TestGetHostManagedAccountPasswordAuth(t *testing.T) {
t.Parallel()
ds := new(mock.Store)
svc, baseSvc := newTestServiceWithMock(t, ds)
teamID := uint(1)
verified := string(fleet.MDMDeliveryVerified)
ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
return &fleet.Host{ID: hostID, UUID: "test-uuid", Platform: "darwin", TeamID: &teamID}, nil
}
ds.GetHostManagedLocalAccountStatusFunc = func(ctx context.Context, hostUUID string) (*fleet.HostMDMManagedLocalAccount, error) {
return &fleet.HostMDMManagedLocalAccount{Status: &verified, PasswordAvailable: true}, nil
}
ds.GetHostManagedLocalAccountPasswordFunc = func(ctx context.Context, hostUUID string) (*fleet.HostManagedLocalAccountPassword, error) {
return &fleet.HostManagedLocalAccountPassword{}, nil
}
baseSvc.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
return nil
}
testCases := []struct {
name string
user *fleet.User
shouldFail bool
}{
{
"global admin",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
},
{
"global maintainer",
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
false,
},
{
"global observer",
&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
false,
},
{
"global observer+",
&fleet.User{GlobalRole: ptr.String(fleet.RoleObserverPlus)},
false,
},
{
"global gitops",
&fleet.User{GlobalRole: ptr.String(fleet.RoleGitOps)},
true,
},
{
"team admin, belongs to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
false,
},
{
"team maintainer, belongs to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
false,
},
{
"team observer, belongs to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
false,
},
{
"team observer+, belongs to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserverPlus}}},
false,
},
{
"team gitops, belongs to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleGitOps}}},
true,
},
{
"team admin, DOES NOT belong to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleAdmin}}},
true,
},
{
"team observer, DOES NOT belong to team",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleObserver}}},
true,
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: tt.user})
_, err := svc.GetHostManagedAccountPassword(ctx, 1)
checkAuthErr(t, tt.shouldFail, err)
})
}
}
+39 -2
View File
@@ -203,7 +203,7 @@ func (svc *Service) updateAppConfigMDMAppleSetup(ctx context.Context, payload fl
return err
}
var didUpdate, didUpdateMacOSEndUserAuth bool
var didUpdate, didUpdateMacOSEndUserAuth, didUpdateManagedLocalAccount bool
if payload.EnableEndUserAuthentication != nil {
if ac.MDM.MacOSSetup.EnableEndUserAuthentication != *payload.EnableEndUserAuthentication {
ac.MDM.MacOSSetup.EnableEndUserAuthentication = *payload.EnableEndUserAuthentication
@@ -272,6 +272,24 @@ func (svc *Service) updateAppConfigMDMAppleSetup(ctx context.Context, payload fl
}
}
if payload.EnableManagedLocalAccount != nil {
if ac.MDM.MacOSSetup.EnableManagedLocalAccount == nil || *ac.MDM.MacOSSetup.EnableManagedLocalAccount != *payload.EnableManagedLocalAccount {
ac.MDM.MacOSSetup.EnableManagedLocalAccount = payload.EnableManagedLocalAccount
didUpdateManagedLocalAccount = true
didUpdate = true
}
}
if payload.EndUserLocalAccountType != nil {
if *payload.EndUserLocalAccountType != "admin" {
return fleet.NewInvalidArgumentError("end_user_local_account_type", `only "admin" is supported`)
}
if ac.MDM.MacOSSetup.EndUserLocalAccountType == nil || *ac.MDM.MacOSSetup.EndUserLocalAccountType != *payload.EndUserLocalAccountType {
ac.MDM.MacOSSetup.EndUserLocalAccountType = payload.EndUserLocalAccountType
didUpdate = true
}
}
if didUpdate {
if err := svc.ds.SaveAppConfig(ctx, ac); err != nil {
return err
@@ -281,6 +299,11 @@ func (svc *Service) updateAppConfigMDMAppleSetup(ctx context.Context, payload fl
return err
}
}
if didUpdateManagedLocalAccount {
if err := svc.updateMacOSSetupEnableManagedLocalAccount(ctx, *ac.MDM.MacOSSetup.EnableManagedLocalAccount, nil, nil); err != nil {
return err
}
}
}
return nil
}
@@ -302,6 +325,19 @@ func (svc *Service) updateMacOSSetupEnableEndUserAuth(ctx context.Context, enabl
return nil
}
func (svc *Service) updateMacOSSetupEnableManagedLocalAccount(ctx context.Context, enable bool, teamID *uint, teamName *string) error {
var act fleet.ActivityDetails
if enable {
act = fleet.ActivityTypeEnabledManagedLocalAccount{TeamID: teamID, TeamName: teamName}
} else {
act = fleet.ActivityTypeDisabledManagedLocalAccount{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 managed local account 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
@@ -312,7 +348,8 @@ func (svc *Service) validateMDMAppleSetupPayload(ctx context.Context, payload fl
}
// 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 {
if (payload.RequireAllSoftware != nil || payload.EnableReleaseDeviceManually != nil || payload.ManualAgentInstall != nil ||
payload.EnableManagedLocalAccount != nil || payload.EndUserLocalAccountType != nil) && !ac.MDM.EnabledAndConfigured {
return fleet.ErrMDMNotConfigured
}
+24 -1
View File
@@ -2012,7 +2012,7 @@ func (svc *Service) updateTeamMDMDiskEncryption(ctx context.Context, tm *fleet.T
}
func (svc *Service) updateTeamMDMAppleSetup(ctx context.Context, tm *fleet.Team, payload fleet.MDMAppleSetupPayload) error {
var didUpdate, didUpdateMacOSEndUserAuth bool
var didUpdate, didUpdateMacOSEndUserAuth, didUpdateManagedLocalAccount bool
if payload.EnableEndUserAuthentication != nil {
if tm.Config.MDM.MacOSSetup.EnableEndUserAuthentication != *payload.EnableEndUserAuthentication {
@@ -2082,6 +2082,24 @@ func (svc *Service) updateTeamMDMAppleSetup(ctx context.Context, tm *fleet.Team,
}
}
if payload.EnableManagedLocalAccount != nil {
if tm.Config.MDM.MacOSSetup.EnableManagedLocalAccount == nil || *tm.Config.MDM.MacOSSetup.EnableManagedLocalAccount != *payload.EnableManagedLocalAccount {
tm.Config.MDM.MacOSSetup.EnableManagedLocalAccount = payload.EnableManagedLocalAccount
didUpdateManagedLocalAccount = true
didUpdate = true
}
}
if payload.EndUserLocalAccountType != nil {
if *payload.EndUserLocalAccountType != "admin" {
return fleet.NewInvalidArgumentError("end_user_local_account_type", `only "admin" is supported`)
}
if tm.Config.MDM.MacOSSetup.EndUserLocalAccountType == nil || *tm.Config.MDM.MacOSSetup.EndUserLocalAccountType != *payload.EndUserLocalAccountType {
tm.Config.MDM.MacOSSetup.EndUserLocalAccountType = payload.EndUserLocalAccountType
didUpdate = true
}
}
if didUpdate {
if _, err := svc.ds.SaveTeam(ctx, tm); err != nil {
return err
@@ -2091,6 +2109,11 @@ func (svc *Service) updateTeamMDMAppleSetup(ctx context.Context, tm *fleet.Team,
return err
}
}
if didUpdateManagedLocalAccount {
if err := svc.updateMacOSSetupEnableManagedLocalAccount(ctx, *tm.Config.MDM.MacOSSetup.EnableManagedLocalAccount, &tm.ID, &tm.Name); err != nil {
return err
}
}
}
return nil
}