Add ManagedDeviceTrait / TRAIT_MANAGED_DEVICE device asset trait#995
Conversation
Add a new device asset trait for MDM/UEM inventory. A managed-device resource represents a managed endpoint (laptop, desktop, mobile, etc.) enrolled in a device management platform. Like other asset traits it carries no grants or entitlements and rides the resource annotations bag. - proto: add ManagedDeviceTrait + DeviceOS messages to annotation_trait.proto, and TRAIT_MANAGED_DEVICE = 12 (next free value) to ResourceType.Trait. - regenerate pb/ via buf generate. - add pkg/types/resource/managed_device_trait.go with option builders and NewManagedDeviceTrait, mirroring secret_trait.go. - add WithManagedDeviceTrait and NewManagedDeviceResource to resource.go. Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
| } | ||
|
|
||
| // NewManagedDeviceTrait creates a new `ManagedDeviceTrait` with the given options. | ||
| func NewManagedDeviceTrait(opts ...ManagedDeviceTraitOption) (*v2.ManagedDeviceTrait, error) { |
There was a problem hiding this comment.
🟡 Suggestion: No test file accompanies these new helpers. Comparable recent additive traits (license_trait_test.go, vendor_trait_test.go, vendor_agreement_trait_test.go) each have coverage. Consider adding a managed_device_trait_test.go that round-trips a device through NewManagedDeviceResource and re-extracts the ManagedDeviceTrait from the annotations bag, to lock in the annotation-packing behavior downstream connectors will rely on. (confidence: high)
There was a problem hiding this comment.
Addressed in 87bfe5d: added pkg/types/resource/managed_device_trait_test.go. It round-trips a device through NewManagedDeviceResource and re-extracts the ManagedDeviceTrait from the annotations bag, plus covers full-population option setters, tri-state BoolValue nullability (unset stays nil; explicit false is wrapped, not dropped), WithManagedDeviceTrait merge semantics, and the defined_only enum validate rules on the trait enums and DeviceOS. go test ./pkg/types/resource/... green (131 tests).
General PR Review: Add ManagedDeviceTrait / TRAIT_MANAGED_DEVICE device asset traitBlocking Issues: 0 | Suggestions: 0 | Threads Resolved: 0 Review SummaryThe new commit removes the pre-release profile google.protobuf.Struct field (number 14) from the newly-added ManagedDeviceTrait, dropping it from the proto, the regenerated pb.go/opaque/validate output, and the WithManagedDeviceProfile option (plus its now-unused structpb import). Because ManagedDeviceTrait is introduced in this same PR and has never been released, removing field 14 carries no wire- or serialized-state-compatibility risk, and the generated artifacts are consistent with the source change. The full PR diff was scanned for security and correctness (including the dropped generated pb.go paths); no new issues were found. The prior suggestion to add a GetManagedDeviceTrait extractor for API consistency remains open but was already raised and is not re-flagged here. Security IssuesNone found. Correctness IssuesNone found. SuggestionsNone. |
Addresses the CI review suggestion on PR #995: comparable additive traits (license/vendor/vendor_agreement) carry test coverage while the new ManagedDeviceTrait helpers did not. Covers full-population option round-trip, tri-state BoolValue nullability (unset stays nil; explicit false is wrapped, not dropped), the NewManagedDeviceResource -> annotations-bag -> re-extract path that downstream device-inventory connectors depend on, WithManagedDeviceTrait merge semantics, and the defined_only enum validate rules on the trait enums and DeviceOS. Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
| string model = 5; // OCSF device.model | ||
| string vendor = 6; // OCSF device.vendor_name | ||
| DeviceOS os = 7; // OCSF device.os | ||
| connector.v2.ResourceId assigned_user = 8; // cross-link to the synced user |
There was a problem hiding this comment.
Should this be expressed as a grant? A user can have their device ownership revoked, or ownership can be granted to another device. It might also be a many-to-many relationship in some orgs.
There was a problem hiding this comment.
Conceptually form a Managed Device POV -- its not a grant. It's truly an Assignment to a Single user. Not a single Managed Device Provider allows multiple assignments from what i can research
There was a problem hiding this comment.
Reworked per this thread: device→user is now modeled as a grant, not a scalar field. We removed assigned_user from ManagedDeviceTrait; connectors emit an assigned entitlement on the device + a grant to the user principal. This gives revocation and the many-to-many case (e.g. Intune shared devices), and — the deciding factor — it's the only way to link a device to an owner that lives in a separate directory (Entra/Okta): baton's ExternalResourceMatch (email/UPN) resolution is grant-only, so a trait ResourceId field structurally can't reference an unsynced external user. Thanks for pushing on this.
…nagedDeviceTrait Device→user assignment is modeled connector-side as a grant, not a scalar trait field. Remove assigned_user (field 8) from the unreleased ManagedDeviceTrait, its WithManagedDeviceAssignedUser helper, and the round-trip test assertions. Field number 8 is left as a gap (no reservation needed — the trait is unreleased). Regenerated pb Go. Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
…moved assigned_user) Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
| google.protobuf.BoolValue is_personal = 12; | ||
| ManagementState management_state = 13 [(validate.rules).enum = {defined_only: true}]; | ||
| google.protobuf.Timestamp enrolled_at = 14; | ||
| google.protobuf.Struct profile = 15; |
There was a problem hiding this comment.
This is the 7th trait with a profile struct. We really should put this on the resource to prevent duplicate profiles when composing traits (since uplift only pays attention to one of them).
There was a problem hiding this comment.
Good catch — and you're right about the mechanism: I checked pkg/mapper/resource/uplift.go (findAnnotationTraitProfile) and uplift returns the first trait with a non-nil profile, so a resource composing multiple profile-bearing traits does drop the others.
We'd like to keep profile on the trait here for now, for two reasons:
- It's really a separate, broader problem. The right fix is about uplift's one-profile behavior and/or a resource-level profile — which touches all ~7 traits and
v2.Resource(no profile field today). That deserves its own change rather than being back-doored by dropping the field from this one new trait. - The per-trait profile doubles as a namespace. Each trait owns its own profile keys, so composing traits can't collide on key names; a single shared resource profile would force every trait's keys into one namespace.
For managed devices the collision is also low-risk in practice — a device resource is effectively single-trait. So we'll keep it consistent with the existing traits here and track the resource-level-profile / profile-merge fix as its own item. Thanks for pushing on it.
| // managed endpoint (laptop, desktop, mobile, etc.) enrolled in a device | ||
| // management platform. Like other asset traits it carries no grants or | ||
| // entitlements; it rides the resource's annotations bag. | ||
| message ManagedDeviceTrait { |
There was a problem hiding this comment.
Last thing I noticed: The comment should probably updated now that this can have grants. Also a nitpick: Should the trait name be DeviceTrait since it can show both managed & unmanaged devices (eg MANAGEMENT_STATE_UNMANAGED)?
There was a problem hiding this comment.
Two things:
- Doc-comment: agreed — I'll update it to note that device→user assignment is now expressed as a grant (an
assignedentitlement on the device), since the inlineassigned_userfield was removed. - Name: we'll keep
ManagedDeviceTrait. The trait captures a device's posture as reported by a device-management source (MDM/UEM), andMANAGEMENT_STATE_UNMANAGEDis one of those posture states — a device the management provider knows about but that isn't currently enrolled/managed — rather than a device with no management context at all. Keeping it scoped/namespaced to the managed-device domain also matches the product framing (device trust / MDM). So the name stays.
Appreciate the thorough review — genuinely useful pass.
…liance error vs non-compliant) Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
…precated) The per-trait profile Struct is being deprecated across traits; the new ManagedDeviceTrait should not add another. profile was the last field (14), so removal leaves fields 1-13 contiguous. Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Summary
Adds a new device asset trait (
ManagedDeviceTrait+TRAIT_MANAGED_DEVICE) for MDM/UEM inventory. A managed-device resource represents a managed endpoint (laptop, desktop, mobile, tablet, etc.) enrolled in a device management platform. Like the other asset traits (SecretTrait,AgentTrait,NonHumanIdentityTrait) it carries no grants or entitlements and rides the resource's annotations bag.This is a foundational SDK change that upcoming device-inventory connectors will depend on.
Changes
Proto (
proto/c1/connector/v2/)annotation_trait.proto: newManagedDeviceTraitmessage (withDeviceType,ManagementState,Complianceenums) and aDeviceOSmessage (withOsTypeenum). Nullable booleans usegoogle.protobuf.BoolValue;assigned_usercross-links to the synced user viaResourceId, mirroringAgentTrait.identity_resource_id.resource.proto:TRAIT_MANAGED_DEVICE = 12added toResourceType.Trait(next free value; existing values untouched — reserve-not-reuse).Field layout follows the OCSF
devicetaxonomy (type_id,model,vendor_name,os). No last-seen field in v1 (deliberately omitted).Generated (
pb/c1/connector/v2/)*.pb.go/*_protoopaque.pb.go/*.pb.validate.goviabuf generate(not hand-edited).Go helpers (
pkg/types/resource/)managed_device_trait.go:ManagedDeviceTraitOption,With*builders for every field, andNewManagedDeviceTrait(...)— mirrorssecret_trait.go.resource.go:WithManagedDeviceTrait(...)andNewManagedDeviceResource(...)— mirrorWithSecretTrait/NewSecretResource.Verification
buf lint— cleanbuf breaking --against main— clean (additive only)buf generate— reproducible, regenerated files committedgo build ./...— passgo test ./...— passgofmt+golangci-lint run ./pkg/types/resource/...— 0 issuesNotes
Draft — do not merge. Human review + merge intended. Downstream connectors will pin to this branch until it lands on
main.