Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .task/checksum/generate-ent-smart
Original file line number Diff line number Diff line change
@@ -1 +1 @@
f670462bfc175e35ac6916a5b473d1a7
bc97112060064a0367b26d233edd5551
2 changes: 1 addition & 1 deletion .task/checksum/generate-graphql-smart
Original file line number Diff line number Diff line change
@@ -1 +1 @@
a11929fa810e3184318b12fe761a218
9a11019d5ad3213ea43292c4dd320cd
1 change: 1 addition & 0 deletions cli/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ require (

require (
al.essio.dev/pkg/shellescape v1.6.0 // indirect
github.com/alicebob/miniredis/v2 v2.37.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
Expand Down
3 changes: 1 addition & 2 deletions cli/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkH
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w=
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
github.com/alicebob/miniredis/v2 v2.36.1 h1:Dvc5oAnNOr7BIfPn7tF269U8DvRW1dBG2D5n0WrfYMI=
github.com/alicebob/miniredis/v2 v2.36.1/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/alicebob/miniredis/v2 v2.37.0 h1:RheObYW32G1aiJIj81XVt78ZHJpHonHLHW7OLIshq68=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
Expand Down
3 changes: 3 additions & 0 deletions common/enums/notificationtopic.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ var (
NotificationTopicMention NotificationTopic = "MENTION"
// NotificationTopicExport represents an export notification
NotificationTopicExport NotificationTopic = "EXPORT"
// NotificationTopicStandardUpdate represents a standard update notification
NotificationTopicStandardUpdate NotificationTopic = "STANDARD_UPDATE"
// NotificationTopicInvalid represents an invalid notification topic
NotificationTopicInvalid NotificationTopic = "INVALID"
)

var notificationTopicValues = []NotificationTopic{
NotificationTopicTaskAssignment, NotificationTopicApproval,
NotificationTopicMention, NotificationTopicExport,
NotificationTopicStandardUpdate,
}

// Values returns a slice of strings that represents all the possible values of the NotificationTopic enum.
Expand Down
2 changes: 1 addition & 1 deletion internal/ent/checksum/.history_schema_checksum
Original file line number Diff line number Diff line change
@@ -1 +1 @@
9799fe17fb4256a7df5d9e064bd04f6a3d9d275eef04e164edb8acb1266f337d
b70c80ada6dc313778439ffd3c4855bb6b021abc7e777e691e6a778556cd1648
2 changes: 1 addition & 1 deletion internal/ent/checksum/.schema_checksum
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2b5916313bfeaef2aece947018ce7c69102ee71b4bc3b5af67873dd3c24d44a9
71076f05700cb25041318216dcb8aa9cd8cbf08c5366c0b7f48507c05cb54d8a
2 changes: 1 addition & 1 deletion internal/ent/generated/migrate/schema.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion internal/ent/generated/notification/notification.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion internal/ent/hooks/listeners_gala_registration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,13 @@ func TestRegisterGalaNotificationListeners(t *testing.T) {

ids, err := RegisterGalaNotificationListeners(registry)
require.NoError(t, err)
require.Len(t, ids, 6)
require.Len(t, ids, 7)

require.True(t, registry.InterestedIn(eventqueue.MutationTopicName(eventqueue.MutationConcernNotification, entgen.TypeTask), ent.OpCreate.String()))
require.True(t, registry.InterestedIn(eventqueue.MutationTopicName(eventqueue.MutationConcernNotification, entgen.TypeInternalPolicy), ent.OpUpdate.String()))
require.True(t, registry.InterestedIn(eventqueue.MutationTopicName(eventqueue.MutationConcernNotification, entgen.TypeRisk), ent.OpDelete.String()))
require.True(t, registry.InterestedIn(eventqueue.MutationTopicName(eventqueue.MutationConcernNotification, entgen.TypeProcedure), ent.OpUpdateOne.String()))
require.True(t, registry.InterestedIn(eventqueue.MutationTopicName(eventqueue.MutationConcernNotification, entgen.TypeNote), ent.OpCreate.String()))
require.True(t, registry.InterestedIn(eventqueue.MutationTopicName(eventqueue.MutationConcernNotification, entgen.TypeExport), ent.OpUpdate.String()))
require.True(t, registry.InterestedIn(eventqueue.MutationTopicName(eventqueue.MutationConcernNotification, entgen.TypeStandard), ent.OpUpdate.String()))
}
5 changes: 2 additions & 3 deletions internal/ent/interceptors/auditlogs.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,12 @@ import (
"github.com/theopenlane/iam/auth"

"github.com/theopenlane/core/internal/ent/generated"
"github.com/theopenlane/core/internal/ent/generated/intercept"
"github.com/theopenlane/core/internal/ent/privacy/utils"
)

// HistoryAccess is a traversal interceptor that checks if the user has the required role for the organization
func HistoryAccess(relation string, orgOwned, userOwed bool, objectOwner string) ent.Interceptor {
return intercept.TraverseFunc(func(ctx context.Context, q intercept.Query) error {
return TraverseFunc(func(ctx context.Context, q Query) error {
au, err := auth.GetAuthenticatedUserFromContext(ctx)
if err != nil {
return err
Expand Down Expand Up @@ -71,7 +70,7 @@ func HistoryAccess(relation string, orgOwned, userOwed bool, objectOwner string)
}

// addFilter adds a filter to the query based on the authenticated user's organization
func addFilter(ctx context.Context, q intercept.Query, orgOwned, userOwed bool, allowedOrgs []string) error {
func addFilter(ctx context.Context, q Query, orgOwned, userOwed bool, allowedOrgs []string) error {
userID, err := auth.GetSubjectIDFromContext(ctx)
if err != nil {
return err
Expand Down
3 changes: 3 additions & 0 deletions internal/ent/notifications/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const (
riskURLPath = "risks/%s"
taskURLPath = "tasks?id=%s"
controlURLPath = "controls/%s"
standardURLPath = "standards/%s"
evidenceURLPath = "evidence?id=%s"
trustCenterNDA = "trust-center/NDAs"
)
Expand All @@ -34,6 +35,8 @@ func getURLPathForObject(base, objectID, objectType string) string {
return base + fmt.Sprintf(taskURLPath, objectID)
case generated.TypeControl:
return base + fmt.Sprintf(controlURLPath, objectID)
case generated.TypeStandard:
return base + fmt.Sprintf(standardURLPath, objectID)
case generated.TypeEvidence:
return base + fmt.Sprintf(evidenceURLPath, objectID)
case generated.TypeTrustCenterNDARequest:
Expand Down
5 changes: 5 additions & 0 deletions internal/ent/notifications/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -558,5 +558,10 @@ func RegisterGalaListeners(registry *gala.Registry) ([]gala.ListenerID, error) {
Name: "notifications.export",
Handle: handleExportMutation,
},
gala.Definition[eventqueue.MutationGalaPayload]{
Topic: eventqueue.MutationTopic(eventqueue.MutationConcernNotification, generated.TypeStandard),
Name: "notifications.standard_update",
Handle: handleStandardMutation,
},
)
}
3 changes: 2 additions & 1 deletion internal/ent/notifications/events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,14 +207,15 @@ func TestRegisterGalaListeners(t *testing.T) {

ids, err := RegisterGalaListeners(registry)
require.NoError(t, err)
require.Len(t, ids, 6)
require.Len(t, ids, 7)

assert.True(t, registry.InterestedIn(eventqueue.MutationTopicName(eventqueue.MutationConcernNotification, generated.TypeTask), "create"))
assert.True(t, registry.InterestedIn(eventqueue.MutationTopicName(eventqueue.MutationConcernNotification, generated.TypeInternalPolicy), "update"))
assert.True(t, registry.InterestedIn(eventqueue.MutationTopicName(eventqueue.MutationConcernNotification, generated.TypeRisk), "delete"))
assert.True(t, registry.InterestedIn(eventqueue.MutationTopicName(eventqueue.MutationConcernNotification, generated.TypeProcedure), "update_one"))
assert.True(t, registry.InterestedIn(eventqueue.MutationTopicName(eventqueue.MutationConcernNotification, generated.TypeNote), "create"))
assert.True(t, registry.InterestedIn(eventqueue.MutationTopicName(eventqueue.MutationConcernNotification, generated.TypeExport), "update"))
assert.True(t, registry.InterestedIn(eventqueue.MutationTopicName(eventqueue.MutationConcernNotification, generated.TypeStandard), "update"))
}

func TestErrorConstants(t *testing.T) {
Expand Down
199 changes: 199 additions & 0 deletions internal/ent/notifications/standard_update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package notifications

import (
"context"
"fmt"

"github.com/samber/lo"
"github.com/theopenlane/iam/auth"

"github.com/theopenlane/core/common/enums"
"github.com/theopenlane/core/common/models"
"github.com/theopenlane/core/internal/ent/eventqueue"
"github.com/theopenlane/core/internal/ent/generated"
"github.com/theopenlane/core/internal/ent/generated/control"
"github.com/theopenlane/core/internal/ent/generated/orgmembership"
"github.com/theopenlane/core/internal/ent/generated/standard"
"github.com/theopenlane/core/pkg/gala"
"github.com/theopenlane/core/pkg/logx"
)

// handleStandardMutation processes standard mutations and creates notifications
// for org admins when a system-owned standard revision is bumped up
func handleStandardMutation(ctx gala.HandlerContext, payload eventqueue.MutationGalaPayload) error {
if !isUpdateOperation(payload.Operation) {
return nil
}

if !eventqueue.MutationFieldChanged(payload, standard.FieldRevision) {
return nil
}

ctx, client, ok := eventqueue.ClientFromHandler(ctx)
if !ok {
return ErrFailedToGetClient
}

props := ctx.Envelope.Headers.Properties

standardID, ok := eventqueue.MutationEntityID(payload, props)
if !ok {
return ErrEntityIDNotFound
}

allowCtx := ctx.Context

std, err := client.Standard.Get(allowCtx, standardID)
if err != nil {
return fmt.Errorf("failed to query standard: %w", err)
}

if !std.SystemOwned {
return nil
}

type standardControl struct {
OwnerID string `json:"owner_id"`
ReferenceFrameworkRevision *string `json:"reference_framework_revision"`
}

var controls []standardControl

err = client.Control.Query().
Where(
control.StandardID(standardID),
control.OwnerIDNotNil(),
).
Select(
control.FieldOwnerID,
control.FieldReferenceFrameworkRevision,
).
Scan(allowCtx, &controls)
if err != nil {
return fmt.Errorf("failed to query controls for standard: %w", err)
}

if len(controls) == 0 {
return nil
}

filteredControls := lo.Filter(controls, func(c standardControl, _ int) bool {
return c.OwnerID != ""
})

type organizations struct {
revision string
controlCount int
}

groups := lo.GroupBy(filteredControls, func(c standardControl) string {
return c.OwnerID
})

orgMap := lo.MapValues(groups, func(cs []standardControl, _ string) organizations {
return organizations{
revision: lo.FromPtrOr(cs[0].ReferenceFrameworkRevision, ""),
controlCount: len(cs),
}
})

significantOrgs := lo.PickBy(orgMap, func(_ string, info organizations) bool {
return detectVersionBump(info.revision, std.Revision) != ""
})

consoleURL := client.EntConfig.Notifications.ConsoleURL

lo.ForEach(lo.Entries(significantOrgs), func(entry lo.Entry[string, organizations], _ int) {
orgID := entry.Key
value := entry.Value

ids, err := fetchOrgAdminsAndOwners(allowCtx, client, orgID)
if err != nil {
logx.FromContext(ctx.Context).Error().Err(err).
Str("org_id", orgID).
Msg("failed to get org admin and owner IDs")

return
}

if len(ids) == 0 {
return
}

data := map[string]any{
"url": getURLPathForObject(consoleURL, standardID, generated.TypeStandard),
"standard_id": standardID,
"standard_short_name": std.ShortName,
"old_revision": value.revision,
"new_revision": std.Revision,
"change_type": detectVersionBump(value.revision, std.Revision),
"affected_controls_count": value.controlCount,
}

topic := enums.NotificationTopicStandardUpdate
notifInput := &generated.CreateNotificationInput{
NotificationType: enums.NotificationTypeOrganization,
Title: fmt.Sprintf("%s update available", std.ShortName),
Body: fmt.Sprintf("%s has been updated to %s", std.ShortName, std.Revision),
Data: data,
OwnerID: &orgID,
Topic: &topic,
ObjectType: generated.TypeStandard,
}

notificationCtx := auth.WithAuthenticatedUser(ctx.Context, &auth.AuthenticatedUser{
SubjectID: ids[0],
OrganizationID: orgID,
OrganizationIDs: []string{orgID},
})

if err := newNotificationCreation(notificationCtx, client, ids, notifInput); err != nil {
logx.FromContext(ctx.Context).Error().Err(err).
Str("org_id", orgID).
Msg("failed to create standard update notification")

return
}
})

return nil
}

func fetchOrgAdminsAndOwners(ctx context.Context, client *generated.Client, orgID string) ([]string, error) {
var ids []string

err := client.OrgMembership.Query().
Where(
orgmembership.OrganizationIDEQ(orgID),
orgmembership.RoleIn(enums.RoleOwner, enums.RoleAdmin),
).
Select(orgmembership.FieldUserID).
Scan(ctx, &ids)
if err != nil {
return nil, err
}

return ids, nil
}

func detectVersionBump(oldRevision, newRevision string) string {
oldVersion, err := models.ToSemverVersion(&oldRevision)
if err != nil {
return "major"
}

newVersion, err := models.ToSemverVersion(&newRevision)
if err != nil {
return "major"
}

if oldVersion.Major != newVersion.Major {
return "major"
}

if oldVersion.Minor != newVersion.Minor {
return "minor"
}

return ""
}
Loading