Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions api/types/jamf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright 2023 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package types

import (
"net/url"
"strings"
"time"

"github.com/gravitational/trace"
"golang.org/x/exp/slices"
)

const (
// JamfOnMissingNOOP is the textual representation for the NOOP on_missing
// action.
JamfOnMissingNoop = "NOOP"
// JamfOnMissingDelete is the textual representation for the DELETE on_missing
// action.
JamfOnMissingDelete = "DELETE"
)

// JamfOnMissingActions is a slice of all textual on_missing representations,
// excluding the empty string.
var JamfOnMissingActions = []string{
JamfOnMissingNoop,
JamfOnMissingDelete,
}

const (
// JamfSyncPeriodPartialDefault is the default value for sync_period_partial
// in inventory entries.
JamfSyncPeriodPartialDefault = 6 * time.Hour
// JamfSyncPeriodFullDefault is the default value for sync_period_full in
// inventory entries.
JamfSyncPeriodFullDefault = 24 * time.Hour
)

// ValidateJamfSpecV1 validates a [JamfSpecV1] instance.
func ValidateJamfSpecV1(s *JamfSpecV1) error {
switch {
case s == nil:
return trace.BadParameter("spec required")
case s.Username == "":
return trace.BadParameter("username required")
case s.Password == "":
return trace.BadParameter("password required")
}

switch u, err := url.Parse(s.ApiEndpoint); {
case err != nil:
return trace.BadParameter("invalid API endpoint: %v", err)
case u.Host == "":
return trace.BadParameter("invalid API endpoint: missing hostname")
}

for i, e := range s.Inventory {
switch {
case e == nil:
return trace.BadParameter("inventory entry #%v is nil", i)
case e.OnMissing != "" && !slices.Contains(JamfOnMissingActions, e.OnMissing):
return trace.BadParameter(
"inventory[%v]: invalid on_missing action %q (expect empty or one of [%v])",
i, e.OnMissing, strings.Join(JamfOnMissingActions, ","))
}

syncPartial := e.SyncPeriodPartial
if syncPartial == 0 {
syncPartial = Duration(JamfSyncPeriodPartialDefault)
}
syncFull := e.SyncPeriodFull
if syncFull == 0 {
syncFull = Duration(JamfSyncPeriodFullDefault)
}
if syncPartial > syncFull {
return trace.BadParameter("inventory[%v]: sync_period_partial is greater than sync_period_full, partial syncs will never happen", i)
}
}

return nil
}
177 changes: 177 additions & 0 deletions api/types/jamf_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Copyright 2023 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package types_test

import (
"testing"
"time"

"github.com/gogo/protobuf/proto"
"github.com/gravitational/trace"
"github.com/stretchr/testify/assert"

"github.com/gravitational/teleport/api/types"
)

