scope

package
v1.30.0 Latest Latest
Warning

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

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

Documentation

Overview

Package scope provides scope manipulation for Jamf Classic API resources. It handles reading, modifying, and writing scope sections (targets, limitations, exclusions) on policies, configuration profiles, and other scopeable resources.

Index

Constants

View Source
const (
	SectionTarget     = "target"
	SectionLimitation = "limitation"
	SectionExclusion  = "exclusion"
)

Scope sections. The Classic API models a scope as one set of targets plus two narrowing sets, mirroring the admin UI's Targets / Limitations / Exclusions tabs.

View Source
const AnnotationCategories = "jamf:scope-categories"

AnnotationCategories carries a scope command's own item-flag vocabulary, so the root's unknown-flag handler can name the categories THIS resource has.

Registering only the resource's own flags is what makes --help and shell completion honest, but it costs the explanatory refusal: cobra rejects an unregistered flag as "unknown flag: --computer-group" before RunE and the matrix never sees it. That is accurate and unhelpful on exactly the mistake the matrix exists to explain — a category that belongs to another device family — so the vocabulary travels on an annotation rather than being re-derived in root.go, which has no access to the resource.

Variables

Sections is the ordered section vocabulary, shared by --section's help text and its shell completion so the two cannot disagree.

Functions

func AddScopeFlags

func AddScopeFlags(cmd *cobra.Command, res Resource, section *string)

AddScopeFlags registers --section and the item flags this resource accepts.

Registering only the resource's own categories is what makes `--help` and shell completion honest: a mobile configuration profile no longer offers --computer-group, and restricted software no longer offers a limitations section it has no tab for.

func AddToScope

func AddToScope(s *ScopeXML, section, flagName, name string) bool

AddToScope adds a named item to the given scope section. Returns true if the item was added, false if already present (idempotent no-op).

singularKey is no longer read: the policy-only <limit_to_users> branch this used to carry is gone, the server keeping that element in step with <limitations><user_groups> by itself (see ScopeXML).

func CheckAllFlagConflict added in v1.30.0

func CheckAllFlagConflict(s *ScopeXML, singularKey, section, flagName string) error

CheckAllFlagConflict refuses a target that the resource's currently-set all-flag makes unreachable. The server accepts such a write with 200 and drops the member, so without this the only signal is the post-write verification failing with a message about the resource type.

func FlattenScope

func FlattenScope(s *ScopeXML) []map[string]any

FlattenScope converts a ScopeXML into a flat list of rows for table output.

Every category the XML model carries is emitted, so what `scope get` shows is exactly what `scope add`/`remove` can address. The two that used to be missing were the ones a caller could see in the admin UI and not here: iBeacon limitations and exclusions, and target classes.

func NewScopeCmd

func NewScopeCmd(ctx *registry.CLIContext, res Resource) *cobra.Command

NewScopeCmd creates the "scope" subcommand group with get, add, and remove subcommands for the given Classic API resource.

Each leaf takes `[<id>]` plus `--name`, matching every other command in this CLI. It used to take the name as a bare positional — the only place in the binary that did — which meant a caller who had an ID in hand from `list` had to go and find the name for it, and a name that looks like an ID could not be addressed at all.

func OutputScope

func OutputScope(out registry.OutputFormatter, s *ScopeXML, format string) error

OutputScope writes the scope to the output formatter. The column formats get the scope flattened into rows; json, yaml, ndjson, xml and raw get the full structure.

The keep-set is named and the flattened shape is the default, rather than the other way round, because the format string is not normalised: this used to match "table", "csv" and "plain" exactly, so any other value — a mis-cased -o Table, or the internal json-multi that means JSON on the wire and a table on the screen — took the nested structure to a table renderer. See output.RendersStructureVerbatim.

func PutScope

func PutScope(ctx context.Context, client registry.HTTPClient, res Resource, id string, s *ScopeXML) error

PutScope writes an updated scope back to the Classic API, sending a body that carries nothing but the <scope> element.

Two properties of the Classic API make this the right shape, both wire-checked 2026-09-12 against Jamf Pro 11.31.1 on a direct instance and through the platform gateway:

  • A PUT is a partial update at top-level-section granularity: a body of just <scope> applies the scope and leaves general, self_service, packages, payloads and every other section byte-identical.
  • <scope> itself is replaced wholesale. A body carrying only some scope categories wipes the rest, so the whole block has to be sent — which is what the caller's ScopeXML, read from the server and edited in place, already is. An empty category element is what clears a category.

