Add pre-assume enrolled device status query

The /enroll wait UI polled device state via node(), which
requires an assumed org session, so confirmation never
succeeded for unassumed viewers. Expose viewer.enrolledDevice
behind itam:employee-device:get (own-device, skip assumption)
and point the poller at it.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
This commit is contained in:
Ludovic Vielle
2026-07-17 16:54:57 +02:00
parent afe0c84881
commit 238c19d509
8 changed files with 140 additions and 6 deletions

View File

@@ -101,6 +101,8 @@ func (r *deviceConnectionResolver) TotalCount(ctx context.Context, obj *types.De
}
// EnrollDevice is the resolver for the enrollDevice field.
// SkipAssumptionCheck: self-enrollment from /enroll runs before the viewer
// assumes the target organization.
func (r *mutationResolver) EnrollDevice(ctx context.Context, input types.EnrollDeviceInput) (*types.CreateDevicePayload, error) {
identity := authn.IdentityFromContext(ctx)

View File

@@ -31,4 +31,7 @@ type Viewer {
before: CursorKey
orderBy: DeviceOrder
): DeviceConnection! @goField(forceResolver: true)
# Own-device read for self-enrollment status polling before org assumption.
enrolledDevice(id: ID!): Device @goField(forceResolver: true)
}

View File

@@ -17,6 +17,7 @@ import (
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/server/api/authn"
"go.probo.inc/probo/pkg/server/api/authz"
"go.probo.inc/probo/pkg/server/api/console/v1/schema"
"go.probo.inc/probo/pkg/server/api/console/v1/types"
"go.probo.inc/probo/pkg/server/gqlutils"
@@ -230,6 +231,36 @@ func (r *viewerResolver) EnrolledDevices(ctx context.Context, obj *types.Viewer,
return types.NewOwnedDeviceConnection(devicesPage, r, organizationID, profile.ID), nil
}
// EnrolledDevice is the resolver for the enrolledDevice field.
func (r *viewerResolver) EnrolledDevice(ctx context.Context, obj *types.Viewer, id gid.GID) (*types.Device, error) {
scope, err := r.authorize(
ctx,
id,
itam.ActionEmployeeDeviceGet,
authz.WithSkipAssumptionCheck(),
)
if err != nil {
if gqlutils.IsForbidden(err) {
return nil, gqlutils.NotFoundf(ctx, "resource not found")
}
return nil, err
}
device, err := r.itam.GetDevice(ctx, scope, id)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot get enrolled device", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewDevice(device), nil
}
// Viewer returns schema.ViewerResolver implementation.
func (r *Resolver) Viewer() schema.ViewerResolver { return &viewerResolver{r} }

View File

@@ -120,6 +120,22 @@ func Forbiddenf(ctx context.Context, format string, a ...any) *gqlerror.Error {
return Forbidden(ctx, fmt.Errorf(format, a...))
}
// IsForbidden reports whether err is a GraphQL error with code FORBIDDEN.
func IsForbidden(err error) bool {
return hasCode(err, "FORBIDDEN")
}
func hasCode(err error, code string) bool {
gqlErr, ok := errors.AsType[*gqlerror.Error](err)
if !ok {
return false
}
got, _ := gqlErr.Extensions["code"].(string)
return got == code
}
func NotFound(ctx context.Context, err error) *gqlerror.Error {
return &gqlerror.Error{
Message: err.Error(),