jamfplatform

package
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 15, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package jamfplatform provides a Go client for the Jamf Platform API.

Create a root client with NewClient, then construct service clients from the sub-packages under jamfplatform/ (devices, devicegroups, deviceactions, blueprints, ddmreport, compliancebenchmarks, pro, ...) to call typed methods.

c := jamfplatform.NewClient(
	"https://eu.api.jamfcloud.com",
	os.Getenv("JAMFPLATFORM_CLIENT_ID"),
	os.Getenv("JAMFPLATFORM_CLIENT_SECRET"),
	jamfplatform.WithTenantID(os.Getenv("JAMFPLATFORM_TENANT_ID")),
)

ds, err := devices.New(c).ListDevices(ctx, nil, "")

The root client handles OAuth2 authentication and token refresh automatically; each sub-package shares the same transport via its [New] constructor.

Error handling uses *APIResponseError for structured API errors:

d, err := devices.New(c).GetDevice(ctx, id)
if errors.As(err, &apiErr) && apiErr.HasStatus(404) {
	// handle not found
}

Response headers

Generated methods return the decoded body only. Response headers — including Location on 201 Created, Retry-After on 429 (which the transport already honors with a bounded single retry), and Deprecation on soon-to-be-removed endpoints (logged automatically) — are available to consumers via the WithLogger option. Install a Logger whose LogResponse receives http.Header if you need to inspect Location or any other per-request header.

Note that the body returned by create endpoints already carries an "href" field pointing at the new resource, equivalent to Location.

Index

Constants

View Source
const (
	// ScopeOrganization is the zero value and sends no header: the gateway
	// resolves the organization from the access token. It exists so the
	// generated Privileges registry can name this scope instead of leaving an
	// empty slice, which would be indistinguishable from "the spec declared
	// nothing". There is no WithOrganizationID — an unset scope already means
	// organization.
	ScopeOrganization = client.ScopeOrganization
	// ScopeTenant scopes requests to a single product tenant, sent as
	// X-Tenant-Id. The legacy scope; prefer ScopeEnvironment.
	ScopeTenant = client.ScopeTenant
	// ScopeEnvironment scopes requests to a platform environment — a grouping
	// of tenants — sent as X-Environment-Id. The scope to prefer.
	ScopeEnvironment = client.ScopeEnvironment
)

Scope kinds. A zero value means no scope, which is how organization-scoped credentials work: the gateway resolves the context from the access token, so the client sends no scope header. That zero value is named ScopeOrganization for the benefit of the generated Privileges registry; it carries no header, confirmed absent across every published spec and the gateway configuration.

View Source
const JamfProAPIVersion = "11.32.0"

JamfProAPIVersion is the Jamf Pro release this SDK was generated from, read directly from the info.version field of the openapi-jpapi.json spec at generate time. Use it to log/report which API surface the linked SDK build targets.

Variables

View Source
var BuildRSQLExpression = client.BuildRSQLExpression
View Source
var ErrUnexpectedResponse = client.ErrUnexpectedResponse

ErrUnexpectedResponse reports that an endpoint answered with an HTML page where JSON was expected — an edge proxy, WAF or IP allowlist rejecting the caller, or a gateway error page — which means the request did not reach Jamf. Raised on the OAuth token exchange, where such a block surfaces first, and on any API response whose error body is an HTML page.

Jamf Pro's own HTML error template is excluded: it is a real application message and is lifted into Details() instead. A plain-text refusal is excluded too — notably the gateway's "Authentication failed", which is a credential or api-product problem and not a block, so it carries guidance rather than this sentinel.

The page itself is condensed to its headings and any edge request id, so Error() stays one line; the full body remains on APIResponseError.Body.

Read the status before choosing a remedy. This sentinel says only that a page answered instead of an API, and two different faults do that: a WAF, proxy or allowlist refusing this host, which arrives as a 403 or as a 200 carrying a login shell and is a standing block; and Jamf's own gateway failing, which arrives as a 502/503/504 the transport has already retried and is usually transient. AsAPIError(err).StatusCode separates them, so report an egress IP for the first rather than for both.