The body is marshalled by encoding/xml rather than spliced into the document the server returned, and the element ORDER that produces is load-bearing. The Classic API's XML binding is sequence-ordered: it reads scope children in schema order and silently ignores what arrives out of order, answering 200 either way. The order a GET returns is not that order — macapplications, for one, answers <exclusions> as buildings, departments, mobile_device_groups, users, ... — so echoing the server's own bytes back is accepted and applied to nothing. ScopeXML's field order is the schema order; keep it that way.

The /subset/Scope shortcut is not used: the Jamf Platform Gateway's Classic proxy forwards only top-level Classic paths, so it answers 403 there while working on a direct instance (re-probed 2026-09-12). One code path that works on both beats two that disagree. CLASS TARGETS NEED TWO WRITES. Jamf Pro's Classic API stores <classes> only while the stored category is EMPTY. A write made while it already holds a member clears it — carrying the identical value or omitting it both clear it, and the child's identifier shape makes no difference (wire-checked 2026-09-12 on Jamf Pro 11.31.1, 5/5 each way). Since a scope PUT replaces <scope> wholesale, no single request can preserve an existing class across any other scope change.

So a scope with class members is delivered in two requests: the first carries every intended change with <classes> emptied, leaving the category empty; the second carries the same scope with the classes populated, which the server now accepts. Verified 5/5 — a department added to an ebook that already had a class ends up with both.

Three properties make that safe rather than clever:

  • It is never worse than one request. The first PUT already carries the caller's real change, so an interruption between the two leaves exactly what a single PUT would have left: the change applied, the classes gone.
  • It is keyed on the CATEGORY, not the resource. Only ebooks carry <classes> today, but the rule is a property of the element, so a resource that gains one is handled with no edit here.
  • A scope with no class members takes the single-request path, so the common case is unchanged and the extra write happens only where it is the difference between working and silently losing data.

func RemoveFromScope

func RemoveFromScope(s *ScopeXML, section, flagName, name string) bool

RemoveFromScope removes a named item from the given scope section. Returns true if removed, false if not found (idempotent no-op).

func ScopeFlagsFor added in v1.30.0

func ScopeFlagsFor(singularKey string) []string

ScopeFlagsFor returns the flags a resource accepts anywhere, so a command registers only the flags its own resource can use. Sorted for stable help.

func SectionsFor added in v1.30.0

func SectionsFor(singularKey string) []string

SectionsFor returns the sections a resource actually has, so --section's help and completion do not offer a limitations tab to restricted software.

func ValidateScopeCombination

func ValidateScopeCombination(singularKey, section, flagName string) error

ValidateScopeCombination checks that a section/flag pair is one the resource actually carries, refusing before anything is sent.

The refusal names the flags that would have worked for this resource and section, because the two axes a caller gets wrong are "wrong section for a valid category" (--computer-group as a limitation) and "wrong category for this resource type" (--computer-group on a mobile profile), and a message listing one global vocabulary cannot distinguish them.

func VerifyScopeWrite added in v1.30.0

func VerifyScopeWrite(ctx context.Context, client registry.HTTPClient, res Resource, id string, sent *ScopeXML, touched ScopeTarget, touchedSection string, expectPresent bool) error

VerifyScopeWrite re-reads a resource's scope after a write and reports every member that was sent and did not come back.

It compares the WHOLE scope rather than only the item the command changed, which the single-item check it replaced could not do — and collateral loss is real, not hypothetical. An ebook's <classes> target can only be set while it is empty: once populated, any later scope write clears it, whether the body carries the identical value or omits it (wire-checked 2026-09-12, 5/5 each way). So `scope add --building` on an ebook that has a class destroys the class, and a check scoped to --building sees a clean write.

Because <scope> is replaced wholesale, that shape of loss can appear on any category the server decides not to store, so the guard is general rather than a rule about ebooks. `touched` is named separately only so the message can lead with the member the caller asked about.

Types

type ExclusionsXML

