Fix bug preventing gitops role from fleetctl applying macos setup assistant (and bootstrap package) (#12193)

This commit is contained in:
Martin Angers
2023-06-07 13:29:36 -04:00
committed by GitHub
parent 9b876de99c
commit 68ddaafac0
20 changed files with 442 additions and 126 deletions
@@ -0,0 +1,2 @@
* Fixed an issue preventing a user with the `gitops` role to apply some MDM settings via `fleetctl apply` (the `macos_setup_assistant` and `bootstrap_package` settings)
* Added a response payload to the `POST /api/latest/fleet/spec/teams` contributor API endpoint, it now returns an object with a `team_ids_by_name` key which maps team names with their corresponding id.
+173 -43
View File
@@ -797,7 +797,6 @@ func TestApplyAsGitOps(t *testing.T) {
return nil
}
// Apply global config.
currentAppConfig := &fleet.AppConfig{
OrgInfo: fleet.OrgInfo{
OrgName: "Fleet",
@@ -817,6 +816,52 @@ func TestApplyAsGitOps(t *testing.T) {
currentAppConfig = config
return nil
}
savedTeam := &fleet.Team{ID: 123}
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
if name == "Team1" {
return savedTeam, nil
}
return nil, errors.New("unexpected team name!")
}
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
savedTeam = team
return team, nil
}
ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
return savedTeam, nil
}
var teamEnrollSecrets []*fleet.EnrollSecret
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
if teamID == nil || *teamID != 123 {
return fmt.Errorf("unexpected data: %+v", teamID)
}
teamEnrollSecrets = secrets
return nil
}
ds.BatchSetMDMAppleProfilesFunc = func(ctx context.Context, teamID *uint, profiles []*fleet.MDMAppleConfigProfile) error {
return nil
}
ds.BulkSetPendingMDMAppleHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs, profileIDs []uint, hostUUIDs []string) error {
return nil
}
ds.GetMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleSetupAssistant, error) {
return nil, &notFoundError{}
}
ds.SetOrUpdateMDMAppleSetupAssistantFunc = func(ctx context.Context, asst *fleet.MDMAppleSetupAssistant) (*fleet.MDMAppleSetupAssistant, error) {
return asst, nil
}
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
return job, nil
}
ds.GetMDMAppleBootstrapPackageMetaFunc = func(ctx context.Context, teamID uint) (*fleet.MDMAppleBootstrapPackage, error) {
return nil, &notFoundError{}
}
ds.InsertMDMAppleBootstrapPackageFunc = func(ctx context.Context, bp *fleet.MDMAppleBootstrapPackage) error {
return nil
}
// Apply global config.
name := writeTmpYml(t, `---
apiVersion: v1
kind: config
@@ -843,38 +888,78 @@ spec:
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, currentAppConfig.Features.EnableHostUsers)
// Apply team config.
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
if name == "Team1" {
return &fleet.Team{ID: 123}, nil
}
return nil, errors.New("unexpected team name!")
}
var savedTeam *fleet.Team
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
savedTeam = team
return team, nil
}
var teamEnrollSecrets []*fleet.EnrollSecret
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
if teamID == nil || *teamID != 123 {
return fmt.Errorf("unexpected data: %+v", teamID)
}
teamEnrollSecrets = secrets
return nil
}
ds.BatchSetMDMAppleProfilesFunc = func(ctx context.Context, teamID *uint, profiles []*fleet.MDMAppleConfigProfile) error {
return nil
}
ds.BulkSetPendingMDMAppleHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs, profileIDs []uint, hostUUIDs []string) error {
return nil
}
mobileConfig := mobileconfigForTest("foo", "bar")
mobileConfigPath := filepath.Join(t.TempDir(), "foo.mobileconfig")
err = os.WriteFile(mobileConfigPath, mobileConfig, 0o644)
require.NoError(t, err)
emptySetupAsst := writeTmpJSON(t, map[string]any{})
// Apply global config with custom setting and macos setup assistant.
name = writeTmpYml(t, fmt.Sprintf(`---
apiVersion: v1
kind: config
spec:
mdm:
macos_updates:
minimum_version: 10.10.10
deadline: 2020-02-02
macos_settings:
custom_settings:
- %s
macos_setup:
macos_setup_assistant: %s
`, mobileConfigPath, emptySetupAsst))
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
// features left untouched, not provided
assert.True(t, currentAppConfig.Features.EnableHostUsers)
assert.Equal(t, fleet.MDM{
EnabledAndConfigured: true,
MacOSSetup: fleet.MacOSSetup{
MacOSSetupAssistant: optjson.SetString(emptySetupAsst),
},
MacOSUpdates: fleet.MacOSUpdates{
MinimumVersion: optjson.SetString("10.10.10"),
Deadline: optjson.SetString("2020-02-02"),
},
MacOSSettings: fleet.MacOSSettings{
CustomSettings: []string{mobileConfigPath},
},
}, currentAppConfig.MDM)
// start a server to return the bootstrap package
srv, _ := serveMDMBootstrapPackage(t, "../../server/service/testdata/bootstrap-packages/signed.pkg", "signed.pkg")
// Apply global config with bootstrap package
bootstrapURL := srv.URL + "/signed.pkg"
name = writeTmpYml(t, fmt.Sprintf(`---
apiVersion: v1
kind: config
spec:
mdm:
macos_setup:
bootstrap_package: %s
`, bootstrapURL))
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
// features left untouched, not provided
assert.True(t, currentAppConfig.Features.EnableHostUsers)
// MDM settings left untouched except for the bootstrap package
assert.Equal(t, fleet.MDM{
EnabledAndConfigured: true,
MacOSSetup: fleet.MacOSSetup{
MacOSSetupAssistant: optjson.SetString(emptySetupAsst),
BootstrapPackage: optjson.SetString(bootstrapURL),
},
MacOSUpdates: fleet.MacOSUpdates{
MinimumVersion: optjson.SetString("10.10.10"),
Deadline: optjson.SetString("2020-02-02"),
},
MacOSSettings: fleet.MacOSSettings{
CustomSettings: []string{mobileConfigPath},
},
}, currentAppConfig.MDM)
// Apply team config.
name = writeTmpYml(t, fmt.Sprintf(`
apiVersion: v1
kind: team
@@ -913,6 +998,64 @@ spec:
assert.True(t, ds.ApplyEnrollSecretsFuncInvoked)
assert.True(t, ds.BatchSetMDMAppleProfilesFuncInvoked)
// add macos setup assistant to team
name = writeTmpYml(t, fmt.Sprintf(`
apiVersion: v1
kind: team
spec:
team:
name: Team1
mdm:
macos_setup:
macos_setup_assistant: %s
`, emptySetupAsst))
require.Equal(t, "[+] applied 1 teams\n", runAppForTest(t, []string{"apply", "-f", name}))
require.True(t, ds.GetMDMAppleSetupAssistantFuncInvoked)
require.True(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
require.True(t, ds.NewJobFuncInvoked)
// all left untouched, only setup assistant added
assert.Equal(t, fleet.TeamMDM{
MacOSSettings: fleet.MacOSSettings{
CustomSettings: []string{mobileConfigPath},
EnableDiskEncryption: false,
},
MacOSUpdates: fleet.MacOSUpdates{
MinimumVersion: optjson.SetString("10.10.10"),
Deadline: optjson.SetString("1992-03-01"),
},
MacOSSetup: fleet.MacOSSetup{
MacOSSetupAssistant: optjson.SetString(emptySetupAsst),
},
}, savedTeam.Config.MDM)
// add bootstrap package to team
name = writeTmpYml(t, fmt.Sprintf(`
apiVersion: v1
kind: team
spec:
team:
name: Team1
mdm:
macos_setup:
bootstrap_package: %s
`, bootstrapURL))
require.Equal(t, "[+] applied 1 teams\n", runAppForTest(t, []string{"apply", "-f", name}))
// all left untouched, only bootstrap package added
assert.Equal(t, fleet.TeamMDM{
MacOSSettings: fleet.MacOSSettings{
CustomSettings: []string{mobileConfigPath},
EnableDiskEncryption: false,
},
MacOSUpdates: fleet.MacOSUpdates{
MinimumVersion: optjson.SetString("10.10.10"),
Deadline: optjson.SetString("1992-03-01"),
},
MacOSSetup: fleet.MacOSSetup{
MacOSSetupAssistant: optjson.SetString(emptySetupAsst),
BootstrapPackage: optjson.SetString(bootstrapURL),
},
}, savedTeam.Config.MDM)
// Apply policies.
var appliedPolicySpecs []*fleet.PolicySpec
ds.ApplyPolicySpecsFunc = func(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error {
@@ -1666,23 +1809,10 @@ spec:
for _, c := range cases {
t.Run(c.pkgName, func(t *testing.T) {
pkgBytes, err := os.ReadFile(filepath.Join("../../server/service/testdata/bootstrap-packages", c.pkgName))
require.NoError(t, err)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", strconv.Itoa(len(pkgBytes)))
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment;filename="%s"`, c.pkgName))
if n, err := w.Write(pkgBytes); err != nil {
require.NoError(t, err)
require.Equal(t, len(pkgBytes), n)
}
}))
defer srv.Close()
srv, pkgLen := serveMDMBootstrapPackage(t, filepath.Join("../../server/service/testdata/bootstrap-packages", c.pkgName), c.pkgName)
ds := setupServer(t, true)
ds.InsertMDMAppleBootstrapPackageFunc = func(ctx context.Context, bp *fleet.MDMAppleBootstrapPackage) error {
require.Equal(t, len(bp.Bytes), len(pkgBytes))
require.Equal(t, len(bp.Bytes), pkgLen)
return nil
}
ds.GetMDMAppleBootstrapPackageMetaFunc = func(ctx context.Context, teamID uint) (*fleet.MDMAppleBootstrapPackage, error) {
+20
View File
@@ -3,8 +3,11 @@ package main
import (
"bytes"
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
"strconv"
"testing"
"time"
@@ -83,3 +86,20 @@ func runAppNoChecks(args []string) (*bytes.Buffer, error) {
}
func noopExitErrHandler(c *cli.Context, err error) {}
func serveMDMBootstrapPackage(t *testing.T, pkgPath, pkgName string) (*httptest.Server, int) {
pkgBytes, err := os.ReadFile(pkgPath)
require.NoError(t, err)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", strconv.Itoa(len(pkgBytes)))
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment;filename="%s"`, pkgName))
if n, err := w.Write(pkgBytes); err != nil {
require.NoError(t, err)
require.Equal(t, len(pkgBytes), n)
}
}))
t.Cleanup(srv.Close)
return srv, len(pkgBytes)
}
@@ -1304,6 +1304,14 @@ If the `name` is not already associated with an existing team, this API route cr
`Status: 200`
```json
{
"team_ids_by_name": {
"Client Platform Engineering": 123
}
}
```
### Apply labels
Adds the supplied labels to Fleet. Each label requires the `name`, and `label_membership_type` properties.
+8 -4
View File
@@ -80,8 +80,10 @@ GitOps is an API-only and write-only role that can be used on CI/CD pipelines.
| Edit [MDM settings for teams](https://fleetdm.com/docs/using-fleet/mdm-macos-settings) | | | | ✅ | ✅ |
| Upload an EULA file for MDM automatic enrollment\* | | | | ✅ | |
| View/download MDM macOS setup assistant\* | | | ✅ | ✅ | |
| Edit/upload MDM macOS setup assistant\* | | | ✅ | ✅ | |
| Enable/disable MDM macOS setup end user authentication\* | | | ✅ | ✅ | |
| Edit/upload MDM macOS setup assistant\* | | | ✅ | ✅ | |
| View metadata of MDM macOS bootstrap packages\* | | | ✅ | ✅ | |
| Edit/upload MDM macOS bootstrap packages\* | | | ✅ | ✅ | ✅ |
| Enable/disable MDM macOS setup end user authentication\* | | | ✅ | ✅ | ✅ |
\* Applies only to Fleet Premium
@@ -139,8 +141,10 @@ Users that are members of multiple teams can be assigned different roles for eac
| View results of MDM commands executed on macOS hosts* | ✅ | ✅ | ✅ | ✅ | |
| Edit [team MDM settings](https://fleetdm.com/docs/using-fleet/mdm-macos-settings) | | | | ✅ | ✅ |
| View/download MDM macOS setup assistant | | | ✅ | ✅ | |
| Edit/upload MDM macOS setup assistant | | | ✅ | ✅ | |
| Enable/disable MDM macOS setup end user authentication | | | ✅ | ✅ | |
| Edit/upload MDM macOS setup assistant | | | ✅ | ✅ | |
| View metadata of MDM macOS bootstrap packages | | | ✅ | ✅ | |
| Edit/upload MDM macOS bootstrap packages | | | ✅ | ✅ | ✅ |
| Enable/disable MDM macOS setup end user authentication | | | ✅ | ✅ | ✅ |
\* Applies only to [Fleet REST API](https://fleetdm.com/docs/using-fleet/rest-api)
+7 -6
View File
@@ -2085,7 +2085,7 @@ Returns the count of all hosts organized by status. `online_count` includes all
"platform": "windows",
"hosts_count": 12044
}
]
}
```
@@ -4191,9 +4191,10 @@ Get information about a bootstrap package that was uploaded to Fleet.
#### Parameters
| Name | Type | In | Description |
| ------- | ------ | --- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| team_id | string | url | **Required** The team id for the package. Zero (0) can be specified to get information about the bootstrap package for hosts that don't belong to a team. |
| Name | Type | In | Description |
| ------- | ------ | --- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| team_id | string | url | **Required** The team id for the package. Zero (0) can be specified to get information about the bootstrap package for hosts that don't belong to a team. |
| for_update | boolean | query | If set to `true`, the authorization will be for a `write` action instead of a `read`. Useful for the write-only `gitops` role when requesting the bootstrap metadata to check if the package needs to be replaced. |
#### Example
@@ -4335,7 +4336,7 @@ _Available in Fleet Premium_
### Upload an EULA file
### Upload an EULA file
_Available in Fleet Premium_
@@ -4374,7 +4375,7 @@ Content-Type: application/octet-stream
`Status: 200`
### Get metadata about an EULA file
### Get metadata about an EULA file
_Available in Fleet Premium_
+6 -2
View File
@@ -320,8 +320,12 @@ func (svc *Service) GetMDMAppleBootstrapPackageBytes(ctx context.Context, token
return pkg, nil
}
func (svc *Service) GetMDMAppleBootstrapPackageMetadata(ctx context.Context, teamID uint) (*fleet.MDMAppleBootstrapPackage, error) {
if err := svc.authz.Authorize(ctx, &fleet.MDMAppleBootstrapPackage{TeamID: teamID}, fleet.ActionRead); err != 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
}
+19 -14
View File
@@ -578,20 +578,20 @@ func (svc *Service) checkAuthorizationForTeams(ctx context.Context, specs []*fle
return nil
}
func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, applyOpts fleet.ApplySpecOptions) error {
func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, applyOpts fleet.ApplySpecOptions) (map[string]uint, error) {
if len(specs) == 0 {
setAuthCheckedOnPreAuthErr(ctx)
// Nothing to do.
return nil
return map[string]uint{}, nil
}
if err := svc.checkAuthorizationForTeams(ctx, specs); err != nil {
return err
return nil, err
}
appConfig, err := svc.ds.AppConfig(ctx)
if err != nil {
return err
return nil, err
}
appConfig.Obfuscate()
@@ -612,11 +612,11 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec,
// OK
case ctxerr.Cause(err) == sql.ErrNoRows:
if spec.Name == "" {
return fleet.NewInvalidArgumentError("name", "name may not be empty")
return nil, fleet.NewInvalidArgumentError("name", "name may not be empty")
}
create = true
default:
return err
return nil, err
}
if len(spec.AgentOptions) > 0 && !bytes.Equal(spec.AgentOptions, jsonNull) {
@@ -626,21 +626,21 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec,
level.Info(svc.logger).Log("err", err, "msg", "force-apply team agent options with validation errors")
}
if !applyOpts.Force {
return ctxerr.Wrap(ctx, err, "validate agent options")
return nil, ctxerr.Wrap(ctx, err, "validate agent options")
}
}
}
if len(spec.Secrets) > fleet.MaxEnrollSecretsCount {
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("secrets", "too many secrets"), "validate secrets")
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("secrets", "too many secrets"), "validate secrets")
}
if err := spec.MDM.MacOSUpdates.Validate(); err != nil {
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("macos_updates", err.Error()))
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("macos_updates", err.Error()))
}
if create {
team, err := svc.createTeamFromSpec(ctx, spec, appConfig, secrets, applyOpts.DryRun)
if err != nil {
return ctxerr.Wrap(ctx, err, "creating team from spec")
return nil, ctxerr.Wrap(ctx, err, "creating team from spec")
}
details = append(details, fleet.TeamActivityDetail{
ID: team.ID,
@@ -650,7 +650,7 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec,
}
if err := svc.editTeamFromSpec(ctx, team, spec, appConfig, secrets, applyOpts.DryRun); err != nil {
return ctxerr.Wrap(ctx, err, "editing team from spec")
return nil, ctxerr.Wrap(ctx, err, "editing team from spec")
}
details = append(details, fleet.TeamActivityDetail{
@@ -660,10 +660,15 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec,
}
if applyOpts.DryRun {
return nil
return nil, nil
}
idsByName := make(map[string]uint, len(details))
if len(details) > 0 {
for _, tm := range details {
idsByName[tm.Name] = tm.ID
}
if err := svc.ds.NewActivity(
ctx,
authz.UserFromContext(ctx),
@@ -671,10 +676,10 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec,
Teams: details,
},
); err != nil {
return ctxerr.Wrap(ctx, err, "create activity for team spec")
return nil, ctxerr.Wrap(ctx, err, "create activity for team spec")
}
}
return nil
return idsByName, nil
}
func (svc *Service) createTeamFromSpec(
+16
View File
@@ -705,6 +705,13 @@ allow {
action == [read, write][_]
}
# Global gitops can write bootstrap packages.
allow {
object.type == "mdm_apple_bootstrap_package"
subject.global_role == gitops
action == write
}
# Team admins and maintainers can read and write bootstrap packages on their teams.
allow {
not is_null(object.team_id)
@@ -714,6 +721,15 @@ allow {
action == [read, write][_]
}
# Team gitops can write bootstrap packages on their teams.
allow {
not is_null(object.team_id)
object.team_id != 0
object.type == "mdm_apple_bootstrap_package"
team_role(subject, object.team_id) == gitops
action == write
}
##
# MDM Apple Setup Assistant
##
+90
View File
@@ -1480,6 +1480,96 @@ func TestAuthorizeMDMAppleSetupAssistant(t *testing.T) {
})
}
func TestAuthorizeMDMAppleBootstrapPackage(t *testing.T) {
t.Parallel()
globalSettings := &fleet.MDMAppleBootstrapPackage{}
team1Settings := &fleet.MDMAppleBootstrapPackage{
TeamID: 1,
}
runTestCases(t, []authTestCase{
{user: test.UserNoRoles, object: globalSettings, action: write, allow: false},
{user: test.UserNoRoles, object: globalSettings, action: read, allow: false},
{user: test.UserNoRoles, object: team1Settings, action: write, allow: false},
{user: test.UserNoRoles, object: team1Settings, action: read, allow: false},
{user: test.UserAdmin, object: globalSettings, action: write, allow: true},
{user: test.UserAdmin, object: globalSettings, action: read, allow: true},
{user: test.UserAdmin, object: team1Settings, action: write, allow: true},
{user: test.UserAdmin, object: team1Settings, action: read, allow: true},
{user: test.UserMaintainer, object: globalSettings, action: write, allow: true},
{user: test.UserMaintainer, object: globalSettings, action: read, allow: true},
{user: test.UserMaintainer, object: team1Settings, action: write, allow: true},
{user: test.UserMaintainer, object: team1Settings, action: read, allow: true},
{user: test.UserObserver, object: globalSettings, action: write, allow: false},
{user: test.UserObserver, object: globalSettings, action: read, allow: false},
{user: test.UserObserver, object: team1Settings, action: write, allow: false},
{user: test.UserObserver, object: team1Settings, action: read, allow: false},
{user: test.UserObserverPlus, object: globalSettings, action: write, allow: false},
{user: test.UserObserverPlus, object: globalSettings, action: read, allow: false},
{user: test.UserObserverPlus, object: team1Settings, action: write, allow: false},
{user: test.UserObserverPlus, object: team1Settings, action: read, allow: false},
{user: test.UserGitOps, object: globalSettings, action: write, allow: true},
{user: test.UserGitOps, object: globalSettings, action: read, allow: false},
{user: test.UserGitOps, object: team1Settings, action: write, allow: true},
{user: test.UserGitOps, object: team1Settings, action: read, allow: false},
{user: test.UserTeamAdminTeam1, object: globalSettings, action: write, allow: false},
{user: test.UserTeamAdminTeam1, object: globalSettings, action: read, allow: false},
{user: test.UserTeamAdminTeam1, object: team1Settings, action: write, allow: true},
{user: test.UserTeamAdminTeam1, object: team1Settings, action: read, allow: true},
{user: test.UserTeamAdminTeam2, object: globalSettings, action: write, allow: false},
{user: test.UserTeamAdminTeam2, object: globalSettings, action: read, allow: false},
{user: test.UserTeamAdminTeam2, object: team1Settings, action: write, allow: false},
{user: test.UserTeamAdminTeam2, object: team1Settings, action: read, allow: false},
{user: test.UserTeamMaintainerTeam1, object: globalSettings, action: write, allow: false},
{user: test.UserTeamMaintainerTeam1, object: globalSettings, action: read, allow: false},
{user: test.UserTeamMaintainerTeam1, object: team1Settings, action: write, allow: true},
{user: test.UserTeamMaintainerTeam1, object: team1Settings, action: read, allow: true},
{user: test.UserTeamMaintainerTeam2, object: globalSettings, action: write, allow: false},
{user: test.UserTeamMaintainerTeam2, object: globalSettings, action: read, allow: false},
{user: test.UserTeamMaintainerTeam2, object: team1Settings, action: write, allow: false},
{user: test.UserTeamMaintainerTeam2, object: team1Settings, action: read, allow: false},
{user: test.UserTeamObserverTeam1, object: globalSettings, action: write, allow: false},
{user: test.UserTeamObserverTeam1, object: globalSettings, action: read, allow: false},
{user: test.UserTeamObserverTeam1, object: team1Settings, action: write, allow: false},
{user: test.UserTeamObserverTeam1, object: team1Settings, action: read, allow: false},
{user: test.UserTeamObserverTeam2, object: globalSettings, action: write, allow: false},
{user: test.UserTeamObserverTeam2, object: globalSettings, action: read, allow: false},
{user: test.UserTeamObserverTeam2, object: team1Settings, action: write, allow: false},
{user: test.UserTeamObserverTeam2, object: team1Settings, action: read, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: globalSettings, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: globalSettings, action: read, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: team1Settings, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam1, object: team1Settings, action: read, allow: false},
{user: test.UserTeamObserverPlusTeam2, object: globalSettings, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam2, object: globalSettings, action: read, allow: false},
{user: test.UserTeamObserverPlusTeam2, object: team1Settings, action: write, allow: false},
{user: test.UserTeamObserverPlusTeam2, object: team1Settings, action: read, allow: false},
{user: test.UserTeamGitOpsTeam1, object: globalSettings, action: write, allow: false},
{user: test.UserTeamGitOpsTeam1, object: globalSettings, action: read, allow: false},
{user: test.UserTeamGitOpsTeam1, object: team1Settings, action: write, allow: true},
{user: test.UserTeamGitOpsTeam1, object: team1Settings, action: read, allow: false},
{user: test.UserTeamGitOpsTeam2, object: globalSettings, action: write, allow: false},
{user: test.UserTeamGitOpsTeam2, object: globalSettings, action: read, allow: false},
{user: test.UserTeamGitOpsTeam2, object: team1Settings, action: write, allow: false},
{user: test.UserTeamGitOpsTeam2, object: team1Settings, action: read, allow: false},
})
}
func assertAuthorized(t *testing.T, user *fleet.User, object, action interface{}) {
t.Helper()
+1 -1
View File
@@ -475,7 +475,7 @@ type MDMAppleSetupPayload struct {
// AuthzType implements authz.AuthzTyper.
func (p MDMAppleSetupPayload) AuthzType() string {
return "mdm_apple_settings" // TODO: add mdm_apple_setup to rego?
return "mdm_apple_settings"
}
// HostDEPAssignment represents a row in the host_dep_assignments table
+3 -2
View File
@@ -491,7 +491,8 @@ type Service interface {
// ModifyTeamEnrollSecrets modifies enroll secrets for a team.
ModifyTeamEnrollSecrets(ctx context.Context, teamID uint, secrets []EnrollSecret) ([]*EnrollSecret, error)
// ApplyTeamSpecs applies the changes for each team as defined in the specs.
ApplyTeamSpecs(ctx context.Context, specs []*TeamSpec, applyOpts ApplySpecOptions) error
// On success, it returns the mapping of team names to team ids.
ApplyTeamSpecs(ctx context.Context, specs []*TeamSpec, applyOpts ApplySpecOptions) (map[string]uint, error)
// /////////////////////////////////////////////////////////////////////////////
// ActivitiesService
@@ -699,7 +700,7 @@ type Service interface {
GetMDMAppleBootstrapPackageBytes(ctx context.Context, token string) (*MDMAppleBootstrapPackage, error)
GetMDMAppleBootstrapPackageMetadata(ctx context.Context, teamID uint) (*MDMAppleBootstrapPackage, error)
GetMDMAppleBootstrapPackageMetadata(ctx context.Context, teamID uint, forUpdate bool) (*MDMAppleBootstrapPackage, error)
DeleteMDMAppleBootstrapPackage(ctx context.Context, teamID *uint) error
+8 -2
View File
@@ -1840,6 +1840,12 @@ func (svc *Service) GetMDMAppleBootstrapPackageBytes(ctx context.Context, token
type bootstrapPackageMetadataRequest struct {
TeamID uint `url:"team_id"`
// ForUpdate is used to indicate that the authorization should be for a
// "write" instead of a "read", this is needed specifically for the gitops
// user which is a write-only user, but needs to call this endpoint to check
// if it needs to upload the bootstrap package (if the hashes are different).
ForUpdate bool `query:"for_update,optional"`
}
type bootstrapPackageMetadataResponse struct {
@@ -1851,14 +1857,14 @@ func (r bootstrapPackageMetadataResponse) error() error { return r.Err }
func bootstrapPackageMetadataEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*bootstrapPackageMetadataRequest)
meta, err := svc.GetMDMAppleBootstrapPackageMetadata(ctx, req.TeamID)
meta, err := svc.GetMDMAppleBootstrapPackageMetadata(ctx, req.TeamID, req.ForUpdate)
if err != nil {
return bootstrapPackageMetadataResponse{Err: err}, nil
}
return bootstrapPackageMetadataResponse{MDMAppleBootstrapPackage: meta}, nil
}
func (svc *Service) GetMDMAppleBootstrapPackageMetadata(ctx context.Context, teamID uint) (*fleet.MDMAppleBootstrapPackage, error) {
func (svc *Service) GetMDMAppleBootstrapPackageMetadata(ctx context.Context, teamID uint, forUpdate bool) (*fleet.MDMAppleBootstrapPackage, error) {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc.authz.SkipAuthorization(ctx)
+37 -31
View File
@@ -16,6 +16,7 @@ import (
"github.com/fleetdm/fleet/v4/pkg/spec"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
kithttp "github.com/go-kit/kit/transport/http"
)
// Client is used to consume Fleet APIs from Go code
@@ -223,28 +224,40 @@ func (c *Client) authenticatedRequest(params interface{}, verb string, path stri
}
func (c *Client) CheckMDMEnabled() error {
appCfg, err := c.GetAppConfig()
if err != nil {
return err
}
if !appCfg.MDM.EnabledAndConfigured {
return errors.New("MDM features aren't turned on. Use `fleetctl generate mdm-apple` and then `fleet serve` with `mdm` configuration to turn on MDM features.")
}
return nil
return c.runAppConfigChecks(func(ac *fleet.EnrichedAppConfig) error {
if !ac.MDM.EnabledAndConfigured {
return errors.New("MDM features aren't turned on. Use `fleetctl generate mdm-apple` and then `fleet serve` with `mdm` configuration to turn on MDM features.")
}
return nil
})
}
func (c *Client) CheckPremiumMDMEnabled() error {
return c.runAppConfigChecks(func(ac *fleet.EnrichedAppConfig) error {
if ac.License == nil || !ac.License.IsPremium() {
return errors.New("missing or invalid license")
}
if !ac.MDM.EnabledAndConfigured {
return errors.New("MDM features aren't turned on. Use `fleetctl generate mdm-apple` and then `fleet serve` with `mdm` configuration to turn on MDM features.")
}
return nil
})
}
func (c *Client) runAppConfigChecks(fn func(ac *fleet.EnrichedAppConfig) error) error {
appCfg, err := c.GetAppConfig()
if err != nil {
var sce kithttp.StatusCoder
if errors.As(err, &sce) && sce.StatusCode() == http.StatusForbidden {
// do not return an error, user may not have permission to read app
// config (e.g. gitops) and those appconfig checks are just convenience
// to avoid the round-trip with potentially large payload to the server.
// Those will still be validated with the actual API call.
return nil
}
return err
}
if appCfg.License == nil || !appCfg.License.IsPremium() {
return errors.New("missing or invalid license")
}
if !appCfg.MDM.EnabledAndConfigured {
return errors.New("MDM features aren't turned on. Use `fleetctl generate mdm-apple` and then `fleet serve` with `mdm` configuration to turn on MDM features.")
}
return nil
return fn(appCfg)
}
// ApplyGroup applies the given spec group to Fleet.
@@ -413,7 +426,8 @@ func (c *Client) ApplyGroup(
// Next, apply the teams specs before saving the profiles, so that any
// non-existing team gets created.
if err := c.ApplyTeams(specs.Teams, opts); err != nil {
teamIDsByName, err := c.ApplyTeams(specs.Teams, opts)
if err != nil {
return fmt.Errorf("applying teams: %w", err)
}
@@ -425,23 +439,15 @@ func (c *Client) ApplyGroup(
}
}
if len(tmBootstrapPackages)+len(tmMacSetupAssistants) > 0 && !opts.DryRun {
// TODO: we need to chat an define on a better way to do this, maybe make
// the endpoints support both id/name? have separate endpoints? Or make
// the apply team spec endpoint return team ids?
tms, err := c.ListTeams("")
if err != nil {
return err
}
for _, tm := range tms {
if bp, ok := tmBootstrapPackages[tm.Name]; ok {
if err := c.EnsureBootstrapPackage(bp, tm.ID); err != nil {
return fmt.Errorf("uploading bootstrap package for team %q: %w", tm.Name, err)
for tmName, tmID := range teamIDsByName {
if bp, ok := tmBootstrapPackages[tmName]; ok {
if err := c.EnsureBootstrapPackage(bp, tmID); err != nil {
return fmt.Errorf("uploading bootstrap package for team %q: %w", tmName, err)
}
}
if b, ok := tmMacSetupAssistants[tm.Name]; ok {
if err := c.uploadMacOSSetupAssistant(b, &tm.ID, tmMacSetup[tm.Name].MacOSSetupAssistant.Value); err != nil {
return fmt.Errorf("uploading macOS setup assistant for team %q: %w", tm.Name, err)
if b, ok := tmMacSetupAssistants[tmName]; ok {
if err := c.uploadMacOSSetupAssistant(b, &tmID, tmMacSetup[tmName].MacOSSetupAssistant.Value); err != nil {
return fmt.Errorf("uploading macOS setup assistant for team %q: %w", tmName, err)
}
}
}
+8 -3
View File
@@ -50,11 +50,16 @@ func (c *Client) RequestAppleCSR(email, org string) (*fleet.AppleCSR, error) {
return responseBody.AppleCSR, err
}
func (c *Client) GetBootstrapPackageMetadata(teamID uint) (*fleet.MDMAppleBootstrapPackage, error) {
func (c *Client) GetBootstrapPackageMetadata(teamID uint, forUpdate bool) (*fleet.MDMAppleBootstrapPackage, error) {
verb, path := "GET", fmt.Sprintf("/api/latest/fleet/mdm/apple/bootstrap/%d/metadata", teamID)
request := bootstrapPackageMetadataRequest{}
var responseBody bootstrapPackageMetadataResponse
err := c.authenticatedRequest(request, verb, path, &responseBody)
var err error
if forUpdate {
err = c.authenticatedRequestWithQuery(request, verb, path, &responseBody, "for_update=true")
} else {
err = c.authenticatedRequest(request, verb, path, &responseBody)
}
return responseBody.MDMAppleBootstrapPackage, err
}
@@ -110,7 +115,7 @@ func (c *Client) UploadBootstrapPackage(pkg *fleet.MDMAppleBootstrapPackage) err
func (c *Client) EnsureBootstrapPackage(bp *fleet.MDMAppleBootstrapPackage, teamID uint) error {
isFirstTime := false
oldMeta, err := c.GetBootstrapPackageMetadata(teamID)
oldMeta, err := c.GetBootstrapPackageMetadata(teamID, true)
if err != nil {
// not found is OK, it means this is our first time uploading a package
if !errors.Is(err, notFoundErr{}) {
+6 -2
View File
@@ -42,10 +42,14 @@ func (c *Client) DeleteTeam(teamID uint) error {
// ApplyTeams sends the list of Teams to be applied to the
// Fleet instance.
func (c *Client) ApplyTeams(specs []json.RawMessage, opts fleet.ApplySpecOptions) error {
func (c *Client) ApplyTeams(specs []json.RawMessage, opts fleet.ApplySpecOptions) (map[string]uint, error) {
verb, path := "POST", "/api/latest/fleet/spec/teams"
var responseBody applyTeamSpecsResponse
return c.authenticatedRequestWithQuery(map[string]interface{}{"specs": specs}, verb, path, &responseBody, opts.RawQuery())
err := c.authenticatedRequestWithQuery(map[string]interface{}{"specs": specs}, verb, path, &responseBody, opts.RawQuery())
if err != nil {
return nil, err
}
return responseBody.TeamIDsByName, nil
}
// ApplyTeamProfiles sends the list of profiles to be applied for the specified
+16 -4
View File
@@ -108,10 +108,13 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
},
},
}
s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
var applyResp applyTeamSpecsResponse
s.DoJSON("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK, &applyResp)
require.Len(t, applyResp.TeamIDsByName, 1)
team, err := s.ds.TeamByName(context.Background(), teamName)
require.NoError(t, err)
require.Equal(t, applyResp.TeamIDsByName[teamName], team.ID)
assert.Len(t, team.Secrets, 1)
require.JSONEq(t, string(agentOpts), string(*team.Config.AgentOptions))
require.Equal(t, fleet.Features{
@@ -197,7 +200,10 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
},
},
}
s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK, "dry_run", "true")
applyResp = applyTeamSpecsResponse{}
s.DoJSON("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK, &applyResp, "dry_run", "true")
// dry-run never returns id to name mappings as it may not have them
require.Empty(t, applyResp.TeamIDsByName)
// dry-run with macos disk encryption set to true
teamSpecs = map[string]any{
@@ -334,7 +340,9 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
},
},
}
s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
applyResp = applyTeamSpecsResponse{}
s.DoJSON("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK, &applyResp)
require.Len(t, applyResp.TeamIDsByName, 1)
teams, err = s.ds.ListTeams(context.Background(), fleet.TeamFilter{User: user}, fleet.ListOptions{})
require.NoError(t, err)
@@ -342,6 +350,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
team, err = s.ds.TeamByName(context.Background(), "team2")
require.NoError(t, err)
require.Equal(t, applyResp.TeamIDsByName["team2"], team.ID)
appConfig, err := s.ds.AppConfig(context.Background())
require.NoError(t, err)
@@ -364,10 +373,13 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
},
},
}
s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
applyResp = applyTeamSpecsResponse{}
s.DoJSON("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK, &applyResp)
require.Len(t, applyResp.TeamIDsByName, 1)
team, err = s.ds.TeamByName(context.Background(), "team2")
require.NoError(t, err)
require.Equal(t, applyResp.TeamIDsByName["team2"], team.ID)
require.Len(t, team.Secrets, 1)
assert.Equal(t, "ABC", team.Secrets[0].Secret)
-1
View File
@@ -208,7 +208,6 @@ func (svc *Service) VerifyMDMAppleConfigured(ctx context.Context) error {
// skipauth: Authorization is currently for user endpoints only.
svc.authz.SkipAuthorization(ctx)
return fleet.ErrMDMNotConfigured
}
return nil
+6 -5
View File
@@ -213,7 +213,8 @@ func (req *applyTeamSpecsRequest) DecodeBody(ctx context.Context, r io.Reader) e
}
type applyTeamSpecsResponse struct {
Err error `json:"error,omitempty"`
Err error `json:"error,omitempty"`
TeamIDsByName map[string]uint `json:"team_ids_by_name,omitempty"`
}
func (r applyTeamSpecsResponse) error() error { return r.Err }
@@ -230,22 +231,22 @@ func applyTeamSpecsEndpoint(ctx context.Context, request interface{}, svc fleet.
}
}
err := svc.ApplyTeamSpecs(ctx, actualSpecs, fleet.ApplySpecOptions{
idsByName, err := svc.ApplyTeamSpecs(ctx, actualSpecs, fleet.ApplySpecOptions{
Force: req.Force,
DryRun: req.DryRun,
})
if err != nil {
return applyTeamSpecsResponse{Err: err}, nil
}
return applyTeamSpecsResponse{}, nil
return applyTeamSpecsResponse{TeamIDsByName: idsByName}, nil
}
func (svc Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, applyOpts fleet.ApplySpecOptions) error {
func (svc Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, applyOpts fleet.ApplySpecOptions) (map[string]uint, error) {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc.authz.SkipAuthorization(ctx)
return fleet.ErrMissingLicense
return nil, fleet.ErrMissingLicense
}
////////////////////////////////////////////////////////////////////////////////
+8 -6
View File
@@ -188,7 +188,7 @@ func TestTeamAuth(t *testing.T) {
_, err = svc.ModifyTeamEnrollSecrets(ctx, 1, []fleet.EnrollSecret{{Secret: "newteamsecret", CreatedAt: time.Now()}})
checkAuthErr(t, tt.shouldFailTeamSecretsWrite, err)
err = svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "team1"}}, fleet.ApplySpecOptions{})
_, err = svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "team1"}}, fleet.ApplySpecOptions{})
checkAuthErr(t, tt.shouldFailTeamWrite, err)
})
}
@@ -282,7 +282,7 @@ func TestApplyTeamSpecs(t *testing.T) {
return nil
}
err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "team1", Features: tt.spec}}, fleet.ApplySpecOptions{})
_, err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "team1", Features: tt.spec}}, fleet.ApplySpecOptions{})
require.NoError(t, err)
})
}
@@ -350,11 +350,11 @@ func TestApplyTeamSpecs(t *testing.T) {
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
return &fleet.Team{Config: fleet.TeamConfig{Features: tt.old}}, nil
return &fleet.Team{ID: 123, Config: fleet.TeamConfig{Features: tt.old}}, nil
}
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
return &fleet.Team{}, nil
return &fleet.Team{ID: 123}, nil
}
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
@@ -363,8 +363,10 @@ func TestApplyTeamSpecs(t *testing.T) {
return nil
}
err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "team1", Features: tt.spec}}, fleet.ApplySpecOptions{})
idsByTeam, err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "team1", Features: tt.spec}}, fleet.ApplySpecOptions{})
require.NoError(t, err)
require.Len(t, idsByTeam, 1)
require.Equal(t, uint(123), idsByTeam["team1"])
})
}
})
@@ -383,7 +385,7 @@ func TestApplyTeamSpecsErrorInTeamByName(t *testing.T) {
}
authzctx := &authz_ctx.AuthorizationContext{}
ctx = authz_ctx.NewContext(ctx, authzctx)
err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "Foo"}}, fleet.ApplySpecOptions{})
_, err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "Foo"}}, fleet.ApplySpecOptions{})
require.Error(t, err)
az, ok := authz_ctx.FromContext(ctx)
require.True(t, ok)