Update authorization for MDM profiles to be platform-independent (#15023)

This commit is contained in:
Martin Angers 2023-11-08 11:36:57 -05:00 committed by GitHub
parent c602a1a2d3
commit c7d5e5e618
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 224 additions and 31 deletions

View file

@ -789,7 +789,7 @@ func (svc *Service) MDMApplePreassignProfile(ctx context.Context, payload fleet.
// 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.MDMAppleConfigProfile{}, fleet.ActionWrite); err != nil {
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 {
@ -805,7 +805,7 @@ func (svc *Service) MDMAppleMatchPreassignment(ctx context.Context, externalHost
// 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.MDMAppleConfigProfile{}, fleet.ActionWrite); err != nil {
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 {
@ -976,9 +976,7 @@ func teamNameFromPreassignGroups(groups []string) string {
}
func (svc *Service) GetMDMDiskEncryptionSummary(ctx context.Context, teamID *uint) (*fleet.MDMDiskEncryptionSummary, error) {
// TODO: Consider adding a new generic OSSetting type or Windows-specific type for authz checks
// like this.
if err := svc.authz.Authorize(ctx, fleet.MDMAppleConfigProfile{TeamID: teamID}, fleet.ActionRead); err != nil {
if err := svc.authz.Authorize(ctx, fleet.MDMConfigProfileAuthz{TeamID: teamID}, fleet.ActionRead); err != nil {
return nil, ctxerr.Wrap(ctx, err)
}
var macOS fleet.MDMAppleFileVaultSummary

View file

@ -595,37 +595,37 @@ allow {
}
##
# Apple MDM
# Apple and Windows MDM
##
# Global admins and maintainers can read and write Apple MDM config profiles.
# Global admins and maintainers can read and write MDM config profiles.
allow {
object.type == "mdm_apple_config_profile"
object.type == "mdm_config_profile"
subject.global_role == [admin, maintainer][_]
action == [read, write][_]
}
# Global gitops can write Apple MDM config profiles.
# Global gitops can write MDM config profiles.
allow {
object.type == "mdm_apple_config_profile"
object.type == "mdm_config_profile"
subject.global_role == gitops
action == write
}
# Team admins and maintainers can read and write Apple MDM config profiles on their teams.
# Team admins and maintainers can read and write MDM config profiles on their teams.
allow {
not is_null(object.team_id)
object.team_id != 0
object.type == "mdm_apple_config_profile"
object.type == "mdm_config_profile"
team_role(subject, object.team_id) == [admin, maintainer][_]
action == [read, write][_]
}
# Team gitops can write Apple MDM config profiles on their teams.
# Team gitops can write MDM config profiles on their teams.
allow {
not is_null(object.team_id)
object.team_id != 0
object.type == "mdm_apple_config_profile"
object.type == "mdm_config_profile"
team_role(subject, object.team_id) == gitops
action == write
}

View file

@ -1287,11 +1287,11 @@ func TestAuthorizeTeamPolicy(t *testing.T) {
})
}
func TestAuthorizeMDMAppleConfigProfile(t *testing.T) {
func TestAuthorizeMDMConfigProfile(t *testing.T) {
t.Parallel()
globalProfile := &fleet.MDMAppleConfigProfile{}
team1Profile := &fleet.MDMAppleConfigProfile{
globalProfile := &fleet.MDMConfigProfileAuthz{}
team1Profile := &fleet.MDMConfigProfileAuthz{
TeamID: ptr.Uint(1),
}
runTestCases(t, []authTestCase{

View file

@ -576,3 +576,42 @@ WHERE
Detail: dest.Detail,
}, nil
}
func (ds *Datastore) GetMDMWindowsProfile(ctx context.Context, profileUUID string) (*fleet.MDMWindowsConfigProfile, error) {
stmt := `
SELECT
profile_uuid,
team_id,
name,
syncml,
created_at,
updated_at
FROM
mdm_windows_configuration_profiles
WHERE
profile_uuid=?`
var res fleet.MDMWindowsConfigProfile
err := sqlx.GetContext(ctx, ds.reader(ctx), &res, stmt, profileUUID)
if err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("MDMWindowsProfile").WithName(profileUUID))
}
return nil, ctxerr.Wrap(ctx, err, "get mdm windows config profile")
}
return &res, nil
}
func (ds *Datastore) DeleteMDMWindowsProfile(ctx context.Context, profileUUID string) error {
res, err := ds.writer(ctx).ExecContext(ctx, `DELETE FROM mdm_windows_configuration_profiles WHERE profile_uuid=?`, profileUUID)
if err != nil {
return ctxerr.Wrap(ctx, err)
}
deleted, _ := res.RowsAffected() // cannot fail for mysql
if deleted != 1 {
return ctxerr.Wrap(ctx, notFound("MDMWindowsProfile").WithName(profileUUID))
}
return nil
}

View file

@ -208,11 +208,6 @@ func NewMDMAppleConfigProfile(raw []byte, teamID *uint) (*MDMAppleConfigProfile,
}, nil
}
// AuthzType implements authz.AuthzTyper.
func (cp MDMAppleConfigProfile) AuthzType() string {
return "mdm_apple_config_profile"
}
func (cp MDMAppleConfigProfile) ValidateUserProvided() error {
if _, ok := mobileconfig.FleetPayloadIdentifiers()[cp.Identifier]; ok {
return fmt.Errorf("payload identifier %s is not allowed", cp.Identifier)

View file

@ -1068,6 +1068,14 @@ type Datastore interface {
// UpdateMDMWindowsEnrollmentsHostUUID updates the host UUID for a given MDM device ID.
UpdateMDMWindowsEnrollmentsHostUUID(ctx context.Context, hostUUID string, mdmDeviceID string) error
// GetMDMWindowsProfile returns the Windows MDM profile corresponding to the
// specified profile uuid.
GetMDMWindowsProfile(ctx context.Context, profileUUID string) (*MDMWindowsConfigProfile, error)
// DeleteMDMWindowsProfile deletes the Windows MDM profile corresponding to
// the specified profile uuid.
DeleteMDMWindowsProfile(ctx context.Context, profileUUID string) error
///////////////////////////////////////////////////////////////////////////////
// MDM Commands

View file

@ -294,3 +294,14 @@ const (
MDMOperationTypeInstall MDMOperationType = "install"
MDMOperationTypeRemove MDMOperationType = "remove"
)
// MDMConfigProfileAuthz is used to check user authorization to read/write an
// MDM configuration profile.
type MDMConfigProfileAuthz struct {
TeamID *uint `json:"team_id"` // required for authorization by team
}
// AuthzType implements authz.AuthzTyper.
func (m MDMConfigProfileAuthz) AuthzType() string {
return "mdm_config_profile"
}

View file

@ -810,6 +810,9 @@ type Service interface {
// Set or update the disk encryption key for a host.
SetOrUpdateDiskEncryptionKey(ctx context.Context, encryptionKey, clientError string) error
// DeleteMDMWindowsProfile deletes the specified windows profile.
DeleteMDMWindowsProfile(ctx context.Context, profileUUID string) error
///////////////////////////////////////////////////////////////////////////////
// Common MDM

View file

@ -1,5 +1,9 @@
package fleet
import (
"time"
)
// MDMWindowsBitLockerSummary reports the number of Windows hosts being managed by Fleet with
// BitLocker. Each host may be counted in only one of six mutually-exclusive categories:
// Verified, Verifying, ActionRequired, Enforcing, Failed, RemovingEnforcement.
@ -14,3 +18,13 @@ type MDMWindowsBitLockerSummary struct {
Failed uint `json:"failed" db:"failed"`
RemovingEnforcement uint `json:"removing_enforcement" db:"removing_enforcement"`
}
// MDMWindowsConfigProfile represents a Windows MDM profile in Fleet.
type MDMWindowsConfigProfile struct {
ProfileUUID string `db:"profile_uuid" json:"profile_uuid"`
TeamID *uint `db:"team_id" json:"team_id"`
Name string `db:"name" json:"name"`
SyncML []byte `db:"syncml" json:"-"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}

View file

@ -698,6 +698,10 @@ type GetMDMWindowsCommandResultsFunc func(ctx context.Context, commandUUID strin
type UpdateMDMWindowsEnrollmentsHostUUIDFunc func(ctx context.Context, hostUUID string, mdmDeviceID string) error
type GetMDMWindowsProfileFunc func(ctx context.Context, profileUUID string) (*fleet.MDMWindowsConfigProfile, error)
type DeleteMDMWindowsProfileFunc func(ctx context.Context, profileUUID string) error
type GetMDMCommandPlatformFunc func(ctx context.Context, commandUUID string) (string, error)
type ListMDMCommandsFunc func(ctx context.Context, tmFilter fleet.TeamFilter, listOpts *fleet.MDMCommandListOptions) ([]*fleet.MDMCommand, error)
@ -1749,6 +1753,12 @@ type DataStore struct {
UpdateMDMWindowsEnrollmentsHostUUIDFunc UpdateMDMWindowsEnrollmentsHostUUIDFunc
UpdateMDMWindowsEnrollmentsHostUUIDFuncInvoked bool
GetMDMWindowsProfileFunc GetMDMWindowsProfileFunc
GetMDMWindowsProfileFuncInvoked bool
DeleteMDMWindowsProfileFunc DeleteMDMWindowsProfileFunc
DeleteMDMWindowsProfileFuncInvoked bool
GetMDMCommandPlatformFunc GetMDMCommandPlatformFunc
GetMDMCommandPlatformFuncInvoked bool
@ -4177,6 +4187,20 @@ func (s *DataStore) UpdateMDMWindowsEnrollmentsHostUUID(ctx context.Context, hos
return s.UpdateMDMWindowsEnrollmentsHostUUIDFunc(ctx, hostUUID, mdmDeviceID)
}
func (s *DataStore) GetMDMWindowsProfile(ctx context.Context, profileUUID string) (*fleet.MDMWindowsConfigProfile, error) {
s.mu.Lock()
s.GetMDMWindowsProfileFuncInvoked = true
s.mu.Unlock()
return s.GetMDMWindowsProfileFunc(ctx, profileUUID)
}
func (s *DataStore) DeleteMDMWindowsProfile(ctx context.Context, profileUUID string) error {
s.mu.Lock()
s.DeleteMDMWindowsProfileFuncInvoked = true
s.mu.Unlock()
return s.DeleteMDMWindowsProfileFunc(ctx, profileUUID)
}
func (s *DataStore) GetMDMCommandPlatform(ctx context.Context, commandUUID string) (string, error) {
s.mu.Lock()
s.GetMDMCommandPlatformFuncInvoked = true

View file

@ -307,7 +307,7 @@ func newMDMAppleConfigProfileEndpoint(ctx context.Context, request interface{},
}
func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r io.Reader, size int64) (*fleet.MDMAppleConfigProfile, error) {
if err := svc.authz.Authorize(ctx, &fleet.MDMAppleConfigProfile{TeamID: &teamID}, fleet.ActionWrite); err != nil {
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: &teamID}, fleet.ActionWrite); err != nil {
return nil, ctxerr.Wrap(ctx, err)
}
var teamName string
@ -386,7 +386,7 @@ func listMDMAppleConfigProfilesEndpoint(ctx context.Context, request interface{}
}
func (svc *Service) ListMDMAppleConfigProfiles(ctx context.Context, teamID uint) ([]*fleet.MDMAppleConfigProfile, error) {
if err := svc.authz.Authorize(ctx, &fleet.MDMAppleConfigProfile{TeamID: &teamID}, fleet.ActionRead); err != nil {
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: &teamID}, fleet.ActionRead); err != nil {
return nil, ctxerr.Wrap(ctx, err)
}
@ -462,7 +462,7 @@ func (svc *Service) GetMDMAppleConfigProfile(ctx context.Context, profileID uint
}
// now we can do a specific authz check based on team id of profile before we return the profile
if err := svc.authz.Authorize(ctx, cp, fleet.ActionRead); err != nil {
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: cp.TeamID}, fleet.ActionRead); err != nil {
return nil, err
}
@ -511,7 +511,7 @@ func (svc *Service) DeleteMDMAppleConfigProfile(ctx context.Context, profileID u
}
// now we can do a specific authz check based on team id of profile before we delete the profile
if err := svc.authz.Authorize(ctx, cp, fleet.ActionWrite); err != nil {
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: cp.TeamID}, fleet.ActionWrite); err != nil {
return ctxerr.Wrap(ctx, err)
}
@ -572,7 +572,7 @@ func getMDMAppleProfilesSummaryEndpoint(ctx context.Context, request interface{}
}
func (svc *Service) GetMDMAppleProfilesSummary(ctx context.Context, teamID *uint) (*fleet.MDMAppleConfigProfilesSummary, error) {
if err := svc.authz.Authorize(ctx, fleet.MDMAppleConfigProfile{TeamID: teamID}, fleet.ActionRead); err != nil {
if err := svc.authz.Authorize(ctx, fleet.MDMConfigProfileAuthz{TeamID: teamID}, fleet.ActionRead); err != nil {
return nil, ctxerr.Wrap(ctx, err)
}
@ -609,7 +609,7 @@ func getMdmAppleFileVaultSummaryEndpoint(ctx context.Context, request interface{
}
func (svc *Service) GetMDMAppleFileVaultSummary(ctx context.Context, teamID *uint) (*fleet.MDMAppleFileVaultSummary, error) {
if err := svc.authz.Authorize(ctx, fleet.MDMAppleConfigProfile{TeamID: teamID}, fleet.ActionRead); err != nil {
if err := svc.authz.Authorize(ctx, fleet.MDMConfigProfileAuthz{TeamID: teamID}, fleet.ActionRead); err != nil {
return nil, ctxerr.Wrap(ctx, err)
}
@ -1417,7 +1417,7 @@ func (svc *Service) BatchSetMDMAppleProfiles(ctx context.Context, tmID *uint, tm
}
}
if err := svc.authz.Authorize(ctx, &fleet.MDMAppleConfigProfile{TeamID: tmID}, fleet.ActionWrite); err != nil {
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: tmID}, fleet.ActionWrite); err != nil {
return ctxerr.Wrap(ctx, err)
}

View file

@ -475,12 +475,16 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
// platform-agnostic POST /mdm/commands/commands. It is still supported
// indefinitely for backwards compatibility.
mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/commands", listMDMAppleCommandsEndpoint, listMDMAppleCommandsRequest{})
// Deprecated: GET and DELETE /mdm/apple/profiles/{profile_id} are now
// deprecated, replaced by the platform-agnostic GET/DELETE
// /mdm/profiles/{profile_id}. It is still supported indefinitely for
// backwards compatibility.
mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/profiles/{profile_id:[0-9]+}", getMDMAppleConfigProfileEndpoint, getMDMAppleConfigProfileRequest{})
mdmAppleMW.DELETE("/api/_version_/fleet/mdm/apple/profiles/{profile_id:[0-9]+}", deleteMDMAppleConfigProfileEndpoint, deleteMDMAppleConfigProfileRequest{})
mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/filevault/summary", getMdmAppleFileVaultSummaryEndpoint, getMDMAppleFileVaultSummaryRequest{})
mdmAppleMW.POST("/api/_version_/fleet/mdm/apple/profiles", newMDMAppleConfigProfileEndpoint, newMDMAppleConfigProfileRequest{})
mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/profiles", listMDMAppleConfigProfilesEndpoint, listMDMAppleConfigProfilesRequest{})
mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/profiles/{profile_id:[0-9]+}", getMDMAppleConfigProfileEndpoint, getMDMAppleConfigProfileRequest{})
mdmAppleMW.DELETE("/api/_version_/fleet/mdm/apple/profiles/{profile_id:[0-9]+}", deleteMDMAppleConfigProfileEndpoint, deleteMDMAppleConfigProfileRequest{})
mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/profiles/summary", getMDMAppleProfilesSummaryEndpoint, getMDMAppleProfilesSummaryRequest{})
mdmAppleMW.POST("/api/_version_/fleet/mdm/apple/enrollment_profile", createMDMAppleSetupAssistantEndpoint, createMDMAppleSetupAssistantRequest{})
mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/enrollment_profile", getMDMAppleSetupAssistantEndpoint, getMDMAppleSetupAssistantRequest{})
@ -525,6 +529,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
mdmAnyMW.GET("/api/_version_/fleet/mdm/commands", listMDMCommandsEndpoint, listMDMCommandsRequest{})
mdmAnyMW.GET("/api/_version_/fleet/mdm/disk_encryption/summary", getMDMDiskEncryptionSummaryEndpoint, getMDMDiskEncryptionSummaryRequest{})
mdmAnyMW.GET("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/encryption_key", getHostEncryptionKey, getHostEncryptionKeyRequest{})
//mdmAnyMW.DELETE("/api/_version_/fleet/mdm/profiles/{profile_id_or_uuid}", deleteMDMProfileEndpoint, deleteMDMProfileRequest{})
// the following set of mdm endpoints must always be accessible (even
// if MDM is not configured) as it bootstraps the setup of MDM

View file

@ -935,3 +935,99 @@ func (svc *Service) authorizeAllHostsTeams(ctx context.Context, hostUUIDs []stri
}
return hosts, nil
}
////////////////////////////////////////////////////////////////////////////////
// DELETE /mdm/profiles/{id_or_uuid}
////////////////////////////////////////////////////////////////////////////////
type deleteMDMProfileRequest struct {
ProfileIDOrUUID string `url:"profile_id_or_uuid"`
}
type deleteMDMProfileResponse struct {
Err error `json:"error,omitempty"`
}
func (r deleteMDMProfileResponse) error() error { return r.Err }
var _ = deleteMDMProfileEndpoint // Temporary, to ensure it is used.
func deleteMDMProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*deleteMDMProfileRequest)
appleID, isApple := isAppleProfileID(req.ProfileIDOrUUID)
var err error
if isApple {
err = svc.DeleteMDMAppleConfigProfile(ctx, appleID)
} else {
err = svc.DeleteMDMWindowsProfile(ctx, req.ProfileIDOrUUID)
}
return &deleteMDMProfileResponse{Err: err}, nil
}
func (svc *Service) DeleteMDMWindowsProfile(ctx context.Context, profileUUID string) error {
// first we perform a perform basic authz check
if err := svc.authz.Authorize(ctx, &fleet.Team{}, fleet.ActionRead); err != nil {
return ctxerr.Wrap(ctx, err)
}
prof, err := svc.ds.GetMDMWindowsProfile(ctx, profileUUID)
if err != nil {
return ctxerr.Wrap(ctx, err)
}
var teamName string
teamID := *prof.TeamID
if teamID >= 1 {
tm, err := svc.EnterpriseOverrides.TeamByIDOrName(ctx, &teamID, nil)
if err != nil {
return ctxerr.Wrap(ctx, err)
}
teamName = tm.Name
}
// now we can do a specific authz check based on team id of profile before we delete the profile
if err := svc.authz.Authorize(ctx, &fleet.MDMConfigProfileAuthz{TeamID: prof.TeamID}, fleet.ActionWrite); err != nil {
return ctxerr.Wrap(ctx, err)
}
// TODO: prevent deleting profiles that are managed by Fleet
//if _, ok := mobileconfig.FleetPayloadIdentifiers()[cp.Identifier]; ok {
// return &fleet.BadRequestError{
// Message: "profiles managed by Fleet can't be deleted using this endpoint.",
// InternalErr: fmt.Errorf("deleting profile %s for team %s not allowed because it's managed by Fleet", cp.Identifier, teamName),
// }
//}
if err := svc.ds.DeleteMDMWindowsProfile(ctx, profileUUID); err != nil {
return ctxerr.Wrap(ctx, err)
}
//// cannot use the profile ID as it is now deleted
//if err := svc.ds.BulkSetPendingMDMAppleHostProfiles(ctx, nil, []uint{teamID}, nil, nil); err != nil {
// return ctxerr.Wrap(ctx, err, "bulk set pending host profiles")
//}
_ = teamName
//if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeDeletedMacosProfile{
// TeamID: &teamID,
// TeamName: &teamName,
// ProfileName: cp.Name,
// ProfileIdentifier: cp.Identifier,
//}); err != nil {
// return ctxerr.Wrap(ctx, err, "logging activity for delete mdm apple config profile")
//}
return nil
}
// returns the numeric Apple profile ID and true if it is an Apple identifier,
// or 0 and false otherwise.
func isAppleProfileID(profileIDOrUUID string) (uint, bool) {
// parsing as 32 bits as that's the maximum value of the DB column (and can
// be safely converted to uint).
id, err := strconv.ParseUint(profileIDOrUUID, 10, 32)
if err != nil {
return 0, false
}
return uint(id), true
}