type ExclusionsXML struct {
	Computers          ScopeItemSlice `xml:"computers" json:"computers"`
	ComputerGroups     ScopeItemSlice `xml:"computer_groups" json:"computer_groups"`
	MobileDevices      ScopeItemSlice `xml:"mobile_devices" json:"mobile_devices"`
	MobileDeviceGroups ScopeItemSlice `xml:"mobile_device_groups" json:"mobile_device_groups"`
	Users              ScopeItemSlice `xml:"users" json:"users"`
	UserGroups         ScopeItemSlice `xml:"user_groups" json:"user_groups"`
	NetworkSegments    ScopeItemSlice `xml:"network_segments" json:"network_segments"`
	Buildings          ScopeItemSlice `xml:"buildings" json:"buildings"`
	Departments        ScopeItemSlice `xml:"departments" json:"departments"`
	JSSUsers           ScopeItemSlice `xml:"jss_users" json:"jss_users"`
	JSSUserGroups      ScopeItemSlice `xml:"jss_user_groups" json:"jss_user_groups"`
	IBeacons           ScopeItemSlice `xml:"ibeacons" json:"ibeacons"`
}

ExclusionsXML models the <exclusions> section.

type LimitationsXML

type LimitationsXML struct {
	Users           ScopeItemSlice `xml:"users" json:"users"`
	UserGroups      ScopeItemSlice `xml:"user_groups" json:"user_groups"`
	NetworkSegments ScopeItemSlice `xml:"network_segments" json:"network_segments"`
	IBeacons        ScopeItemSlice `xml:"ibeacons" json:"ibeacons"`
}

LimitationsXML models the <limitations> section.

There is no computer_groups field: no scopeable resource returns one in its limitations block (a computer group narrows nothing — it is a target), and the field this struct used to carry was write-only noise nothing populated and nothing read.

type NamedItem

type NamedItem struct {
	ID   string `xml:"id,omitempty" json:"id,omitempty"`
	Name string `xml:"name" json:"name"`
	UDID string `xml:"udid,omitempty" json:"udid,omitempty"`
}

NamedItem is an item identified by name (and optionally ID or UDID) in scope XML. ID is a string to accommodate both integer IDs (most resources) and UUID IDs (e.g. ebook scope user groups) returned by the Classic API. UDID is populated for individual mobile devices and computers.

type Ref added in v1.30.0

type Ref struct {
	ID   string
	Name string
}

Ref identifies which object a scope command addresses: exactly one of an ID or a name, matching every other Classic command's `<id>` positional plus `--name` flag.

func NewRef added in v1.30.0

func NewRef(args []string, flagName string) (Ref, error)

NewRef builds a Ref from a scope command's positional and --name flag, refusing the two together and the two absent.

Rejecting `<id>` and `--name` together is the Platform convention (see CLAUDE.md, Identifier convention) and matters more here than elsewhere: there is no correct resolution when they disagree, and preferring one silently would mutate the scope of an object the caller did not name.

func (Ref) String added in v1.30.0

func (r Ref) String() string

String renders the reference for an error message, so a failure names what the caller actually typed rather than always saying "name".

type Resource

type Resource struct {
	APIPath     string // URL segment under /JSSResource/, e.g. "policies"
	SingularKey string // XML root key for a single object, e.g. "policy"

	// CLIName is the command name this resource ships under, e.g.
	// "classic-policies". It exists so --help examples are invocations a
	// caller can paste rather than fragments starting at "scope add".
	CLIName string

	// ResolveByList resolves name→ID by listing the collection, for the two
	// resources with no /name/ endpoint.
	ResolveByList bool
}

Resource identifies a Classic API resource that supports scope operations.

type ScopeDrop added in v1.30.0

type ScopeDrop struct {
	Section  string   // target, limitation or exclusion
	Category string   // the CLI flag name, e.g. "class"
	Missing  []string // the members that went in and did not come back
}

ScopeDrop records one scope category whose members the server did not store.

func DiffScope added in v1.30.0

func DiffScope(sent, got *ScopeXML, touchedSection string, touched ScopeTarget) []ScopeDrop

DiffScope returns every category where a member present in sent is absent from got. The touched member is excluded, since its own absence is reported with a more specific diagnosis.

type ScopeItemSlice

type ScopeItemSlice struct {
	Items    []NamedItem
	ElemName string
}

ScopeItemSlice is a list of NamedItem elements under a single XML parent. The child element name (e.g. "computer_group") is learned during unmarshal and reused during marshal. For newly-created lists it falls back to the parent element name with trailing "s" stripped.

func (ScopeItemSlice) MarshalJSON

func (s ScopeItemSlice) MarshalJSON() ([]byte, error)