The only sentinel the SDK exposes, and the only error worth matching with errors.Is; everything else is *APIResponseError. It exists because the condition is inferred from the shape of the body rather than reported by Jamf, and because acting on it means doing something extra rather than just rendering a message:

if errors.Is(err, jamfplatform.ErrUnexpectedResponse) {
	// Not a credential problem. Report the host's egress IP and a
	// timestamp so Jamf Support can find the block.
}

A rejected credential returns JSON (401 invalid_client) and never carries this sentinel, so the two causes stay distinguishable — they need opposite remedies.

Named to match jamfprotect-go-sdk so provider code ports unchanged when the Protect resources move into terraform-provider-jamfplatform.

View Source
var FormatArgument = client.FormatArgument

Functions

func PollUntil

func PollUntil(ctx context.Context, interval time.Duration, checker func(context.Context) (bool, error)) error

Types

type APIResponseError

type APIResponseError = client.APIResponseError

APIResponseError is returned for any non-success HTTP status. Consumers should inspect it via AsAPIError plus the accessor methods (HasStatus/Details/FieldErrors/Summary) rather than string-matching the Error() output. Non-HTTP errors (denylist refusal, context cancellation, IO failures, etc.) surface as plain wrapped errors — format them with err.Error(), except for ErrUnexpectedResponse below.

func AsAPIError

func AsAPIError(err error) *APIResponseError

AsAPIError unwraps err and returns the underlying *APIResponseError if present, otherwise nil. Shorthand for errors.As that saves callers from managing the target pointer and importing the concrete error type.

type AmbiguousMatchError

type AmbiguousMatchError = client.AmbiguousMatchError

AmbiguousMatchError is returned by Resolve<Resource>ByName methods when multiple resources share the requested name. Matches carries the IDs of all colliding resources so consumers can surface disambiguation options.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client provides typed methods for all Jamf Platform API operations.

func NewClient

func NewClient(baseURL, clientID, clientSecret string, opts ...Option) *Client

NewClient creates a new Jamf Platform API client.

func (*Client) AccessToken

func (c *Client) AccessToken(ctx context.Context) (*oauth2.Token, error)

AccessToken returns a valid OAuth2 token from the client's credentials configuration.

func (*Client) BaseURL

func (c *Client) BaseURL() string

BaseURL returns the base URL configured for the client.

func (*Client) Scope

func (c *Client) Scope() (ScopeKind, string)

Scope reports which kind of scope this client carries and the ID it sends.

Prefer this over reading a single ID: the scope is a three-valued property, and an accessor for one kind cannot express the others. A zero kind with an empty ID means no scope header is sent at all — the organization-scoped case — so a caller switching on the kind should handle it rather than assume a scope is always present.

func (*Client) Transport

func (c *Client) Transport() *client.Transport

Transport returns the underlying transport used by sub-package clients in jamfplatform/. Sub-package constructors (e.g. devices.New) call this to share the authenticated HTTP layer.

func (*Client) ValidateCredentials

func (c *Client) ValidateCredentials(ctx context.Context) error

ValidateCredentials tests authentication by requesting an OAuth token.

type ErrorDetail

type ErrorDetail = client.Error

ErrorDetail is a single structured error entry parsed from an API response body. Consumers receive these via APIResponseError.Details() or APIResponseError.FieldErrors().

type Logger

type Logger interface {
	LogRequest(ctx context.Context, method, url string, body []byte)
	LogResponse(ctx context.Context, statusCode int, headers http.Header, body []byte)
}

type MethodPrivileges

