Skip to content

Commit 61689a4

Browse files
committed
add tims review suggestions
1 parent 3b470f6 commit 61689a4

17 files changed

+863
-498
lines changed

providers/jamf/connection/connection.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,19 @@ type JamfConnection struct {
1919
Conf *inventory.Config
2020
asset *inventory.Asset
2121
Client *jamfpro.Client
22+
23+
// localUserAccounts caches per-computer local user accounts from the
24+
// initial inventory fetch, keyed by computer ID. This avoids N+1 API
25+
// calls when iterating computerInventory then accessing localUserAccounts.
26+
localUserAccounts map[string][]jamfpro.ComputerInventorySubsetLocalUserAccount
2227
}
2328

2429
func NewJamfConnection(id uint32, asset *inventory.Asset, conf *inventory.Config) (*JamfConnection, error) {
2530
conn := &JamfConnection{
26-
Connection: plugin.NewConnection(id, asset),
27-
Conf: conf,
28-
asset: asset,
31+
Connection: plugin.NewConnection(id, asset),
32+
Conf: conf,
33+
asset: asset,
34+
localUserAccounts: make(map[string][]jamfpro.ComputerInventorySubsetLocalUserAccount),
2935
}
3036

3137
// Extract credentials and options from conf
@@ -88,3 +94,16 @@ func (j *JamfConnection) Identifier() string {
8894
domain := j.Conf.Options["instance_domain"]
8995
return "//platformid.api.mondoo.app/runtime/jamf/" + strings.ToLower(domain)
9096
}
97+
98+
// CacheLocalUserAccounts stores local user accounts for a computer ID,
99+
// populated during the initial inventory fetch.
100+
func (j *JamfConnection) CacheLocalUserAccounts(computerID string, accounts []jamfpro.ComputerInventorySubsetLocalUserAccount) {
101+
j.localUserAccounts[computerID] = accounts
102+
}
103+
104+
// GetCachedLocalUserAccounts retrieves cached local user accounts for a
105+
// computer ID. Returns nil, false if no cache entry exists.
106+
func (j *JamfConnection) GetCachedLocalUserAccounts(computerID string) ([]jamfpro.ComputerInventorySubsetLocalUserAccount, bool) {
107+
accounts, ok := j.localUserAccounts[computerID]
108+
return accounts, ok
109+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright Mondoo, Inc. 2024, 2026
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package connection
5+
6+
import (
7+
"testing"
8+
9+
"github.com/deploymenttheory/go-api-sdk-jamfpro/sdk/jamfpro"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
"go.mondoo.com/mql/v13/providers-sdk/v1/inventory"
13+
"go.mondoo.com/mql/v13/providers-sdk/v1/vault"
14+
)
15+
16+
func TestNewJamfConnection_MissingCredentials(t *testing.T) {
17+
_, err := NewJamfConnection(0, &inventory.Asset{}, &inventory.Config{
18+
Options: map[string]string{},
19+
})
20+
require.Error(t, err)
21+
assert.Contains(t, err.Error(), "missing required Jamf credentials")
22+
}
23+
24+
func TestNewJamfConnection_MissingDomain(t *testing.T) {
25+
cred := &vault.Credential{
26+
Type: vault.CredentialType_password,
27+
User: "client-id",
28+
Secret: []byte("client-secret"),
29+
}
30+
_, err := NewJamfConnection(0, &inventory.Asset{}, &inventory.Config{
31+
Options: map[string]string{},
32+
Credentials: []*vault.Credential{cred},
33+
})
34+
require.Error(t, err)
35+
assert.Contains(t, err.Error(), "missing required Jamf credentials")
36+
}
37+
38+
func TestNewJamfConnection_MissingClientID(t *testing.T) {
39+
_, err := NewJamfConnection(0, &inventory.Asset{}, &inventory.Config{
40+
Options: map[string]string{
41+
"instance_domain": "https://example.jamfcloud.com",
42+
},
43+
})
44+
require.Error(t, err)
45+
assert.Contains(t, err.Error(), "missing required Jamf credentials")
46+
}
47+
48+
func TestJamfConnection_PlatformInfo(t *testing.T) {
49+
conn := &JamfConnection{}
50+
platform, err := conn.PlatformInfo()
51+
require.NoError(t, err)
52+
assert.Equal(t, "jamf", platform.Name)
53+
assert.Equal(t, "Jamf Pro", platform.Title)
54+
assert.Equal(t, "api", platform.Kind)
55+
assert.Equal(t, "jamf", platform.Runtime)
56+
assert.Equal(t, []string{"jamf"}, platform.Family)
57+
}
58+
59+
func TestJamfConnection_Identifier(t *testing.T) {
60+
conn := &JamfConnection{
61+
Conf: &inventory.Config{
62+
Options: map[string]string{
63+
"instance_domain": "https://MyCompany.jamfcloud.com",
64+
},
65+
},
66+
}
67+
id := conn.Identifier()
68+
assert.Equal(t, "//platformid.api.mondoo.app/runtime/jamf/https://mycompany.jamfcloud.com", id)
69+
}
70+
71+
func TestJamfConnection_Name(t *testing.T) {
72+
conn := &JamfConnection{}
73+
assert.Equal(t, "jamf", conn.Name())
74+
}
75+
76+
func TestJamfConnection_LocalUserAccountsCache(t *testing.T) {
77+
conn := &JamfConnection{
78+
localUserAccounts: make(map[string][]jamfpro.ComputerInventorySubsetLocalUserAccount),
79+
}
80+
81+
// Cache should be empty initially
82+
_, ok := conn.GetCachedLocalUserAccounts("123")
83+
assert.False(t, ok)
84+
85+
// After caching, should return data
86+
testAccounts := []jamfpro.ComputerInventorySubsetLocalUserAccount{
87+
{UID: "501", Username: "admin", Admin: true},
88+
{UID: "502", Username: "user1", Admin: false},
89+
}
90+
conn.CacheLocalUserAccounts("123", testAccounts)
91+
92+
cached, ok := conn.GetCachedLocalUserAccounts("123")
93+
assert.True(t, ok)
94+
require.Len(t, cached, 2)
95+
assert.Equal(t, "admin", cached[0].Username)
96+
assert.True(t, cached[0].Admin)
97+
assert.Equal(t, "user1", cached[1].Username)
98+
assert.False(t, cached[1].Admin)
99+
100+
// Different ID should miss cache
101+
_, ok = conn.GetCachedLocalUserAccounts("456")
102+
assert.False(t, ok)
103+
}

providers/jamf/go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ go 1.25.8
77
require (
88
github.com/deploymenttheory/go-api-sdk-jamfpro v1.28.0
99
github.com/rs/zerolog v1.35.0
10+
github.com/stretchr/testify v1.11.1
1011
go.mondoo.com/mql/v13 v13.4.0
1112
)
1213

@@ -60,6 +61,7 @@ require (
6061
github.com/cockroachdb/redact v1.1.8 // indirect
6162
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
6263
github.com/danieljoos/wincred v1.2.3 // indirect
64+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
6365
github.com/deploymenttheory/go-api-http-client v0.4.1 // indirect
6466
github.com/deploymenttheory/go-api-http-client-integrations v0.0.13 // indirect
6567
github.com/dvsekhvalnov/jose2go v1.8.0 // indirect
@@ -117,6 +119,7 @@ require (
117119
github.com/pjbgf/sha1cd v0.5.0 // indirect
118120
github.com/pkg/errors v0.9.1 // indirect
119121
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
122+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
120123
github.com/rivo/uniseg v0.4.7 // indirect
121124
github.com/rogpeppe/go-internal v1.14.1 // indirect
122125
github.com/ryanuber/go-glob v1.0.0 // indirect
@@ -157,6 +160,7 @@ require (
157160
google.golang.org/grpc v1.80.0 // indirect
158161
google.golang.org/protobuf v1.36.11 // indirect
159162
gopkg.in/warnings.v0 v0.1.2 // indirect
163+
gopkg.in/yaml.v3 v3.0.1 // indirect
160164
moul.io/http2curl v1.0.0 // indirect
161165
sigs.k8s.io/yaml v1.6.0 // indirect
162166
)

providers/jamf/provider/provider.go

Lines changed: 16 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -36,47 +36,22 @@ func (s *Service) ParseCLI(req *plugin.ParseCLIReq) (*plugin.ParseCLIRes, error)
3636
flags = map[string]*llx.Primitive{}
3737
}
3838

39-
conf := &inventory.Config{
40-
Type: req.Connector,
41-
Options: map[string]string{},
42-
}
43-
44-
// CLI flags for credentials
45-
var clientId, clientSecret, instanceDomain string
46-
47-
if v, ok := flags["client-id"]; ok && len(v.Value) != 0 {
48-
clientId = string(v.Value)
49-
conf.Options["client_id"] = clientId
50-
}
51-
if v, ok := flags["client-secret"]; ok && len(v.Value) != 0 {
52-
clientSecret = string(v.Value)
53-
conf.Options["client_secret"] = clientSecret
54-
}
55-
if v, ok := flags["instance-domain"]; ok && len(v.Value) != 0 {
56-
instanceDomain = string(v.Value)
57-
conf.Options["instance_domain"] = instanceDomain
58-
}
39+
// Resolve credentials: flags take precedence over env vars
40+
clientId := flagOrEnv(flags, "client-id", "CLIENT_ID")
41+
clientSecret := flagOrEnv(flags, "client-secret", "CLIENT_SECRET")
42+
instanceDomain := flagOrEnv(flags, "instance-domain", "INSTANCE_DOMAIN")
5943

60-
// Fallback to env if flags not provided
61-
if clientId == "" {
62-
clientId = os.Getenv("CLIENT_ID")
63-
}
64-
if clientSecret == "" {
65-
clientSecret = os.Getenv("CLIENT_SECRET")
66-
}
67-
if instanceDomain == "" {
68-
instanceDomain = os.Getenv("INSTANCE_DOMAIN")
44+
conf := &inventory.Config{
45+
Type: req.Connector,
46+
Options: map[string]string{
47+
"instance_domain": instanceDomain,
48+
},
6949
}
7050

71-
// Attach as credentials if available
7251
if clientId != "" || clientSecret != "" {
7352
conf.Credentials = append(conf.Credentials, vault.NewPasswordCredential(clientId, clientSecret))
7453
}
7554

76-
if instanceDomain != "" && conf.Options["instance_domain"] == "" {
77-
conf.Options["instance_domain"] = instanceDomain
78-
}
79-
8055
asset := &inventory.Asset{
8156
Name: "Jamf",
8257
Connections: []*inventory.Config{conf},
@@ -85,6 +60,13 @@ func (s *Service) ParseCLI(req *plugin.ParseCLIReq) (*plugin.ParseCLIRes, error)
8560
return &plugin.ParseCLIRes{Asset: asset}, nil
8661
}
8762

63+
func flagOrEnv(flags map[string]*llx.Primitive, flagName, envName string) string {
64+
if v, ok := flags[flagName]; ok && len(v.Value) != 0 {
65+
return string(v.Value)
66+
}
67+
return os.Getenv(envName)
68+
}
69+
8870
func (s *Service) Connect(req *plugin.ConnectReq, callback plugin.ProviderCallback) (*plugin.ConnectRes, error) {
8971
if req == nil || req.Asset == nil {
9072
return nil, errors.New("no connection data provided")
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright Mondoo, Inc. 2024, 2026
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package provider
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
"go.mondoo.com/mql/v13/llx"
12+
"go.mondoo.com/mql/v13/providers-sdk/v1/plugin"
13+
)
14+
15+
func TestParseCLI_WithFlags(t *testing.T) {
16+
s := Init()
17+
18+
res, err := s.ParseCLI(&plugin.ParseCLIReq{
19+
Connector: "jamf",
20+
Flags: map[string]*llx.Primitive{
21+
"client-id": {Value: []byte("my-client-id")},
22+
"client-secret": {Value: []byte("my-client-secret")},
23+
"instance-domain": {Value: []byte("https://example.jamfcloud.com")},
24+
},
25+
})
26+
require.NoError(t, err)
27+
require.NotNil(t, res.Asset)
28+
29+
conf := res.Asset.Connections[0]
30+
assert.Equal(t, "jamf", conf.Type)
31+
assert.Equal(t, "https://example.jamfcloud.com", conf.Options["instance_domain"])
32+
33+
// Credentials should be set
34+
require.Len(t, conf.Credentials, 1)
35+
assert.Equal(t, "my-client-id", conf.Credentials[0].User)
36+
assert.Equal(t, "my-client-secret", string(conf.Credentials[0].Secret))
37+
38+
// Secrets should NOT be in Options
39+
_, hasClientId := conf.Options["client_id"]
40+
_, hasClientSecret := conf.Options["client_secret"]
41+
assert.False(t, hasClientId, "client_id should not be in Options")
42+
assert.False(t, hasClientSecret, "client_secret should not be in Options")
43+
}
44+
45+
func TestParseCLI_EmptyFlags(t *testing.T) {
46+
s := Init()
47+
48+
res, err := s.ParseCLI(&plugin.ParseCLIReq{
49+
Connector: "jamf",
50+
Flags: nil,
51+
})
52+
require.NoError(t, err)
53+
require.NotNil(t, res.Asset)
54+
55+
conf := res.Asset.Connections[0]
56+
assert.Equal(t, "jamf", conf.Type)
57+
assert.Empty(t, conf.Credentials)
58+
}
59+
60+
func TestParseCLI_PartialFlags(t *testing.T) {
61+
s := Init()
62+
63+
res, err := s.ParseCLI(&plugin.ParseCLIReq{
64+
Connector: "jamf",
65+
Flags: map[string]*llx.Primitive{
66+
"instance-domain": {Value: []byte("https://example.jamfcloud.com")},
67+
},
68+
})
69+
require.NoError(t, err)
70+
require.NotNil(t, res.Asset)
71+
72+
conf := res.Asset.Connections[0]
73+
assert.Equal(t, "https://example.jamfcloud.com", conf.Options["instance_domain"])
74+
assert.Empty(t, conf.Credentials, "no credentials when only domain is provided")
75+
}
76+
77+
func TestParseCLI_EnvVarFallback(t *testing.T) {
78+
t.Setenv("CLIENT_ID", "env-client-id")
79+
t.Setenv("CLIENT_SECRET", "env-client-secret")
80+
t.Setenv("INSTANCE_DOMAIN", "https://env.jamfcloud.com")
81+
82+
s := Init()
83+
84+
res, err := s.ParseCLI(&plugin.ParseCLIReq{
85+
Connector: "jamf",
86+
Flags: map[string]*llx.Primitive{},
87+
})
88+
require.NoError(t, err)
89+
require.NotNil(t, res.Asset)
90+
91+
conf := res.Asset.Connections[0]
92+
assert.Equal(t, "https://env.jamfcloud.com", conf.Options["instance_domain"])
93+
require.Len(t, conf.Credentials, 1)
94+
assert.Equal(t, "env-client-id", conf.Credentials[0].User)
95+
assert.Equal(t, "env-client-secret", string(conf.Credentials[0].Secret))
96+
}
97+
98+
func TestParseCLI_FlagsTakePrecedenceOverEnv(t *testing.T) {
99+
t.Setenv("CLIENT_ID", "env-client-id")
100+
t.Setenv("INSTANCE_DOMAIN", "https://env.jamfcloud.com")
101+
102+
s := Init()
103+
104+
res, err := s.ParseCLI(&plugin.ParseCLIReq{
105+
Connector: "jamf",
106+
Flags: map[string]*llx.Primitive{
107+
"client-id": {Value: []byte("flag-client-id")},
108+
"instance-domain": {Value: []byte("https://flag.jamfcloud.com")},
109+
},
110+
})
111+
require.NoError(t, err)
112+
113+
conf := res.Asset.Connections[0]
114+
assert.Equal(t, "https://flag.jamfcloud.com", conf.Options["instance_domain"])
115+
require.Len(t, conf.Credentials, 1)
116+
assert.Equal(t, "flag-client-id", conf.Credentials[0].User)
117+
}

0 commit comments

Comments
 (0)