From 0126666fd288aea4ab2db6ccdcbbb864c6eb0b3f Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Fri, 6 Jun 2025 14:27:43 -0700 Subject: [PATCH] Add controls on organization Signed-off-by: Sacha Al Himdani --- pkg/coredata/control.go | 59 ++++++ pkg/probo/control_service.go | 32 +++ pkg/server/api/console/v1/schema.graphql | 8 + pkg/server/api/console/v1/schema/schema.go | 232 +++++++++++++++++++++ pkg/server/api/console/v1/types/types.go | 1 + pkg/server/api/console/v1/v1_resolver.go | 30 +++ 6 files changed, 362 insertions(+) diff --git a/pkg/coredata/control.go b/pkg/coredata/control.go index 06a5c289a..cb06ed350 100644 --- a/pkg/coredata/control.go +++ b/pkg/coredata/control.go @@ -287,6 +287,65 @@ WHERE return nil } +func (c *Controls) LoadByOrganizationID( + ctx context.Context, + conn pg.Conn, + scope Scoper, + organizationID gid.GID, + cursor *page.Cursor[ControlOrderField], +) error { + q := ` +WITH ctrl AS ( + SELECT + c.id, + c.section_title, + c.framework_id, + c.tenant_id, + c.name, + c.description, + c.created_at, + c.updated_at + FROM + controls c + INNER JOIN + frameworks f ON c.framework_id = f.id + WHERE + f.organization_id = @organization_id +) +SELECT + id, + section_title, + framework_id, + tenant_id, + name, + description, + created_at, + updated_at +FROM + ctrl +WHERE %s + AND %s +` + q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment()) + + args := pgx.NamedArgs{"organization_id": organizationID} + maps.Copy(args, scope.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query controls: %w", err) + } + + controls, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Control]) + if err != nil { + return fmt.Errorf("cannot collect controls: %w", err) + } + + *c = controls + + return nil +} + func (c *Control) LoadByFrameworkIDAndSectionTitle( ctx context.Context, conn pg.Conn, diff --git a/pkg/probo/control_service.go b/pkg/probo/control_service.go index 03efc1d8e..ffcd71b52 100644 --- a/pkg/probo/control_service.go +++ b/pkg/probo/control_service.go @@ -384,6 +384,38 @@ func (s ControlService) ListForFrameworkID( return page.NewPage(controls, cursor), nil } +func (s ControlService) ListForOrganizationID( + ctx context.Context, + organizationID gid.GID, + cursor *page.Cursor[coredata.ControlOrderField], +) (*page.Page[*coredata.Control, coredata.ControlOrderField], error) { + var controls coredata.Controls + organization := &coredata.Organization{} + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := organization.LoadByID(ctx, conn, s.svc.scope, organizationID); err != nil { + return fmt.Errorf("cannot load organization: %w", err) + } + + return controls.LoadByOrganizationID( + ctx, + conn, + s.svc.scope, + organization.ID, + cursor, + ) + }, + ) + + if err != nil { + return nil, fmt.Errorf("cannot list controls: %w", err) + } + + return page.NewPage(controls, cursor), nil +} + func (s ControlService) ListForRiskID( ctx context.Context, riskID gid.GID, diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index 514da8a55..97492071e 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -511,6 +511,14 @@ type Organization implements Node { orderBy: FrameworkOrder ): FrameworkConnection! @goField(forceResolver: true) + controls( + first: Int + after: CursorKey + last: Int + before: CursorKey + orderBy: ControlOrder + ): ControlConnection! @goField(forceResolver: true) + vendors( first: Int after: CursorKey diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index 1b1dd94b9..8186ecc96 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -561,6 +561,7 @@ type ComplexityRoot struct { Organization struct { Assets func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.AssetOrder) int Connectors func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ConnectorOrder) int + Controls func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy) int CreatedAt func(childComplexity int) int Data func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DatumOrder) int Documents func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentOrderBy) int @@ -1014,6 +1015,7 @@ type OrganizationResolver interface { Users(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.UserOrderBy) (*types.UserConnection, error) Connectors(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ConnectorOrder) (*types.ConnectorConnection, error) Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.FrameworkOrderBy) (*types.FrameworkConnection, error) + Controls(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy) (*types.ControlConnection, error) Vendors(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorOrderBy) (*types.VendorConnection, error) Peoples(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.PeopleOrderBy) (*types.PeopleConnection, error) Documents(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentOrderBy) (*types.DocumentConnection, error) @@ -3327,6 +3329,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Organization.Connectors(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.ConnectorOrder)), true + case "Organization.controls": + if e.complexity.Organization.Controls == nil { + break + } + + args, err := ec.field_Organization_controls_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Organization.Controls(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.ControlOrderBy)), true + case "Organization.createdAt": if e.complexity.Organization.CreatedAt == nil { break @@ -5320,6 +5334,14 @@ type Organization implements Node { orderBy: FrameworkOrder ): FrameworkConnection! @goField(forceResolver: true) + controls( + first: Int + after: CursorKey + last: Int + before: CursorKey + orderBy: ControlOrder + ): ControlConnection! @goField(forceResolver: true) + vendors( first: Int after: CursorKey @@ -9841,6 +9863,101 @@ func (ec *executionContext) field_Organization_connectors_argsOrderBy( return zeroVal, nil } +func (ec *executionContext) field_Organization_controls_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Organization_controls_argsFirst(ctx, rawArgs) + if err != nil { + return nil, err + } + args["first"] = arg0 + arg1, err := ec.field_Organization_controls_argsAfter(ctx, rawArgs) + if err != nil { + return nil, err + } + args["after"] = arg1 + arg2, err := ec.field_Organization_controls_argsLast(ctx, rawArgs) + if err != nil { + return nil, err + } + args["last"] = arg2 + arg3, err := ec.field_Organization_controls_argsBefore(ctx, rawArgs) + if err != nil { + return nil, err + } + args["before"] = arg3 + arg4, err := ec.field_Organization_controls_argsOrderBy(ctx, rawArgs) + if err != nil { + return nil, err + } + args["orderBy"] = arg4 + return args, nil +} +func (ec *executionContext) field_Organization_controls_argsFirst( + ctx context.Context, + rawArgs map[string]any, +) (*int, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("first")) + if tmp, ok := rawArgs["first"]; ok { + return ec.unmarshalOInt2ᚖint(ctx, tmp) + } + + var zeroVal *int + return zeroVal, nil +} + +func (ec *executionContext) field_Organization_controls_argsAfter( + ctx context.Context, + rawArgs map[string]any, +) (*page.CursorKey, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) + if tmp, ok := rawArgs["after"]; ok { + return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx, tmp) + } + + var zeroVal *page.CursorKey + return zeroVal, nil +} + +func (ec *executionContext) field_Organization_controls_argsLast( + ctx context.Context, + rawArgs map[string]any, +) (*int, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("last")) + if tmp, ok := rawArgs["last"]; ok { + return ec.unmarshalOInt2ᚖint(ctx, tmp) + } + + var zeroVal *int + return zeroVal, nil +} + +func (ec *executionContext) field_Organization_controls_argsBefore( + ctx context.Context, + rawArgs map[string]any, +) (*page.CursorKey, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) + if tmp, ok := rawArgs["before"]; ok { + return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx, tmp) + } + + var zeroVal *page.CursorKey + return zeroVal, nil +} + +func (ec *executionContext) field_Organization_controls_argsOrderBy( + ctx context.Context, + rawArgs map[string]any, +) (*types.ControlOrderBy, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("orderBy")) + if tmp, ok := rawArgs["orderBy"]; ok { + return ec.unmarshalOControlOrder2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐControlOrderBy(ctx, tmp) + } + + var zeroVal *types.ControlOrderBy + return zeroVal, nil +} + func (ec *executionContext) field_Organization_data_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -12196,6 +12313,8 @@ func (ec *executionContext) fieldContext_Asset_organization(_ context.Context, f return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) + case "controls": + return ec.fieldContext_Organization_controls(ctx, field) case "vendors": return ec.fieldContext_Organization_vendors(ctx, field) case "peoples": @@ -15183,6 +15302,8 @@ func (ec *executionContext) fieldContext_Datum_organization(_ context.Context, f return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) + case "controls": + return ec.fieldContext_Organization_controls(ctx, field) case "vendors": return ec.fieldContext_Organization_vendors(ctx, field) case "peoples": @@ -16766,6 +16887,8 @@ func (ec *executionContext) fieldContext_Document_organization(_ context.Context return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) + case "controls": + return ec.fieldContext_Organization_controls(ctx, field) case "vendors": return ec.fieldContext_Organization_vendors(ctx, field) case "peoples": @@ -19827,6 +19950,8 @@ func (ec *executionContext) fieldContext_Framework_organization(_ context.Contex return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) + case "controls": + return ec.fieldContext_Organization_controls(ctx, field) case "vendors": return ec.fieldContext_Organization_vendors(ctx, field) case "peoples": @@ -25561,6 +25686,67 @@ func (ec *executionContext) fieldContext_Organization_frameworks(ctx context.Con return fc, nil } +func (ec *executionContext) _Organization_controls(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Organization_controls(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Organization().Controls(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["orderBy"].(*types.ControlOrderBy)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*types.ControlConnection) + fc.Result = res + return ec.marshalNControlConnection2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐControlConnection(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Organization_controls(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Organization", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "edges": + return ec.fieldContext_ControlConnection_edges(ctx, field) + case "pageInfo": + return ec.fieldContext_ControlConnection_pageInfo(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ControlConnection", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Organization_controls_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Organization_vendors(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Organization_vendors(ctx, field) if err != nil { @@ -26336,6 +26522,8 @@ func (ec *executionContext) fieldContext_OrganizationEdge_node(_ context.Context return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) + case "controls": + return ec.fieldContext_Organization_controls(ctx, field) case "vendors": return ec.fieldContext_Organization_vendors(ctx, field) case "peoples": @@ -28472,6 +28660,8 @@ func (ec *executionContext) fieldContext_Risk_organization(_ context.Context, fi return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) + case "controls": + return ec.fieldContext_Organization_controls(ctx, field) case "vendors": return ec.fieldContext_Organization_vendors(ctx, field) case "peoples": @@ -29506,6 +29696,8 @@ func (ec *executionContext) fieldContext_Task_organization(_ context.Context, fi return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) + case "controls": + return ec.fieldContext_Organization_controls(ctx, field) case "vendors": return ec.fieldContext_Organization_vendors(ctx, field) case "peoples": @@ -30548,6 +30740,8 @@ func (ec *executionContext) fieldContext_UpdateOrganizationPayload_organization( return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) + case "controls": + return ec.fieldContext_Organization_controls(ctx, field) case "vendors": return ec.fieldContext_Organization_vendors(ctx, field) case "peoples": @@ -31763,6 +31957,8 @@ func (ec *executionContext) fieldContext_Vendor_organization(_ context.Context, return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) + case "controls": + return ec.fieldContext_Organization_controls(ctx, field) case "vendors": return ec.fieldContext_Organization_vendors(ctx, field) case "peoples": @@ -45402,6 +45598,42 @@ func (ec *executionContext) _Organization(ctx context.Context, sel ast.Selection continue } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "controls": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Organization_controls(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "vendors": field := field diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index d83e215e0..148523f95 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -777,6 +777,7 @@ type Organization struct { Users *UserConnection `json:"users"` Connectors *ConnectorConnection `json:"connectors"` Frameworks *FrameworkConnection `json:"frameworks"` + Controls *ControlConnection `json:"controls"` Vendors *VendorConnection `json:"vendors"` Peoples *PeopleConnection `json:"peoples"` Documents *DocumentConnection `json:"documents"` diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 2fcc9847c..bbe151361 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -1973,6 +1973,36 @@ func (r *organizationResolver) Frameworks(ctx context.Context, obj *types.Organi return types.NewFrameworkConnection(page), nil } +// Controls is the resolver for the controls field. +func (r *organizationResolver) Controls(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy) (*types.ControlConnection, error) { + svc := GetTenantService(ctx, r.proboSvc, obj.ID.TenantID()) + + pageOrderBy := page.OrderBy[coredata.ControlOrderField]{ + Field: coredata.ControlOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if orderBy != nil { + pageOrderBy = page.OrderBy[coredata.ControlOrderField]{ + Field: orderBy.Field, + Direction: orderBy.Direction, + } + } + + cursor := types.NewCursor(first, after, last, before, pageOrderBy) + + page, err := svc.Controls.ListForOrganizationID(ctx, obj.ID, cursor) + if err != nil { + return nil, fmt.Errorf("cannot list controls: %w", err) + } + + return types.NewControlConnection(page), nil +} + +// // Controls is the resolver for the controls field. +// func (r *organizationResolver) Controls(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy) (*types.ControlConnection, error) { +// panic(fmt.Errorf("not implemented: Controls - controls")) +// } + // Vendors is the resolver for the vendors field. func (r *organizationResolver) Vendors(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorOrderBy) (*types.VendorConnection, error) { svc := GetTenantService(ctx, r.proboSvc, obj.ID.TenantID())