type MethodPrivileges struct {
	// Method is the generated Go method name, e.g. "CreateBuildingV1".
	Method string
	// HTTPMethod is the HTTP verb of the underlying endpoint, e.g. "POST".
	HTTPMethod string
	// Path is the endpoint's resource path relative to the tenant prefix,
	// e.g. "/buildings/{id}".
	Path string
	// Scopes lists the scope kinds the endpoint accepts. A client carries
	// exactly one scope, so a consumer needs a credential whose scope appears
	// here: ScopeTenant sends X-Tenant-Id, ScopeEnvironment sends
	// X-Environment-Id, and ScopeOrganization sends no header at all because
	// the gateway resolves the organization from the access token.
	//
	// Where two kinds are present the endpoint is published at both and the
	// caller picks one — the header must match the credential, and crossing
	// them over is 403 OWNERSHIP_FORBIDDEN even within one customer. So this
	// is an alternatives set, the opposite of Scoped, which is a conjunction.
	//
	// Where ScopesSource is "spec" this is the spec's own x-scope-types,
	// carried through unchanged; otherwise it is a correction this SDK
	// supplies for a spec that understates the gateway or declares no
	// extension at all. Read ScopesSource to tell the two apart. It is never
	// empty either way: a spec that declares no scope with no correction fails
	// generation rather than emitting an empty set a consumer would read as
	// "no scope required".
	//
	// Two caveats a consumer must not lose. A spec-sourced set is what the
	// spec declares, which is not always what the gateway serves: as of GitOps
	// v2082 the six Platform specs declare environment only, while devices,
	// device-groups, declaration-reporting and device-management-action still
	// answer under X-Tenant-Id (wire-verified 2026-09-04). And scope is
	// declared per spec rather than per operation, so every method built from
	// one spec carries the same set. The field is per-method as a precaution
	// against two specs in one package disagreeing — securitycloud did, one of
	// its six specs having been held at a build predating the others'
	// environment declaration, until v2082 was ingested there and closed the
	// gap. Keeping the field per-method means the next such divergence needs
	// no structural change.
	Scopes []ScopeKind
	// ScopesSource names where Scopes came from:
	//
	//   - "spec": the spec root's own x-scope-types extension.
	//   - "config-override": the SDK's own config.scopeTypes, because the
	//     published spec either understates what the gateway serves or
	//     declares no extension at all. One family is this case: the account
	//     trio (licensing, partners, sso) carries no x-scope-types in any
	//     published build and is organization-scoped by gateway
	//     configuration, the routes resolving the organization from the
	//     access token. securitycloud-device-groups was the second until
	//     2026-09-04, when the v2 update handler was fixed, its hold lifted
	//     and the ingested spec began declaring the set the override had been
	//     asserting — at which point the override self-expired, exactly as
	//     designed.
	//
	// A "config-override" entry is therefore an assertion about the gateway,
	// evidenced on the wire, rather than a claim about the ingested artifact —
	// the same distinction Source draws for Scoped. It is never empty: every
	// spec resolves a scope from one of the two sources or generation fails.
	ScopesSource string
	// Scoped lists the GA capability permissions the endpoint requires, in
	// {capability}:{action} form, e.g. "buildings:create". The capability is
	// kebab-case and carries no product name — one capability is reached by
	// endpoints across several products — and the action is one of exactly
	// six, lowercase and case-sensitive: create, read, update, delete,
	// deploy, execute. The three-part beta slug ("create:pro:buildings") is
	// the retired form and never appears here.
	//
	// Where more than one identifier is present, ALL of them are required.
	// The platform has exactly one route that accepts either of two
	// capabilities rather than both — DELETE /proclassic/logflush takes
	// flush-policy-logs:execute or policies:delete — and its specs declare
	// only the first, so no entry in this registry is an alternatives set. A
	// consumer can therefore render a multi-entry Scoped slice as "grant all
	// of these".
	//
	// An empty slice means nothing declares a privilege for the endpoint,
	// which is not the same as none being required. Most such endpoints are
	// genuinely unauthenticated — /v1/jamf-pro-version,
	// /v2/jamf-pro-information, the /v1/notifications list. A consumer
	// rendering a permissions table must not print an empty slice as "no
	// permission needed"; read Source to tell the two apart.
	Scoped []string
	// Source names where Scoped came from:
	//
	//   - "spec": the operation's own x-required-privileges extension.
	//   - "gateway-policy": the published spec declares none and the SDK
	//     supplies what the gateway's own authorization policy enforces. The
	//     account package's 18 methods are this case, and permanently so:
	//     these routes resolve the organization from the access token, which
	//     exempts them from the transform the publishing pipeline attaches
	//     x-required-privileges during, so the artifact ships without them by
	//     construction. The values come from
	//     the spec source repository's own per-team config.yaml
	//     and the hand-written OPA rules in
	//     the gateway's authorization policy for the account namespace,
	//     which agree on all 18.
	//   - "": Scoped is empty.
	//
	// Note for "gateway-policy": several account rules accept *either* the GA
	// capability recorded here or a retired read:org:*/update:org:* permission.
	// Only the GA form is carried, because Scoped is a conjunction and listing
	// the alternative would read as "both required".
	Source string
	// Legacy lists the human-readable Jamf Pro privilege names, e.g.
	// "Create Buildings". It is populated for the Pro API family only —
	// other families do not publish legacy names.
	//
	// Scoped and Legacy are INDEPENDENT SETS, not parallel arrays. Do not
	// match them by position, and do not assume equal length. Both are copied
	// in spec order.
	//
	// Two things make a positional pairing wrong, and each on its own is
	// sufficient (counts are pro at spec 11.31.0, 746 privileged operations):
	//
	//   - The lengths differ on 29 operations, because the GA capability
	//     consolidation mapped several legacy privileges onto one capability.
	//     GET /v1/computer-groups is Scoped ["device-groups:read"] against
	//     Legacy ["Read Smart Computer Groups", "Read Static Computer
	//     Groups"]. There is no bijection to zip.
	//   - Where the lengths do match, the orders still disagree, in the spec
	//     itself. POST /v1/jamf-management-framework/redeploy/{id} is Scoped
	//     ["computer-check-in:read", "device-actions:execute"] against Legacy
	//     ["Send Computer Remote Command to Install Package", "Read Computer
	//     Check-In"] — reversed. 9 of the 24 equal-length multi-privilege
	//     operations are like this.
	//
	// A consumer rendering a table of required privileges therefore has to
	// present the two lists separately, or label from Scoped alone. Jamf does
	// not publish the scoped-to-legacy mapping in the specs; its "Jamf Pro
	// permissions map" documentation article is the only artefact that carries
	// it, and until that is machine-readable a correct label per scoped
	// identifier cannot be derived here.
	Legacy []string
}

