Add risk assessment boundary model
Introduce RiskAssessmentBoundary as a first-class, self-nesting entity that
groups nodes within a risk assessment scope, and thread it through every
surface.
- coredata: new risk_assessment_boundaries table + migration, boundary_id on
nodes, self-referential parent_boundary_id, entity type registration
- riskmanagement: boundary CRUD service methods, boundary_id wiring on node
create/update, scope-membership and self-parent validation, nested-subgraph
Mermaid rendering
- IAM: core:risk-assessment-boundary:{get,list,create,update,delete} actions
and viewer/auditor read policies
- console GraphQL: RiskAssessmentBoundary type, connection, order enum, CRUD
mutations, boundaries field on scope, boundaryId on nodes
- CLI: risk-assessment boundary command group and --boundary-id on nodes
- MCP: boundary tools and boundary_id on node tools
- n8n: boundary operations and boundary fields on node operations
- console UI: boundary list/create/edit, boundary selector on nodes, diagram
refetch on boundary changes
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -26,9 +26,10 @@ import (
|
||||
|
||||
func (s *Service) BuildScopeMermaidChart(ctx context.Context, scope coredata.Scoper, scopeID gid.GID) (string, error) {
|
||||
var (
|
||||
nodes coredata.RiskAssessmentNodes
|
||||
processes coredata.RiskAssessmentProcesses
|
||||
threats coredata.RiskAssessmentThreats
|
||||
nodes coredata.RiskAssessmentNodes
|
||||
boundaries coredata.RiskAssessmentBoundaries
|
||||
processes coredata.RiskAssessmentProcesses
|
||||
threats coredata.RiskAssessmentThreats
|
||||
)
|
||||
|
||||
err := s.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
||||
@@ -36,6 +37,10 @@ func (s *Service) BuildScopeMermaidChart(ctx context.Context, scope coredata.Sco
|
||||
return fmt.Errorf("cannot load nodes: %w", err)
|
||||
}
|
||||
|
||||
if err := boundaries.LoadAllByRiskAssessmentScopeID(ctx, conn, scope, scopeID); err != nil {
|
||||
return fmt.Errorf("cannot load boundaries: %w", err)
|
||||
}
|
||||
|
||||
if err := processes.LoadAllByRiskAssessmentScopeID(ctx, conn, scope, scopeID); err != nil {
|
||||
return fmt.Errorf("cannot load processes: %w", err)
|
||||
}
|
||||
@@ -50,15 +55,16 @@ func (s *Service) BuildScopeMermaidChart(ctx context.Context, scope coredata.Sco
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buildScopeMermaidChart(nodes, processes, threats), nil
|
||||
return buildScopeMermaidChart(nodes, boundaries, processes, threats), nil
|
||||
}
|
||||
|
||||
func buildScopeMermaidChart(
|
||||
nodes coredata.RiskAssessmentNodes,
|
||||
boundaries coredata.RiskAssessmentBoundaries,
|
||||
processes coredata.RiskAssessmentProcesses,
|
||||
threats coredata.RiskAssessmentThreats,
|
||||
) string {
|
||||
if len(nodes) == 0 {
|
||||
if len(nodes) == 0 && len(boundaries) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -67,13 +73,87 @@ func buildScopeMermaidChart(
|
||||
nodeAlias[n.ID] = fmt.Sprintf("n%d", i)
|
||||
}
|
||||
|
||||
boundaryAlias := make(map[gid.GID]string, len(boundaries))
|
||||
for i, bnd := range boundaries {
|
||||
boundaryAlias[bnd.ID] = fmt.Sprintf("b%d", i)
|
||||
}
|
||||
|
||||
// Group boundaries by their parent so nested boundaries become nested subgraphs.
|
||||
childBoundaries := make(map[gid.GID]coredata.RiskAssessmentBoundaries)
|
||||
|
||||
var rootBoundaries coredata.RiskAssessmentBoundaries
|
||||
|
||||
for _, bnd := range boundaries {
|
||||
if bnd.ParentBoundaryID != nil {
|
||||
if _, ok := boundaryAlias[*bnd.ParentBoundaryID]; ok {
|
||||
childBoundaries[*bnd.ParentBoundaryID] = append(childBoundaries[*bnd.ParentBoundaryID], bnd)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
rootBoundaries = append(rootBoundaries, bnd)
|
||||
}
|
||||
|
||||
// Group nodes by the boundary that contains them; nodes without a
|
||||
// boundary (or referencing an unknown one) are rendered at the top level.
|
||||
nodesByBoundary := make(map[gid.GID]coredata.RiskAssessmentNodes)
|
||||
|
||||
var rootNodes coredata.RiskAssessmentNodes
|
||||
|
||||
for _, n := range nodes {
|
||||
if n.BoundaryID != nil {
|
||||
if _, ok := boundaryAlias[*n.BoundaryID]; ok {
|
||||
nodesByBoundary[*n.BoundaryID] = append(nodesByBoundary[*n.BoundaryID], n)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
rootNodes = append(rootNodes, n)
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("flowchart LR\n")
|
||||
|
||||
for _, n := range nodes {
|
||||
// class statements must live at the flowchart level, not inside a
|
||||
// subgraph block, so collect them and emit once all shapes are written.
|
||||
var classLines []string
|
||||
|
||||
emitNode := func(n *coredata.RiskAssessmentNode, indent string) {
|
||||
id := nodeAlias[n.ID]
|
||||
fmt.Fprintf(&b, " %s\n", mermaidNodeShape(n.NodeType, id, n.Name))
|
||||
fmt.Fprintf(&b, " class %s %s\n", id, mermaidNodeClass(n.NodeType))
|
||||
fmt.Fprintf(&b, "%s%s\n", indent, mermaidNodeShape(n.NodeType, id, n.Name))
|
||||
classLines = append(classLines, fmt.Sprintf(" class %s %s", id, mermaidNodeClass(n.NodeType)))
|
||||
}
|
||||
|
||||
var emitBoundary func(bnd *coredata.RiskAssessmentBoundary, indent string)
|
||||
|
||||
emitBoundary = func(bnd *coredata.RiskAssessmentBoundary, indent string) {
|
||||
alias := boundaryAlias[bnd.ID]
|
||||
fmt.Fprintf(&b, "%ssubgraph %s[\"%s\"]\n", indent, alias, escapeMermaidLabel(bnd.Name))
|
||||
|
||||
inner := indent + " "
|
||||
for _, child := range childBoundaries[bnd.ID] {
|
||||
emitBoundary(child, inner)
|
||||
}
|
||||
|
||||
for _, n := range nodesByBoundary[bnd.ID] {
|
||||
emitNode(n, inner)
|
||||
}
|
||||
|
||||
fmt.Fprintf(&b, "%send\n", indent)
|
||||
|
||||
classLines = append(classLines, fmt.Sprintf(" class %s nodeBoundary", alias))
|
||||
}
|
||||
|
||||
for _, bnd := range rootBoundaries {
|
||||
emitBoundary(bnd, " ")
|
||||
}
|
||||
|
||||
for _, n := range rootNodes {
|
||||
emitNode(n, " ")
|
||||
}
|
||||
|
||||
for _, line := range classLines {
|
||||
b.WriteString(line + "\n")
|
||||
}
|
||||
|
||||
for _, p := range processes {
|
||||
@@ -111,7 +191,7 @@ func buildScopeMermaidChart(
|
||||
}
|
||||
|
||||
b.WriteString(" classDef nodeEntity fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a\n")
|
||||
b.WriteString(" classDef nodeBoundary fill:#fef3c7,stroke:#b45309,color:#78350f\n")
|
||||
b.WriteString(" classDef nodeBoundary fill:#ffffff,stroke:#b45309,color:#78350f\n")
|
||||
b.WriteString(" classDef nodeAsset fill:#e5e7eb,stroke:#374151,color:#111827\n")
|
||||
b.WriteString(" classDef nodeData fill:#dcfce7,stroke:#15803d,color:#14532d\n")
|
||||
b.WriteString(" classDef nodeThreat fill:#fee2e2,stroke:#b91c1c,color:#7f1d1d\n")
|
||||
@@ -125,8 +205,6 @@ func mermaidNodeShape(t coredata.RiskAssessmentNodeType, id, name string) string
|
||||
switch t {
|
||||
case coredata.RiskAssessmentNodeTypeEntity:
|
||||
return fmt.Sprintf("%s([%s])", id, label)
|
||||
case coredata.RiskAssessmentNodeTypeBoundary:
|
||||
return fmt.Sprintf("%s{{%s}}", id, label)
|
||||
case coredata.RiskAssessmentNodeTypeData:
|
||||
return fmt.Sprintf("%s[(%s)]", id, label)
|
||||
case coredata.RiskAssessmentNodeTypeAsset:
|
||||
@@ -140,8 +218,6 @@ func mermaidNodeClass(t coredata.RiskAssessmentNodeType) string {
|
||||
switch t {
|
||||
case coredata.RiskAssessmentNodeTypeEntity:
|
||||
return "nodeEntity"
|
||||
case coredata.RiskAssessmentNodeTypeBoundary:
|
||||
return "nodeBoundary"
|
||||
case coredata.RiskAssessmentNodeTypeData:
|
||||
return "nodeData"
|
||||
case coredata.RiskAssessmentNodeTypeAsset:
|
||||
|
||||
@@ -62,16 +62,30 @@ type (
|
||||
Name *string
|
||||
}
|
||||
|
||||
CreateRiskAssessmentBoundaryRequest struct {
|
||||
RiskAssessmentScopeID gid.GID
|
||||
ParentBoundaryID *gid.GID
|
||||
Name string
|
||||
}
|
||||
|
||||
UpdateRiskAssessmentBoundaryRequest struct {
|
||||
ID gid.GID
|
||||
ParentBoundaryID **gid.GID
|
||||
Name *string
|
||||
}
|
||||
|
||||
CreateRiskAssessmentNodeRequest struct {
|
||||
RiskAssessmentScopeID gid.GID
|
||||
BoundaryID *gid.GID
|
||||
NodeType coredata.RiskAssessmentNodeType
|
||||
Name string
|
||||
}
|
||||
|
||||
UpdateRiskAssessmentNodeRequest struct {
|
||||
ID gid.GID
|
||||
NodeType *coredata.RiskAssessmentNodeType
|
||||
Name *string
|
||||
ID gid.GID
|
||||
BoundaryID **gid.GID
|
||||
NodeType *coredata.RiskAssessmentNodeType
|
||||
Name *string
|
||||
}
|
||||
|
||||
CreateRiskAssessmentProcessRequest struct {
|
||||
@@ -169,12 +183,40 @@ func (r *UpdateRiskAssessmentScopeRequest) Validate() error {
|
||||
return v.Error()
|
||||
}
|
||||
|
||||
func (r *CreateRiskAssessmentBoundaryRequest) Validate() error {
|
||||
v := validator.New()
|
||||
v.Check(r.RiskAssessmentScopeID, "risk_assessment_scope_id", validator.Required(), validator.GID(coredata.RiskAssessmentScopeEntityType))
|
||||
v.Check(r.Name, "name", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength))
|
||||
|
||||
if r.ParentBoundaryID != nil {
|
||||
v.Check(*r.ParentBoundaryID, "parent_boundary_id", validator.Required(), validator.GID(coredata.RiskAssessmentBoundaryEntityType))
|
||||
}
|
||||
|
||||
return v.Error()
|
||||
}
|
||||
|
||||
func (r *UpdateRiskAssessmentBoundaryRequest) Validate() error {
|
||||
v := validator.New()
|
||||
v.Check(r.ID, "id", validator.Required(), validator.GID(coredata.RiskAssessmentBoundaryEntityType))
|
||||
v.Check(r.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength))
|
||||
|
||||
if r.ParentBoundaryID != nil && *r.ParentBoundaryID != nil {
|
||||
v.Check(**r.ParentBoundaryID, "parent_boundary_id", validator.Required(), validator.GID(coredata.RiskAssessmentBoundaryEntityType))
|
||||
}
|
||||
|
||||
return v.Error()
|
||||
}
|
||||
|
||||
func (r *CreateRiskAssessmentNodeRequest) Validate() error {
|
||||
v := validator.New()
|
||||
v.Check(r.RiskAssessmentScopeID, "risk_assessment_scope_id", validator.Required(), validator.GID(coredata.RiskAssessmentScopeEntityType))
|
||||
v.Check(r.Name, "name", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength))
|
||||
v.Check(r.NodeType, "node_type", validator.Required(), validator.OneOfSlice(coredata.RiskAssessmentNodeTypes()))
|
||||
|
||||
if r.BoundaryID != nil {
|
||||
v.Check(*r.BoundaryID, "boundary_id", validator.Required(), validator.GID(coredata.RiskAssessmentBoundaryEntityType))
|
||||
}
|
||||
|
||||
return v.Error()
|
||||
}
|
||||
|
||||
@@ -184,6 +226,10 @@ func (r *UpdateRiskAssessmentNodeRequest) Validate() error {
|
||||
v.Check(r.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength))
|
||||
v.Check(r.NodeType, "node_type", validator.OneOfSlice(coredata.RiskAssessmentNodeTypes()))
|
||||
|
||||
if r.BoundaryID != nil && *r.BoundaryID != nil {
|
||||
v.Check(**r.BoundaryID, "boundary_id", validator.Required(), validator.GID(coredata.RiskAssessmentBoundaryEntityType))
|
||||
}
|
||||
|
||||
return v.Error()
|
||||
}
|
||||
|
||||
@@ -593,6 +639,7 @@ func (s *Service) CreateNode(ctx context.Context, scope coredata.Scoper, req Cre
|
||||
node := &coredata.RiskAssessmentNode{
|
||||
ID: gid.New(scope.GetTenantID(), coredata.RiskAssessmentNodeEntityType),
|
||||
RiskAssessmentScopeID: req.RiskAssessmentScopeID,
|
||||
BoundaryID: req.BoundaryID,
|
||||
NodeType: req.NodeType,
|
||||
Name: req.Name,
|
||||
CreatedAt: now,
|
||||
@@ -607,6 +654,12 @@ func (s *Service) CreateNode(ctx context.Context, scope coredata.Scoper, req Cre
|
||||
return fmt.Errorf("cannot load risk assessment scope: %w", err)
|
||||
}
|
||||
|
||||
if req.BoundaryID != nil {
|
||||
if err := s.assertBoundaryInScope(ctx, tx, scope, *req.BoundaryID, req.RiskAssessmentScopeID, "boundary_id"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
node.OrganizationID = raScope.OrganizationID
|
||||
if err := node.Insert(ctx, tx, scope); err != nil {
|
||||
return fmt.Errorf("cannot insert risk assessment node: %w", err)
|
||||
@@ -664,6 +717,16 @@ func (s *Service) UpdateNode(ctx context.Context, scope coredata.Scoper, req Upd
|
||||
node.NodeType = *req.NodeType
|
||||
}
|
||||
|
||||
if req.BoundaryID != nil {
|
||||
if *req.BoundaryID != nil {
|
||||
if err := s.assertBoundaryInScope(ctx, tx, scope, **req.BoundaryID, node.RiskAssessmentScopeID, "boundary_id"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
node.BoundaryID = *req.BoundaryID
|
||||
}
|
||||
|
||||
node.UpdatedAt = time.Now()
|
||||
if err := node.Update(ctx, tx, scope); err != nil {
|
||||
return fmt.Errorf("cannot update risk assessment node: %w", err)
|
||||
@@ -741,6 +804,179 @@ func (s *Service) CountNodesForScopeID(ctx context.Context, scope coredata.Scope
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *Service) CreateBoundary(ctx context.Context, scope coredata.Scoper, req CreateRiskAssessmentBoundaryRequest) (*coredata.RiskAssessmentBoundary, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid request: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
boundary := &coredata.RiskAssessmentBoundary{
|
||||
ID: gid.New(scope.GetTenantID(), coredata.RiskAssessmentBoundaryEntityType),
|
||||
RiskAssessmentScopeID: req.RiskAssessmentScopeID,
|
||||
ParentBoundaryID: req.ParentBoundaryID,
|
||||
Name: req.Name,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := s.pg.WithTx(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
raScope := coredata.RiskAssessmentScope{}
|
||||
if err := raScope.LoadByID(ctx, tx, scope, req.RiskAssessmentScopeID); err != nil {
|
||||
return fmt.Errorf("cannot load risk assessment scope: %w", err)
|
||||
}
|
||||
|
||||
if req.ParentBoundaryID != nil {
|
||||
if err := s.assertBoundaryInScope(ctx, tx, scope, *req.ParentBoundaryID, req.RiskAssessmentScopeID, "parent_boundary_id"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
boundary.OrganizationID = raScope.OrganizationID
|
||||
if err := boundary.Insert(ctx, tx, scope); err != nil {
|
||||
return fmt.Errorf("cannot insert risk assessment boundary: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return boundary, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetBoundary(ctx context.Context, scope coredata.Scoper, id gid.GID) (*coredata.RiskAssessmentBoundary, error) {
|
||||
boundary := &coredata.RiskAssessmentBoundary{}
|
||||
|
||||
err := s.pg.WithConn(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) error {
|
||||
if err := boundary.LoadByID(ctx, conn, scope, id); err != nil {
|
||||
return fmt.Errorf("cannot load risk assessment boundary: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return boundary, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateBoundary(ctx context.Context, scope coredata.Scoper, req UpdateRiskAssessmentBoundaryRequest) (*coredata.RiskAssessmentBoundary, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid request: %w", err)
|
||||
}
|
||||
|
||||
boundary := &coredata.RiskAssessmentBoundary{}
|
||||
|
||||
err := s.pg.WithTx(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
if err := boundary.LoadByID(ctx, tx, scope, req.ID); err != nil {
|
||||
return fmt.Errorf("cannot load risk assessment boundary: %w", err)
|
||||
}
|
||||
|
||||
if req.Name != nil {
|
||||
boundary.Name = *req.Name
|
||||
}
|
||||
|
||||
if req.ParentBoundaryID != nil {
|
||||
if *req.ParentBoundaryID != nil {
|
||||
if err := s.assertBoundaryInScope(ctx, tx, scope, **req.ParentBoundaryID, boundary.RiskAssessmentScopeID, "parent_boundary_id"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.assertNoBoundaryCycle(ctx, tx, scope, boundary.ID, **req.ParentBoundaryID, "parent_boundary_id"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
boundary.ParentBoundaryID = *req.ParentBoundaryID
|
||||
}
|
||||
|
||||
boundary.UpdatedAt = time.Now()
|
||||
if err := boundary.Update(ctx, tx, scope); err != nil {
|
||||
return fmt.Errorf("cannot update risk assessment boundary: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return boundary, nil
|
||||
}
|
||||
|
||||
func (s *Service) DeleteBoundary(ctx context.Context, scope coredata.Scoper, id gid.GID) error {
|
||||
return s.pg.WithTx(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
boundary := &coredata.RiskAssessmentBoundary{}
|
||||
if err := boundary.Delete(ctx, tx, scope, id); err != nil {
|
||||
return fmt.Errorf("cannot delete risk assessment boundary: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Service) ListBoundariesForScopeID(
|
||||
ctx context.Context,
|
||||
scope coredata.Scoper,
|
||||
scopeID gid.GID,
|
||||
cursor *page.Cursor[coredata.RiskAssessmentBoundaryOrderField],
|
||||
) (*page.Page[*coredata.RiskAssessmentBoundary, coredata.RiskAssessmentBoundaryOrderField], error) {
|
||||
var results coredata.RiskAssessmentBoundaries
|
||||
|
||||
err := s.pg.WithConn(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) error {
|
||||
if err := results.LoadByRiskAssessmentScopeID(ctx, conn, scope, scopeID, cursor); err != nil {
|
||||
return fmt.Errorf("cannot list risk assessment boundaries: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return page.NewPage(results, cursor), nil
|
||||
}
|
||||
|
||||
func (s *Service) CountBoundariesForScopeID(ctx context.Context, scope coredata.Scoper, scopeID gid.GID) (int, error) {
|
||||
var count int
|
||||
|
||||
err := s.pg.WithConn(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) (err error) {
|
||||
bs := &coredata.RiskAssessmentBoundaries{}
|
||||
|
||||
count, err = bs.CountByRiskAssessmentScopeID(ctx, conn, scope, scopeID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot count risk assessment boundaries: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *Service) CreateProcess(ctx context.Context, scope coredata.Scoper, req CreateRiskAssessmentProcessRequest) (*coredata.RiskAssessmentProcess, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid request: %w", err)
|
||||
@@ -1594,6 +1830,79 @@ func (s *Service) assertNodeInScope(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) assertBoundaryInScope(
|
||||
ctx context.Context,
|
||||
tx pg.Tx,
|
||||
scope coredata.Scoper,
|
||||
boundaryID gid.GID,
|
||||
scopeID gid.GID,
|
||||
field string,
|
||||
) error {
|
||||
boundary := &coredata.RiskAssessmentBoundary{}
|
||||
if err := boundary.LoadByID(ctx, tx, scope, boundaryID); err != nil {
|
||||
return validator.ValidationErrors{{
|
||||
Field: field,
|
||||
Code: validator.ErrorCodeCustom,
|
||||
Message: "boundary not found",
|
||||
}}
|
||||
}
|
||||
|
||||
// A boundary in a different scope is reported identically to a missing
|
||||
// one so the error does not reveal that the resource exists elsewhere.
|
||||
if boundary.RiskAssessmentScopeID != scopeID {
|
||||
return validator.ValidationErrors{{
|
||||
Field: field,
|
||||
Code: validator.ErrorCodeCustom,
|
||||
Message: "boundary not found",
|
||||
}}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// assertNoBoundaryCycle walks the ancestor chain starting from the proposed
|
||||
// parent. If it reaches the boundary being updated, the new parent would make
|
||||
// the boundary an ancestor of itself (a cycle), which is rejected. A visited
|
||||
// set guards against any pre-existing cycle in stored data.
|
||||
func (s *Service) assertNoBoundaryCycle(
|
||||
ctx context.Context,
|
||||
tx pg.Tx,
|
||||
scope coredata.Scoper,
|
||||
boundaryID gid.GID,
|
||||
proposedParentID gid.GID,
|
||||
field string,
|
||||
) error {
|
||||
visited := make(map[gid.GID]bool)
|
||||
currentID := proposedParentID
|
||||
|
||||
for {
|
||||
if currentID == boundaryID {
|
||||
return validator.ValidationErrors{{
|
||||
Field: field,
|
||||
Code: validator.ErrorCodeCustom,
|
||||
Message: "boundary cannot be nested under itself or one of its descendants",
|
||||
}}
|
||||
}
|
||||
|
||||
if visited[currentID] {
|
||||
return nil
|
||||
}
|
||||
|
||||
visited[currentID] = true
|
||||
|
||||
current := &coredata.RiskAssessmentBoundary{}
|
||||
if err := current.LoadByID(ctx, tx, scope, currentID); err != nil {
|
||||
return fmt.Errorf("cannot load parent boundary: %w", err)
|
||||
}
|
||||
|
||||
if current.ParentBoundaryID == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
currentID = *current.ParentBoundaryID
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) assertProcessInScope(
|
||||
ctx context.Context,
|
||||
tx pg.Tx,
|
||||
|
||||
Reference in New Issue
Block a user