func (ScopeItemSlice) MarshalXML

func (s ScopeItemSlice) MarshalXML(e *xml.Encoder, start xml.StartElement) error

func (*ScopeItemSlice) UnmarshalXML

func (s *ScopeItemSlice) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error

type ScopeTarget

type ScopeTarget struct {
	FlagName string
	Name     string
}

ScopeTarget holds a resolved flag name and value from a scope add/remove command.

func DetermineScopeTarget

func DetermineScopeTarget(cmd *cobra.Command, res Resource) (ScopeTarget, error)

DetermineScopeTarget inspects the command's flags to find exactly one scope item flag that was set. Returns an error if zero or multiple flags are set.

It walks the resource's own flag set rather than the global one, so an unset-but-registered flag from another resource's shape cannot be reported and the "specify one of" list names only what this command has.

type ScopeXML

type ScopeXML struct {
	XMLName            xml.Name        `xml:"scope" json:"-"`
	AllComputers       bool            `xml:"all_computers" json:"all_computers"`
	AllMobileDevices   bool            `xml:"all_mobile_devices,omitempty" json:"all_mobile_devices,omitempty"`
	AllJSSUsers        bool            `xml:"all_jss_users" json:"all_jss_users"`
	Computers          ScopeItemSlice  `xml:"computers" json:"computers"`
	ComputerGroups     ScopeItemSlice  `xml:"computer_groups" json:"computer_groups"`
	MobileDevices      ScopeItemSlice  `xml:"mobile_devices" json:"mobile_devices"`
	MobileDeviceGroups ScopeItemSlice  `xml:"mobile_device_groups" json:"mobile_device_groups"`
	JSSUsers           ScopeItemSlice  `xml:"jss_users" json:"jss_users"`
	JSSUserGroups      ScopeItemSlice  `xml:"jss_user_groups" json:"jss_user_groups"`
	Buildings          ScopeItemSlice  `xml:"buildings" json:"buildings"`
	Departments        ScopeItemSlice  `xml:"departments" json:"departments"`
	Classes            ScopeItemSlice  `xml:"classes" json:"classes"`
	Limitations        *LimitationsXML `xml:"limitations,omitempty" json:"limitations,omitempty"`
	Exclusions         *ExclusionsXML  `xml:"exclusions,omitempty" json:"exclusions,omitempty"`
}

ScopeXML models the complete <scope> section of a Classic API resource.

All scopeable item slices are present unconditionally so that an unmarshal → modify → marshal round-trip preserves every section the server returned. A missing field here would cause CLI scope add/remove to silently wipe data the user set elsewhere (e.g. individual mobile devices added via UI), since a scope PUT replaces the whole <scope> subtree.

FIELD ORDER IS LOAD-BEARING. The Classic API's XML binding reads scope children in schema order and silently ignores whatever arrives out of it, answering 200 either way — so a body assembled in the order a GET happens to return (which differs per resource: macapplications answers <exclusions> as buildings, departments, mobile_device_groups, users, …) is accepted and applied to nothing. This order is the schema order and is what makes PutScope's marshalled body take effect; re-ordering these fields to match any one resource's GET breaks the others. Wire-checked 2026-09-12.

Fields a given resource does not carry are emitted empty and ignored by the server; only a POPULATED foreign category is refused, with a 409 naming it ("Mobile device groups cannot be assigned to an macOS profile"), which is what the matrix in matrix.go refuses client-side.

<limit_to_users> is deliberately absent. The server denormalises <limitations><user_groups> into <limit_to_users><user_groups> on every write and back again on every read, so the two wire paths always carry identical values and modelling both meant a policy-only special case in five functions. Wire-checked 2026-09-12 in both directions: writing only limitations.user_groups populates limit_to_users, writing only limit_to_users populates limitations.user_groups, and an empty limitations.user_groups with limit_to_users omitted clears both.

func FetchScope

func FetchScope(ctx context.Context, client registry.HTTPClient, res Resource, ref Ref) (string, *ScopeXML, error)

FetchScope GETs a Classic API resource and returns its ID and parsed scope.

An ID is one request. A name is one request too for most resources, via the Classic /name/{name} endpoint, whose response carries <general><id> — the ID needed for the subsequent PUT. Only a resource with no /name/ endpoint (res.ResolveByList: the VPP pair) costs two, listing the collection to resolve the name first.

Jump to

Keyboard shortcuts

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