MethodPrivileges describes the Jamf API privileges required to call a generated SDK method. It is sourced from the x-required-privileges and x-required-privileges-legacy vendor extensions the Jamf OpenAPI specs attach to each operation.

Each generated sub-package (pro, devices, devicegroups, proclassic, ...) exposes a package-level Privileges map keyed by method name plus a PrivilegesFor helper. Consumers that need to document the permissions a given operation requires — for example a Terraform provider emitting a table of required privileges per resource — look the method up there.

Only methods built directly from a spec operation appear in the registry. Synthetic convenience methods (Resolve<X>ByName, Apply<X>) compose one or more underlying endpoints and are intentionally absent; document the privileges of the operations they call instead.

type Option

type Option func(*clientConfig)

Option configures a Client.

func WithAuthorizationHeaderName

func WithAuthorizationHeaderName(name string) Option

WithAuthorizationHeaderName moves the OAuth2 bearer credential out of the Authorization header into the named header on every API request, leaving Authorization free for a header supplied via WithHeaders.

For callers behind a reverse proxy that consumes Authorization for its own service-account credential and expects Jamf's bearer under a different name. The token exchange is unaffected: the client-credential Basic header x/oauth2 writes there is left in place, since relocating it would leave the token endpoint with nothing to authenticate.

"Authorization" and the scope headers are refused and the refusal logged. Relocating the bearer onto Authorization would delete it, and relocating it onto a scope header would overwrite the scope; both fail with a status — 401 and 403 OWNERSHIP_FORBIDDEN — that names something else as the cause.

func WithEnvironmentID

func WithEnvironmentID(id string) Option

WithEnvironmentID configures the platform environment this client is scoped to. It is sent as the X-Environment-Id request header on every API call.

This is the scope to prefer. An environment groups a customer's tenants, and it is what Jamf intends new integrations to be created with; WithTenantID is the legacy alternative. The Platform API GA invalidates every public-beta credential, so integrations have to be re-created regardless — which makes it the moment to create them environment-scoped rather than a migration to schedule later.

