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)
|
||||
})
|
||||
|
||||
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.Parallel()
|
||||
|
||||
|
||||
@@ -57,6 +57,8 @@ var ViewerPolicy = policy.NewPolicy(
|
||||
).WithDescription("Read-only ITAM access for organization viewers")
|
||||
|
||||
// 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(
|
||||
"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.
|
||||
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)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
||||
@@ -119,6 +119,22 @@ type DeviceEdge {
|
||||
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 {
|
||||
device: Device!
|
||||
# enrollmentToken is shown ONCE; exchange via agent REST /enroll.
|
||||
|
||||
@@ -30,7 +30,7 @@ type Viewer {
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: DeviceOrder
|
||||
): DeviceConnection! @goField(forceResolver: true)
|
||||
): EmployeeDeviceConnection! @goField(forceResolver: true)
|
||||
|
||||
# Own-device read for self-enrollment status polling before org assumption.
|
||||
enrolledDevice(id: ID!): Device @goField(forceResolver: true)
|
||||
|
||||
@@ -36,7 +36,16 @@ type (
|
||||
|
||||
Resolver any
|
||||
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],
|
||||
parentType any,
|
||||
parentID gid.GID,
|
||||
ownerID gid.GID,
|
||||
) *DeviceConnection {
|
||||
conn := NewDeviceConnection(p, parentType, parentID)
|
||||
conn.OwnerID = &ownerID
|
||||
) *EmployeeDeviceConnection {
|
||||
edges := make([]*EmployeeDeviceEdge, len(p.Data))
|
||||
for i := range edges {
|
||||
edges[i] = NewEmployeeDeviceEdge(p.Data[i], p.Cursor.OrderBy.Field)
|
||||
}
|
||||
|
||||
return conn
|
||||
return &EmployeeDeviceConnection{
|
||||
Edges: edges,
|
||||
PageInfo: NewPageInfo(p),
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
device := &Device{
|
||||
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.
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -228,7 +228,7 @@ func (r *viewerResolver) EnrolledDevices(ctx context.Context, obj *types.Viewer,
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user