Split employee devices from DeviceConnection
viewer.enrolledDevices shared DeviceConnection with the admin org list, so totalCount had to authorize with both employee-device:list and device:list. Mirror EmployeeDocumentConnection: a dedicated EmployeeDeviceConnection without totalCount, and keep DeviceConnection.totalCount for the org fleet only. Cover assumed-session device:get IDOR in e2e. Signed-off-by: Ludovic Vielle <ludovic@probo.com>
This commit is contained in:
@@ -528,6 +528,22 @@ func TestDeviceEnrollment(t *testing.T) {
|
|||||||
require.Equal(t, "PENDING", result.Node.State)
|
require.Equal(t, "PENDING", result.Node.State)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("employee cannot read another users device via node", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
owner, _, _, _, orgID, _ := setupDeviceEnrollmentClients(t)
|
||||||
|
|
||||||
|
employeeA := testutil.NewClientInOrg(t, testutil.RoleEmployee, owner)
|
||||||
|
employeeB := testutil.NewClientInOrg(t, testutil.RoleEmployee, owner)
|
||||||
|
|
||||||
|
enrolledB := enrollDevice(t, employeeB, orgID)
|
||||||
|
|
||||||
|
_, err := employeeA.Do(getDeviceQuery, map[string]any{
|
||||||
|
"id": enrolledB.EnrollDevice.Device.ID,
|
||||||
|
})
|
||||||
|
testutil.RequireForbiddenError(t, err, "employee cannot read another users device via node")
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("employee cannot list org devices", func(t *testing.T) {
|
t.Run("employee cannot list org devices", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ var ViewerPolicy = policy.NewPolicy(
|
|||||||
).WithDescription("Read-only ITAM access for organization viewers")
|
).WithDescription("Read-only ITAM access for organization viewers")
|
||||||
|
|
||||||
// EmployeePolicy grants self-enrollment access to organization employees.
|
// EmployeePolicy grants self-enrollment access to organization employees.
|
||||||
|
// Employee-device list/get mirror core:employee-document:*: a dedicated
|
||||||
|
// surface so employees can read their own devices without itam:device:list.
|
||||||
var EmployeePolicy = policy.NewPolicy(
|
var EmployeePolicy = policy.NewPolicy(
|
||||||
"itam:employee",
|
"itam:employee",
|
||||||
"ITAM Employee",
|
"ITAM Employee",
|
||||||
|
|||||||
@@ -66,21 +66,6 @@ func (r *deviceResolver) LatestPostures(ctx context.Context, obj *types.Device)
|
|||||||
|
|
||||||
// TotalCount is the resolver for the DeviceConnection.totalCount field.
|
// TotalCount is the resolver for the DeviceConnection.totalCount field.
|
||||||
func (r *deviceConnectionResolver) TotalCount(ctx context.Context, obj *types.DeviceConnection) (int, error) {
|
func (r *deviceConnectionResolver) TotalCount(ctx context.Context, obj *types.DeviceConnection) (int, error) {
|
||||||
if obj.OwnerID != nil {
|
|
||||||
scope, err := r.authorize(ctx, obj.ParentID, itam.ActionEmployeeDeviceList)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
count, err := r.itam.CountForOrganizationIDAndOwnerID(ctx, scope, obj.ParentID, *obj.OwnerID)
|
|
||||||
if err != nil {
|
|
||||||
r.logger.ErrorCtx(ctx, "cannot count devices by owner", log.Error(err))
|
|
||||||
return 0, gqlutils.Internal(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
return count, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
scope, err := r.authorize(ctx, obj.ParentID, itam.ActionDeviceList)
|
scope, err := r.authorize(ctx, obj.ParentID, itam.ActionDeviceList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
|
|||||||
@@ -119,6 +119,22 @@ type DeviceEdge {
|
|||||||
node: Device!
|
node: Device!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EmployeeDeviceConnection
|
||||||
|
@goModel(
|
||||||
|
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.EmployeeDeviceConnection"
|
||||||
|
) {
|
||||||
|
edges: [EmployeeDeviceEdge!]!
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmployeeDeviceEdge
|
||||||
|
@goModel(
|
||||||
|
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.EmployeeDeviceEdge"
|
||||||
|
) {
|
||||||
|
cursor: CursorKey!
|
||||||
|
node: Device!
|
||||||
|
}
|
||||||
|
|
||||||
type CreateDevicePayload {
|
type CreateDevicePayload {
|
||||||
device: Device!
|
device: Device!
|
||||||
# enrollmentToken is shown ONCE; exchange via agent REST /enroll.
|
# enrollmentToken is shown ONCE; exchange via agent REST /enroll.
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ type Viewer {
|
|||||||
last: Int
|
last: Int
|
||||||
before: CursorKey
|
before: CursorKey
|
||||||
orderBy: DeviceOrder
|
orderBy: DeviceOrder
|
||||||
): DeviceConnection! @goField(forceResolver: true)
|
): EmployeeDeviceConnection! @goField(forceResolver: true)
|
||||||
|
|
||||||
# Own-device read for self-enrollment status polling before org assumption.
|
# Own-device read for self-enrollment status polling before org assumption.
|
||||||
enrolledDevice(id: ID!): Device @goField(forceResolver: true)
|
enrolledDevice(id: ID!): Device @goField(forceResolver: true)
|
||||||
|
|||||||
@@ -36,7 +36,16 @@ type (
|
|||||||
|
|
||||||
Resolver any
|
Resolver any
|
||||||
ParentID gid.GID
|
ParentID gid.GID
|
||||||
OwnerID *gid.GID
|
}
|
||||||
|
|
||||||
|
EmployeeDeviceConnection struct {
|
||||||
|
Edges []*EmployeeDeviceEdge
|
||||||
|
PageInfo *PageInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
EmployeeDeviceEdge struct {
|
||||||
|
Cursor page.CursorKey
|
||||||
|
Node *Device
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -58,16 +67,18 @@ func NewDeviceConnection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOwnedDeviceConnection(
|
func NewEmployeeDeviceConnection(
|
||||||
p *page.Page[*coredata.Device, coredata.DeviceOrderField],
|
p *page.Page[*coredata.Device, coredata.DeviceOrderField],
|
||||||
parentType any,
|
) *EmployeeDeviceConnection {
|
||||||
parentID gid.GID,
|
edges := make([]*EmployeeDeviceEdge, len(p.Data))
|
||||||
ownerID gid.GID,
|
for i := range edges {
|
||||||
) *DeviceConnection {
|
edges[i] = NewEmployeeDeviceEdge(p.Data[i], p.Cursor.OrderBy.Field)
|
||||||
conn := NewDeviceConnection(p, parentType, parentID)
|
}
|
||||||
conn.OwnerID = &ownerID
|
|
||||||
|
|
||||||
return conn
|
return &EmployeeDeviceConnection{
|
||||||
|
Edges: edges,
|
||||||
|
PageInfo: NewPageInfo(p),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDeviceEdge(d *coredata.Device, orderBy coredata.DeviceOrderField) *DeviceEdge {
|
func NewDeviceEdge(d *coredata.Device, orderBy coredata.DeviceOrderField) *DeviceEdge {
|
||||||
@@ -77,6 +88,16 @@ func NewDeviceEdge(d *coredata.Device, orderBy coredata.DeviceOrderField) *Devic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewEmployeeDeviceEdge(
|
||||||
|
d *coredata.Device,
|
||||||
|
orderBy coredata.DeviceOrderField,
|
||||||
|
) *EmployeeDeviceEdge {
|
||||||
|
return &EmployeeDeviceEdge{
|
||||||
|
Cursor: d.CursorKey(orderBy),
|
||||||
|
Node: NewDevice(d),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func NewDevice(d *coredata.Device) *Device {
|
func NewDevice(d *coredata.Device) *Device {
|
||||||
device := &Device{
|
device := &Device{
|
||||||
ID: d.ID,
|
ID: d.ID,
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ func (r *viewerResolver) ApprovableDocument(ctx context.Context, obj *types.View
|
|||||||
}
|
}
|
||||||
|
|
||||||
// EnrolledDevices is the resolver for the enrolledDevices field.
|
// EnrolledDevices is the resolver for the enrolledDevices field.
|
||||||
func (r *viewerResolver) EnrolledDevices(ctx context.Context, obj *types.Viewer, organizationID gid.GID, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DeviceOrderBy) (*types.DeviceConnection, error) {
|
func (r *viewerResolver) EnrolledDevices(ctx context.Context, obj *types.Viewer, organizationID gid.GID, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DeviceOrderBy) (*types.EmployeeDeviceConnection, error) {
|
||||||
scope, err := r.authorize(ctx, organizationID, itam.ActionEmployeeDeviceList)
|
scope, err := r.authorize(ctx, organizationID, itam.ActionEmployeeDeviceList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -228,7 +228,7 @@ func (r *viewerResolver) EnrolledDevices(ctx context.Context, obj *types.Viewer,
|
|||||||
return nil, gqlutils.Internal(ctx)
|
return nil, gqlutils.Internal(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.NewOwnedDeviceConnection(devicesPage, r, organizationID, profile.ID), nil
|
return types.NewEmployeeDeviceConnection(devicesPage), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnrolledDevice is the resolver for the enrolledDevice field.
|
// EnrolledDevice is the resolver for the enrolledDevice field.
|
||||||
|
|||||||
Reference in New Issue
Block a user