The header must match the credential. An integration is minted against one scope, and crossing over is refused with 403 OWNERSHIP_FORBIDDEN even when both IDs belong to the same customer, so this is a choice between two integrations rather than two IDs for one.

The two scopes are mutually exclusive: a client carries exactly one, and exactly one scope header is ever sent. Setting both this and WithTenantID is a configuration mistake rather than a combination — **environment takes precedence**, whichever order the options are passed in, because a client built from an environment-scoped credential cannot use a tenant header anyway. Callers that can validate their own input should reject the pair up front instead of relying on that precedence.

func WithFileCookieJar

func WithFileCookieJar(dir string) Option

WithFileCookieJar enables file-based cookie jar persistence in the given directory. The cookie jar survives across process invocations so sticky-session cookies keep pointing a CLI-style caller at the same app node between runs.

func WithFileTokenCache

func WithFileTokenCache(dir string) Option

WithFileTokenCache enables file-based token caching in the given directory.

func WithHTTPClient

func WithHTTPClient(httpClient *http.Client) Option

WithHTTPClient overrides the default HTTP client.

func WithHeaders

func WithHeaders(h http.Header) Option

WithHeaders sets additional HTTP headers sent on every request the client makes, including the OAuth2 token exchange. Calling it more than once merges; a repeated header name takes the last value given.

For callers whose traffic is fronted by a reverse proxy needing headers of its own. Prefer this over WithHTTPClient for that purpose — supplying a client replaces the SDK's tuned transport and drops proxy-from-environment support, the per-phase timeouts, the connection-pool ceiling matched to Terraform's default parallelism, and the write buffer package upload depends on. This layers onto that transport, and composes with WithHTTPClient when both are genuinely needed.

The scope headers (X-Tenant-Id, X-Environment-Id) are rejected and logged; set the scope with WithTenantID or WithEnvironmentID. User-Agent set here is overridden by WithUserAgent. Cookie is allowed but replaces rather than merges, so it displaces the sticky-session cookie Jamf Cloud uses to pin a client to one app node.

func WithLogger

func WithLogger(logger Logger) Option

WithLogger sets a logger for HTTP request/response logging.

The SDK logs nothing until you install a Logger, and it redacts nothing it hands over. LogRequest receives the raw request body; LogResponse receives the raw response body and headers. Request bodies carry whatever secrets a write sends: ClientSecret, AdminPassword, KeystorePassword, the escaped plist in a configuration profile's Payloads. A Logger that renders a body verbatim therefore writes those in plaintext wherever it writes, which is often a support ticket or a CI log. Redact inside the Logger, or log only the method, URL and status.

LogRequest never sees the bearer token or the client credential: the SDK passes it no headers at all, and the OAuth2 token exchange runs on its own http.Client outside the logged path.

LogResponse is different. It receives the response http.Header unfiltered, so filter headers there rather than rendering them wholesale — the SDK's own acceptance tracer prints response headers from a fixed allowlist for exactly this reason, so that a header carrying credential material cannot reach a trace by accident and a header added upstream later cannot either.

func WithMinRequestInterval

func WithMinRequestInterval(d time.Duration) Option

WithMinRequestInterval sets the minimum wall-clock time between the start of consecutive outbound HTTP requests, paced across the shared transport that the SDK fans out over parallel goroutines. It gives the server breathing room and reduces 429s. A value <= 0 disables the gate. When this option is not supplied, the SDK applies a 100ms default.

func WithRetryPolicy

func WithRetryPolicy(waitMin, waitMax time.Duration, maxRetries int) Option

WithRetryPolicy overrides the SDK's automatic retry timing for transient failures (429, 503, and 500/502/504 on GET/DELETE/PUT/HEAD — see isRetryableWriteStatus's godoc in the SDK's internal transport for the exact policy).

waitMin seeds an exponential backoff and waitMax caps it; maxRetries follows retryablehttp's own semantics, so total attempts per request = maxRetries+1 and 0 disables automatic retrying entirely. The production default is waitMin 1s, waitMax 60s, maxRetries 4 — bounding a full retry sequence at 1+2+4+8 = 15s of waiting.