func TestValidateJamfSpecV1(t *testing.T) {
validSpec := &types.JamfSpecV1{
Enabled: true,
ApiEndpoint: "https://yourtenant.jamfcloud.com",
Username: "llama",
Password: "supersecret!!1!",
}
validEntry := &types.JamfInventoryEntry{
FilterRsql: "", // no filters
SyncPeriodPartial: 0, // default period
SyncPeriodFull: 0, // default period
OnMissing: "", // same as NOOP
}

modify := func(f func(spec *types.JamfSpecV1)) *types.JamfSpecV1 {
spec := proto.Clone(validSpec).(*types.JamfSpecV1)
f(spec)
return spec
}

tests := []struct {
name string
spec *types.JamfSpecV1
wantErr string
}{
{
name: "minimal spec",
spec: validSpec,
},
{
name: "spec with inventory",
spec: &types.JamfSpecV1{
Enabled: true,
ApiEndpoint: "https://yourtenant.jamfcloud.com",
Username: "llama",
Password: "supersecret!!1!",
Inventory: []*types.JamfInventoryEntry{
{
FilterRsql: `general.remoteManagement.managed==true and general.platform=="Mac"`,
SyncPeriodPartial: types.Duration(4 * time.Hour),
SyncPeriodFull: types.Duration(48 * time.Hour),
OnMissing: "DELETE",
},
{
FilterRsql: `general.remoteManagement.managed==false`,
OnMissing: "NOOP",
},
validEntry,
},
},
},
{
name: "all fields",
spec: &types.JamfSpecV1{
Enabled: true,
Name: "jamf2",
SyncDelay: types.Duration(2 * time.Minute),
ApiEndpoint: "https://yourtenant.jamfcloud.com",
Username: "llama",
Password: "supersecret!!1!",
Inventory: []*types.JamfInventoryEntry{
{
FilterRsql: `general.remoteManagement.managed==true and general.platform=="Mac"`,
SyncPeriodPartial: types.Duration(4 * time.Hour),
SyncPeriodFull: types.Duration(48 * time.Hour),
OnMissing: "DELETE",
},
},
},
},
{
name: "nil spec",
spec: nil,
wantErr: "spec required",
},
{
name: "api_endpoint invalid",
spec: modify(func(spec *types.JamfSpecV1) {
spec.ApiEndpoint = "https://%%"
}),
wantErr: "API endpoint",
},
{
name: "api_endpoint empty hostname",
spec: modify(func(spec *types.JamfSpecV1) {
spec.ApiEndpoint = "not a valid URL"
}),
wantErr: "missing hostname",
},
{
name: "username empty",
spec: modify(func(spec *types.JamfSpecV1) {
spec.Username = ""
}),
wantErr: "username",
},
{
name: "password empty",
spec: modify(func(spec *types.JamfSpecV1) {
spec.Password = ""
}),
wantErr: "password",
},
{
name: "inventory nil entry",
spec: modify(func(spec *types.JamfSpecV1) {
spec.Inventory = []*types.JamfInventoryEntry{
nil,
}
}),
wantErr: "is nil",
},
{
name: "inventory sync_partial > sync_full",
spec: modify(func(spec *types.JamfSpecV1) {
spec.Inventory = []*types.JamfInventoryEntry{
validEntry,
{
SyncPeriodPartial: types.Duration(12 * time.Hour),
SyncPeriodFull: types.Duration(8 * time.Hour),
},
}
}),
wantErr: "greater than sync_period_full",
},
{
name: "inventory on_missing invalid",
spec: modify(func(spec *types.JamfSpecV1) {
spec.Inventory = []*types.JamfInventoryEntry{
validEntry,
{
OnMissing: "BANANA",
},
}
}),
wantErr: "on_missing",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := types.ValidateJamfSpecV1(test.spec)
if test.wantErr == "" {
assert.NoError(t, err, "ValidateJamfSpecV1 failed")
} else {
assert.ErrorContains(t, err, test.wantErr, "ValidateJamfSpecV1 error mismatch")
assert.True(t, trace.IsBadParameter(err), "ValidateJamfSpecV1 returned non-BadParameter error: %T", err)
}
})
}
}
25 changes: 25 additions & 0 deletions lib/config/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"net/url"
"os"
"path/filepath"
"reflect"
"regexp"
"runtime"
"strings"
Expand Down Expand Up @@ -472,6 +473,12 @@ func ApplyFileConfig(fc *FileConfig, cfg *servicecfg.Config) error {
}
}

// Apply regardless of Jamf being enabled.
// If a config is present, we want it to be valid.
if err := applyJamfConfig(fc, cfg); err != nil {
return trace.Wrap(err)
}

return nil
}

Expand Down Expand Up @@ -2393,3 +2400,21 @@ func applyOktaConfig(fc *FileConfig, cfg *servicecfg.Config) error {
cfg.Okta.APITokenPath = fc.Okta.APITokenPath
return nil
}

func applyJamfConfig(fc *FileConfig, cfg *servicecfg.Config) error {
// Ignore empty configs, validate and transform anything else.
if reflect.DeepEqual(fc.Jamf, JamfService{}) {
return nil
}

jamfSpec, err := fc.Jamf.toJamfSpecV1()
if err != nil {
return trace.Wrap(err)
}

cfg.Jamf = servicecfg.JamfConfig{
Spec: jamfSpec,
ExitOnSync: fc.Jamf.ExitOnSync,
}
return nil
}
Loading