Two distinct uses, both supported:

  • Tests. A unit test mocking a persistently-failing transient status (e.g. an always-500 GET, to exercise a caller's error-handling path) otherwise waits out the full production backoff on every run. Pass a few milliseconds and a low maxRetries.
  • Interactive callers. A CLI generally wants a tighter bound than the default so a transient failure surfaces promptly rather than looking like a hang — e.g. WithRetryPolicy(200*time.Millisecond, 2*time.Second, 2).

To bound total time rather than retry timing, prefer a context deadline: it covers the whole call including every retry attempt and the waits between them, and needs no policy change.

func WithTenantID

func WithTenantID(id string) Option

WithTenantID configures the tenant this client is scoped to. It is sent as the X-Tenant-Id request header on every API call.

Tenant scoping is the legacy form. Prefer WithEnvironmentID: an environment groups a customer's tenants, and it is the scope Jamf intends integrations to be created with. Tenant scope remains supported — a tenant is a single product, and some surfaces are only reachable that way — but new integrations should not choose it by default.

Mutually exclusive with WithEnvironmentID. If both are set, environment wins regardless of the order they are passed in; see WithEnvironmentID.

The gateway used to take the tenant from the URL path (/api/{namespace}/{version}/tenant/{tenantID}); it moved to a header at the Platform API GA. When neither scope option is set, no scope header is sent and the gateway resolves the context from the access token instead.

func WithTokenCache

func WithTokenCache(cache TokenCache) Option

WithTokenCache sets a custom token cache for persisting tokens across process restarts.

func WithUserAgent

func WithUserAgent(userAgent string) Option

WithUserAgent sets a custom user agent string.

type RSQLClause

type RSQLClause = client.RSQLClause

type ScopeKind

type ScopeKind = client.ScopeKind

ScopeKind identifies which kind of Jamf scope a client is bound to. It is an alias for the transport's type, so a consumer can name it, switch on it and call ScopeHeader on it without importing internal/client — which is what made the kind unreachable before.

type TokenCache

type TokenCache interface {
	Load(key string) (token string, expiresAt time.Time, ok bool)
	Store(key string, token string, expiresAt time.Time) error
}

Directories

Path Synopsis
Package account provides typed access to Jamf Platform account API endpoints.
Package account provides typed access to Jamf Platform account API endpoints.
Package aigovernance provides typed access to Jamf Platform aigovernance API endpoints.
Package aigovernance provides typed access to Jamf Platform aigovernance API endpoints.
Package audit provides typed access to Jamf Platform audit API endpoints.
Package audit provides typed access to Jamf Platform audit API endpoints.
Package blueprints provides typed access to Jamf Platform blueprints API endpoints.
Package blueprints provides typed access to Jamf Platform blueprints API endpoints.
Package compliancebenchmarks provides typed access to Jamf Platform compliancebenchmarks API endpoints.
Package compliancebenchmarks provides typed access to Jamf Platform compliancebenchmarks API endpoints.
Package ddmreport provides typed access to Jamf Platform ddmreport API endpoints.
Package ddmreport provides typed access to Jamf Platform ddmreport API endpoints.
Package deviceactions provides typed access to Jamf Platform deviceactions API endpoints.
Package deviceactions provides typed access to Jamf Platform deviceactions API endpoints.
Package devicegroups provides typed access to Jamf Platform devicegroups API endpoints.
Package devicegroups provides typed access to Jamf Platform devicegroups API endpoints.
Package devices provides typed access to Jamf Platform devices API endpoints.
Package devices provides typed access to Jamf Platform devices API endpoints.
Package pro provides typed access to Jamf Platform pro API endpoints.
Package pro provides typed access to Jamf Platform pro API endpoints.
Package proclassic provides typed access to Jamf Platform proclassic API endpoints.
Package proclassic provides typed access to Jamf Platform proclassic API endpoints.
Package securitycloud provides typed access to Jamf Platform securitycloud API endpoints.
Package securitycloud provides typed access to Jamf Platform securitycloud API endpoints.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL