diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 000000000..02f576b91 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,20 @@ +version: "2" + +linters: + default: none + enable: + - errcheck + - govet + - ineffassign + - staticcheck + - wsl_v5 + settings: + wsl_v5: + allow-first-in-block: true + allow-whole-block: false + branch-max-lines: 2 + case-max-lines: 0 + cuddle-max-statements: 1 + default: default + enable: + - after-block diff --git a/cmd/geoloc-import/main.go b/cmd/geoloc-import/main.go index a09fee07d..cfa1a80ac 100644 --- a/cmd/geoloc-import/main.go +++ b/cmd/geoloc-import/main.go @@ -68,6 +68,7 @@ func run() error { } fmt.Println("done") + return nil } @@ -95,6 +96,7 @@ func newPgClientFromDSN(dsn string) (*pg.Client, error) { if u.Port() == "" { host = net.JoinHostPort(u.Hostname(), "5432") } + opts = append(opts, pg.WithAddr(host)) } diff --git a/cmd/migrate-document-versions-markdown/main.go b/cmd/migrate-document-versions-markdown/main.go index 3e07d594a..0c0ad84d8 100644 --- a/cmd/migrate-document-versions-markdown/main.go +++ b/cmd/migrate-document-versions-markdown/main.go @@ -73,6 +73,7 @@ func run() error { } var ids []string + err = pgClient.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { rows, err := conn.Query( ctx, @@ -94,8 +95,10 @@ ORDER BY id; if err := rows.Scan(&id); err != nil { return fmt.Errorf("cannot scan document version id: %w", err) } + ids = append(ids, id) } + return rows.Err() }) if err != nil { @@ -103,13 +106,16 @@ ORDER BY id; } var failures int + for _, idStr := range ids { if err := migrateOneInTx(ctx, pgClient, idStr, dryRun); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) + failures++ if !continueOnError { return fmt.Errorf("stopped after %d failure(s)", failures) } + continue } } @@ -119,6 +125,7 @@ ORDER BY id; } fmt.Println("done") + return nil } @@ -135,6 +142,7 @@ func newPgClientFromDSN(dsn string) (*pg.Client, error) { if u.Port() == "" { host = net.JoinHostPort(u.Hostname(), "5432") } + opts = append(opts, pg.WithAddr(host)) } @@ -197,6 +205,7 @@ func migrateOne(ctx context.Context, tx pg.Tx, idStr string, dryRun bool) error } fmt.Printf("updated %s\n", idStr) + return nil } @@ -207,5 +216,6 @@ func isProseMirrorDocJSON(s string) bool { if err := json.Unmarshal([]byte(s), &probe); err != nil { return false } + return probe.Type == "doc" } diff --git a/cmd/prb/main.go b/cmd/prb/main.go index 21314c329..fc3f1cb16 100644 --- a/cmd/prb/main.go +++ b/cmd/prb/main.go @@ -33,9 +33,11 @@ func main() { if isNonInteractiveEnv() { ios.ForceNonInteractive = true } + if isNoColorEnv() { ios.ForceNoColor = true } + ios.ApplyColorProfile() f := &cmdutil.Factory{ @@ -58,15 +60,19 @@ func isNonInteractiveEnv() bool { if v := os.Getenv("PROBO_NO_INTERACTIVE"); v == "1" || v == "true" { return true } + if v := os.Getenv("CI"); v == "true" || v == "1" { return true } + if os.Getenv("DEBIAN_FRONTEND") == "noninteractive" { return true } + if os.Getenv("TERM") == "dumb" { return true } + return false } @@ -74,8 +80,10 @@ func isNoColorEnv() bool { if _, ok := os.LookupEnv("NO_COLOR"); ok { return true } + if os.Getenv("TERM") == "dumb" { return true } + return false } diff --git a/cmd/probod-bootstrap/main.go b/cmd/probod-bootstrap/main.go index 469eb4c92..8e4a37b82 100644 --- a/cmd/probod-bootstrap/main.go +++ b/cmd/probod-bootstrap/main.go @@ -30,6 +30,7 @@ var ( func main() { outputPath := flag.String("output", "/etc/probod/config.yml", "output path for the generated config file") showVersion := flag.Bool("version", false, "print version and exit") + flag.Parse() if *showVersion { diff --git a/cmd/probod/main.go b/cmd/probod/main.go index a468e71aa..4cb5104c0 100644 --- a/cmd/probod/main.go +++ b/cmd/probod/main.go @@ -29,6 +29,7 @@ var ( func main() { impl := probod.New() unit := unit.NewUnit(impl, "probod", version, env) + err := unit.Run() if err != nil && err != context.Canceled { panic(err) diff --git a/e2e/console/access_review_test.go b/e2e/console/access_review_test.go index 23d95239c..5e9b6f8d7 100644 --- a/e2e/console/access_review_test.go +++ b/e2e/console/access_review_test.go @@ -540,6 +540,7 @@ func TestAccessReviewCampaign_DeleteRemovesFromListAndNode(t *testing.T) { err = owner.Execute(listQuery, map[string]any{"id": orgID}, &listResult) require.NoError(t, err) + for _, edge := range listResult.Node.AccessReviewCampaigns.Edges { assert.NotEqual(t, campaignID, edge.Node.ID, "deleted campaign must not appear in the connection") } @@ -557,6 +558,7 @@ func TestAccessReviewCampaign_DeleteRemovesFromListAndNode(t *testing.T) { ` _, err = owner.Do(nodeQuery, map[string]any{"id": campaignID}) + var gqlErrors testutil.GraphQLErrors require.ErrorAs(t, err, &gqlErrors) require.Len(t, gqlErrors, 1) @@ -1114,11 +1116,13 @@ func TestAccessReviewCampaign_FullLifecycle(t *testing.T) { } var campaignResult campaignQueryResult + require.Eventually(t, func() bool { err := owner.Execute(nodeQuery, map[string]any{"id": campaignID}, &campaignResult) if err != nil { return false } + return campaignResult.Node.Status == "PENDING_ACTIONS" }, 60*time.Second, 1*time.Second, "campaign should transition to PENDING_ACTIONS") @@ -1239,6 +1243,7 @@ func TestAccessReviewCampaign_CloseRequiresAllDecisions(t *testing.T) { } } ` + err := owner.Execute(startQuery, map[string]any{ "input": map[string]any{ "accessReviewCampaignId": campaignID, @@ -1264,6 +1269,7 @@ func TestAccessReviewCampaign_CloseRequiresAllDecisions(t *testing.T) { if err := owner.Execute(nodeQuery, map[string]any{"id": campaignID}, &r); err != nil { return false } + return r.Node.Status == "PENDING_ACTIONS" }, 60*time.Second, 1*time.Second) diff --git a/e2e/console/asset_publish_test.go b/e2e/console/asset_publish_test.go index f9b8baac3..e2a976652 100644 --- a/e2e/console/asset_publish_test.go +++ b/e2e/console/asset_publish_test.go @@ -240,6 +240,7 @@ func TestAsset_PublishAssetList(t *testing.T) { ver1Major := result1.PublishAssetList.DocumentVersionEdge.Node.Major ver2Major := result2.PublishAssetList.DocumentVersionEdge.Node.Major + assert.Equal(t, 1, ver1Major) assert.Equal(t, 2, ver2Major) }, diff --git a/e2e/console/asset_test.go b/e2e/console/asset_test.go index 837648999..64eac5e5a 100644 --- a/e2e/console/asset_test.go +++ b/e2e/console/asset_test.go @@ -125,6 +125,7 @@ func TestAsset_Update(t *testing.T) { }, }, &createResult) require.NoError(t, err) + assetID := createResult.CreateAsset.AssetEdge.Node.ID const query = ` @@ -205,6 +206,7 @@ func TestAsset_Delete(t *testing.T) { }, }, &createResult) require.NoError(t, err) + assetID := createResult.CreateAsset.AssetEdge.Node.ID const query = ` diff --git a/e2e/console/audit_log_test.go b/e2e/console/audit_log_test.go index 7e2ffc85b..9bbee8f52 100644 --- a/e2e/console/audit_log_test.go +++ b/e2e/console/audit_log_test.go @@ -80,17 +80,21 @@ func TestAuditLog_List(t *testing.T) { // Find the thirdParty create entry. found := false + for _, edge := range result.Node.AuditLogEntries.Edges { if edge.Node.Action == "core:thirdParty:create" { found = true + assert.Equal(t, "USER", edge.Node.ActorType) assert.Equal(t, "ThirdParty", edge.Node.ResourceType) assert.NotEmpty(t, edge.Node.ActorID) assert.NotEmpty(t, edge.Node.ResourceID) assert.NotEmpty(t, edge.Node.CreatedAt) + break } } + assert.True(t, found, "expected to find core:thirdParty:create audit log entry") } @@ -143,6 +147,7 @@ func TestAuditLog_Filter(t *testing.T) { }, &result) require.NoError(t, err) assert.GreaterOrEqual(t, result.Node.AuditLogEntries.TotalCount, 1) + for _, edge := range result.Node.AuditLogEntries.Edges { assert.Equal(t, "core:thirdParty:create", edge.Node.Action) } @@ -171,6 +176,7 @@ func TestAuditLog_Filter(t *testing.T) { }, &result) require.NoError(t, err) assert.GreaterOrEqual(t, result.Node.AuditLogEntries.TotalCount, 1) + for _, edge := range result.Node.AuditLogEntries.Edges { assert.Equal(t, "ThirdParty", edge.Node.ResourceType) } diff --git a/e2e/console/audit_test.go b/e2e/console/audit_test.go index 4328f7bb4..7f447f198 100644 --- a/e2e/console/audit_test.go +++ b/e2e/console/audit_test.go @@ -250,9 +250,11 @@ func TestAudit_Create_Validation(t *testing.T) { if !tt.skipOrganization { input["organizationId"] = owner.GetOrganizationID().String() } + if !tt.skipFramework { input["frameworkId"] = frameworkID } + maps.Copy(input, tt.input) _, err := owner.Do(query, map[string]any{"input": input}) @@ -376,6 +378,7 @@ func TestAudit_Update(t *testing.T) { require.NoError(t, err) audit := result.UpdateAudit.Audit + switch tt.assertField { case "name": assert.Equal(t, tt.assertValue, audit.Name) @@ -1188,6 +1191,7 @@ func TestAudit_Pagination(t *testing.T) { assert.GreaterOrEqual(t, result.Node.Audits.TotalCount, 5) testutil.AssertHasMorePages(t, result.Node.Audits.PageInfo) + queryAfter := ` query($id: ID!, $after: CursorKey) { node(id: $id) { @@ -1372,7 +1376,6 @@ func TestAudit_TenantIsolation(t *testing.T) { err := org2Owner.Execute(query, map[string]any{ "id": org1Owner.GetOrganizationID().String(), }, &result) - if err == nil { for _, edge := range result.Node.Audits.Edges { assert.NotEqual(t, auditID, edge.Node.ID, "Should not see audit from another org") @@ -1434,6 +1437,7 @@ func TestAudit_Ordering(t *testing.T) { for i, edge := range result.Node.Audits.Edges { times[i] = edge.Node.CreatedAt } + testutil.AssertTimesOrderedDescending(t, times, "createdAt") }) } @@ -1539,6 +1543,7 @@ func TestAudit_UploadReport(t *testing.T) { Content: pdfContent1, }, &result1) require.NoError(t, err) + firstReportID := result1.UploadAuditReport.Audit.Report.ID // Upload second report (should replace) diff --git a/e2e/console/connector_test.go b/e2e/console/connector_test.go index d7c96a2a3..ed3613e5d 100644 --- a/e2e/console/connector_test.go +++ b/e2e/console/connector_test.go @@ -75,6 +75,7 @@ func TestConnectorProviderInfos(t *testing.T) { assert.NotEmpty(t, infos) providerNames := make(map[string]bool) + for _, info := range infos { assert.NotEmpty(t, info.Provider) assert.NotEmpty(t, info.DisplayName) diff --git a/e2e/console/control_test.go b/e2e/console/control_test.go index bb1f90035..384a6dec3 100644 --- a/e2e/console/control_test.go +++ b/e2e/console/control_test.go @@ -118,7 +118,6 @@ func TestControl_Update(t *testing.T) { assert.Equal(t, controlID, result.UpdateControl.Control.ID) assert.Equal(t, "Updated Control Name", result.UpdateControl.Control.Name) }) - } func TestControl_Delete(t *testing.T) { @@ -243,6 +242,7 @@ func TestControl_RequiredFields(t *testing.T) { }, }, &frameworkResult) require.NoError(t, err) + frameworkID := frameworkResult.CreateFramework.FrameworkEdge.Node.ID createControlQuery := ` @@ -389,6 +389,7 @@ func TestControl_OmittableDescription(t *testing.T) { }, }, &frameworkResult) require.NoError(t, err) + frameworkID := frameworkResult.CreateFramework.FrameworkEdge.Node.ID // Create control with description @@ -427,6 +428,7 @@ func TestControl_OmittableDescription(t *testing.T) { }, }, &createResult) require.NoError(t, err) + controlID := createResult.CreateControl.ControlEdge.Node.ID t.Run("Update with null description should clear it", func(t *testing.T) { @@ -573,6 +575,7 @@ func TestControl_MaturityLevel(t *testing.T) { t.Run("create with INITIAL maturityLevel", func(t *testing.T) { var res createResult + err := owner.Execute(createControlQuery, map[string]any{ "input": map[string]any{ "frameworkId": frameworkID, @@ -589,6 +592,7 @@ func TestControl_MaturityLevel(t *testing.T) { t.Run("create with maturityLevel persists value", func(t *testing.T) { var res createResult + err := owner.Execute(createControlQuery, map[string]any{ "input": map[string]any{ "frameworkId": frameworkID, @@ -605,6 +609,7 @@ func TestControl_MaturityLevel(t *testing.T) { t.Run("update lifecycle: set, change, omit", func(t *testing.T) { var created createResult + err := owner.Execute(createControlQuery, map[string]any{ "input": map[string]any{ "frameworkId": frameworkID, @@ -616,10 +621,12 @@ func TestControl_MaturityLevel(t *testing.T) { }, }, &created) require.NoError(t, err) + controlID := created.CreateControl.ControlEdge.Node.ID // set var setRes updateResult + err = owner.Execute(updateControlQuery, map[string]any{ "input": map[string]any{ "id": controlID, @@ -631,6 +638,7 @@ func TestControl_MaturityLevel(t *testing.T) { // change var changeRes updateResult + err = owner.Execute(updateControlQuery, map[string]any{ "input": map[string]any{ "id": controlID, @@ -642,6 +650,7 @@ func TestControl_MaturityLevel(t *testing.T) { // omit field on next update -> stays unchanged var omitRes updateResult + err = owner.Execute(updateControlQuery, map[string]any{ "input": map[string]any{ "id": controlID, @@ -654,6 +663,7 @@ func TestControl_MaturityLevel(t *testing.T) { t.Run("invalid maturityLevel is rejected", func(t *testing.T) { var res createResult + err := owner.Execute(createControlQuery, map[string]any{ "input": map[string]any{ "frameworkId": frameworkID, @@ -702,6 +712,7 @@ func TestControl_SubResolvers(t *testing.T) { }, }, &frameworkResult) require.NoError(t, err) + frameworkID := frameworkResult.CreateFramework.FrameworkEdge.Node.ID // Create control @@ -738,6 +749,7 @@ func TestControl_SubResolvers(t *testing.T) { }, }, &controlResult) require.NoError(t, err) + controlID := controlResult.CreateControl.ControlEdge.Node.ID // Create a measure and link it @@ -771,6 +783,7 @@ func TestControl_SubResolvers(t *testing.T) { }, }, &measureResult) require.NoError(t, err) + measureID := measureResult.CreateMeasure.MeasureEdge.Node.ID // Create mapping diff --git a/e2e/console/cookie_banner_test.go b/e2e/console/cookie_banner_test.go index de2d501ce..88e4ab274 100644 --- a/e2e/console/cookie_banner_test.go +++ b/e2e/console/cookie_banner_test.go @@ -84,6 +84,7 @@ func TestCookieBanner_Create(t *testing.T) { }, &result) require.NoError(t, err) + node := result.CreateCookieBanner.CookieBannerEdge.Node assert.NotEmpty(t, node.ID) assert.Equal(t, name, node.Name) @@ -135,6 +136,7 @@ func TestCookieBanner_Create(t *testing.T) { }, &result) require.NoError(t, err) + node := result.CreateCookieBanner.CookieBannerEdge.Node assert.NotEmpty(t, node.ID) require.NotNil(t, node.PrivacyPolicyUrl) @@ -189,6 +191,7 @@ func TestCookieBanner_Create(t *testing.T) { for _, e := range result.Node.ConsentCategories.Edges { kinds[e.Node.Kind] = true } + assert.True(t, kinds["NECESSARY"], "should have a NECESSARY category") }) @@ -262,6 +265,7 @@ func TestCookieBanner_Update(t *testing.T) { ` newName := factory.SafeName("Updated") + var result struct { UpdateCookieBanner struct { CookieBanner struct { @@ -410,6 +414,7 @@ func TestCookieBanner_ActivateDeactivate(t *testing.T) { } `json:"cookieBanner"` } `json:"deactivateCookieBanner"` } + err := owner.Execute(` mutation($input: DeactivateCookieBannerInput!) { deactivateCookieBanner(input: $input) { @@ -751,8 +756,10 @@ func TestCookieBanner_UpsertTranslation(t *testing.T) { } `json:"cookieBannerTranslation"` } `json:"upsertCookieBannerTranslation"` } + err := owner.Execute(query, input, &result1) require.NoError(t, err) + firstID := result1.UpsertCookieBannerTranslation.CookieBannerTranslation.ID input["input"].(map[string]any)["translations"] = `{"title":"Ajustes de cookies"}` @@ -765,6 +772,7 @@ func TestCookieBanner_UpsertTranslation(t *testing.T) { } `json:"cookieBannerTranslation"` } `json:"upsertCookieBannerTranslation"` } + err = owner.Execute(query, input, &result2) require.NoError(t, err) assert.Equal(t, firstID, result2.UpsertCookieBannerTranslation.CookieBannerTranslation.ID) diff --git a/e2e/console/cookie_banner_versioning_test.go b/e2e/console/cookie_banner_versioning_test.go index 352df34a0..c43410df9 100644 --- a/e2e/console/cookie_banner_versioning_test.go +++ b/e2e/console/cookie_banner_versioning_test.go @@ -166,6 +166,7 @@ func TestCookieBannerVersioning_NoOpUpdates(t *testing.T) { ` var result struct{} + err := owner.Execute(query, map[string]any{ "input": map[string]any{ "cookieBannerId": bannerID, @@ -238,6 +239,7 @@ func TestCookieBannerVersioning_NoOpUpdates(t *testing.T) { ` var result struct{} + err := owner.Execute(query, map[string]any{ "input": map[string]any{ "cookieCategoryId": categoryID, @@ -273,6 +275,7 @@ func TestCookieBannerVersioning_NoOpUpdates(t *testing.T) { ` var result struct{} + err := owner.Execute(query, map[string]any{ "input": map[string]any{ "cookieCategoryId": categoryID, @@ -308,6 +311,7 @@ func TestCookieBannerVersioning_NoOpUpdates(t *testing.T) { ` var result struct{} + err := owner.Execute(query, map[string]any{ "input": map[string]any{ "cookieCategoryId": categoryID, @@ -342,6 +346,7 @@ func TestCookieBannerVersioning_NoOpUpdates(t *testing.T) { ` var result struct{} + err := owner.Execute(query, map[string]any{ "input": map[string]any{ "trackerPatternId": patternID, @@ -424,6 +429,7 @@ func TestCookieBannerVersioning_ExcludedPattern(t *testing.T) { ` var result struct{} + err := owner.Execute(query, map[string]any{ "input": map[string]any{"trackerPatternId": patternID}, }, &result) @@ -456,6 +462,7 @@ func TestCookieBannerVersioning_ExcludedPattern(t *testing.T) { ` var result struct{} + err := owner.Execute(query, map[string]any{ "input": map[string]any{ "trackerPatternId": patternID, @@ -531,6 +538,7 @@ func reportDetectedCookies(t *testing.T, c *testutil.Client, bannerID string, na Name string `json:"name"` Source string `json:"source"` } + cookies := make([]entry, len(names)) for i, n := range names { cookies[i] = entry{Name: n, Source: "script"} @@ -542,7 +550,9 @@ func reportDetectedCookies(t *testing.T, c *testutil.Client, bannerID string, na endpoint := fmt.Sprintf("%s/api/cookie-banner/v1/%s/report", c.BaseURL(), bannerID) resp, err := c.HTTPClient().Post(endpoint, "application/json", bytes.NewReader(body)) require.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + require.Equal(t, http.StatusNoContent, resp.StatusCode, "report endpoint should return 204") } @@ -584,7 +594,9 @@ func TestCookieBannerVersioning_RealChangesStillBumpVersion(t *testing.T) { updateCookieBanner(input: $input) { cookieBanner { id } } } ` + var result struct{} + err := owner.Execute(query, map[string]any{ "input": map[string]any{ "cookieBannerId": bannerID, @@ -617,7 +629,9 @@ func TestCookieBannerVersioning_RealChangesStillBumpVersion(t *testing.T) { updateTrackerPattern(input: $input) { trackerPattern { id } } } ` + var result struct{} + err := owner.Execute(query, map[string]any{ "input": map[string]any{ "trackerPatternId": patternID, diff --git a/e2e/console/cookie_category_test.go b/e2e/console/cookie_category_test.go index 275fd1455..1d2f06d3e 100644 --- a/e2e/console/cookie_category_test.go +++ b/e2e/console/cookie_category_test.go @@ -89,6 +89,7 @@ func TestCookieCategory_Create(t *testing.T) { }, &result) require.NoError(t, err) + node := result.CreateCookieCategory.CookieCategoryEdge.Node assert.NotEmpty(t, node.ID) assert.Equal(t, "Marketing", node.Name) @@ -343,12 +344,14 @@ func TestCookieCategory_Delete(t *testing.T) { require.NoError(t, err) var necessaryCategoryID string + for _, e := range listResult.Node.ConsentCategories.Edges { if e.Node.Kind == "NECESSARY" { necessaryCategoryID = e.Node.ID break } } + require.NotEmpty(t, necessaryCategoryID, "should find a NECESSARY category") _, err = owner.Do(` diff --git a/e2e/console/datum_publish_test.go b/e2e/console/datum_publish_test.go index f36530618..16347ed6d 100644 --- a/e2e/console/datum_publish_test.go +++ b/e2e/console/datum_publish_test.go @@ -242,6 +242,7 @@ func TestDatum_PublishDataList(t *testing.T) { ver1Major := result1.PublishDataList.DocumentVersionEdge.Node.Major ver2Major := result2.PublishDataList.DocumentVersionEdge.Node.Major + assert.Equal(t, 1, ver1Major) assert.Equal(t, 2, ver2Major) }, diff --git a/e2e/console/datum_test.go b/e2e/console/datum_test.go index 76169bbff..df0692abb 100644 --- a/e2e/console/datum_test.go +++ b/e2e/console/datum_test.go @@ -249,9 +249,11 @@ func TestDatum_Create_Validation(t *testing.T) { if !tt.skipOrganization { input["organizationId"] = owner.GetOrganizationID().String() } + if !tt.skipOwner { input["ownerId"] = profileID } + maps.Copy(input, tt.input) _, err := owner.Do(query, map[string]any{"input": input}) @@ -348,6 +350,7 @@ func TestDatum_Update(t *testing.T) { require.NoError(t, err) datum := result.UpdateDatum.Datum + switch tt.assertField { case "name": assert.Equal(t, tt.assertValue, datum.Name) @@ -1159,6 +1162,7 @@ func TestDatum_Pagination(t *testing.T) { assert.GreaterOrEqual(t, result.Node.Data.TotalCount, 5) testutil.AssertHasMorePages(t, result.Node.Data.PageInfo) + queryAfter := ` query($id: ID!, $after: CursorKey) { node(id: $id) { @@ -1343,7 +1347,6 @@ func TestDatum_TenantIsolation(t *testing.T) { err := org2Owner.Execute(query, map[string]any{ "id": org1Owner.GetOrganizationID().String(), }, &result) - if err == nil { for _, edge := range result.Node.Data.Edges { assert.NotEqual(t, datumID, edge.Node.ID, "Should not see datum from another org") @@ -1404,6 +1407,7 @@ func TestDatum_Ordering(t *testing.T) { for i, edge := range result.Node.Data.Edges { times[i] = edge.Node.CreatedAt } + testutil.AssertTimesOrderedDescending(t, times, "createdAt") }) } diff --git a/e2e/console/document_test.go b/e2e/console/document_test.go index 1c01be286..a5f4b2891 100644 --- a/e2e/console/document_test.go +++ b/e2e/console/document_test.go @@ -276,6 +276,7 @@ func TestDocument_Create_Validation(t *testing.T) { if !tt.skipOrganization { input["organizationId"] = owner.GetOrganizationID().String() } + maps.Copy(input, tt.input) _, err := owner.Do(query, map[string]any{"input": input}) @@ -1111,6 +1112,7 @@ func TestDocument_Pagination(t *testing.T) { assert.GreaterOrEqual(t, result.Node.Documents.TotalCount, 5) testutil.AssertHasMorePages(t, result.Node.Documents.PageInfo) + queryAfter := ` query($id: ID!, $after: CursorKey) { node(id: $id) { @@ -1288,7 +1290,6 @@ func TestDocument_TenantIsolation(t *testing.T) { err := org2Owner.Execute(query, map[string]any{ "id": org1Owner.GetOrganizationID().String(), }, &result) - if err == nil { for _, edge := range result.Node.Documents.Edges { assert.NotEqual(t, documentID, edge.Node.ID, "Should not see document from another org") @@ -1348,6 +1349,7 @@ func TestDocument_Ordering(t *testing.T) { for i, edge := range result.Node.Documents.Edges { times[i] = edge.Node.CreatedAt } + testutil.AssertTimesOrderedDescending(t, times, "createdAt") }) } diff --git a/e2e/console/document_version_test.go b/e2e/console/document_version_test.go index b57b4d395..8f59e12bb 100644 --- a/e2e/console/document_version_test.go +++ b/e2e/console/document_version_test.go @@ -84,6 +84,7 @@ func createTestDocument(t *testing.T, owner *testutil.Client) (docID string, doc if len(result.CreateDocument.DocumentEdge.Node.Versions.Edges) > 0 { docVersionID = result.CreateDocument.DocumentEdge.Node.Versions.Edges[0].Node.ID } + return docID, docVersionID } @@ -502,6 +503,7 @@ func TestDocumentVersion_BulkPublish(t *testing.T) { require.NoError(t, err) assert.Equal(t, 2, len(result.BulkPublishDocuments.DocumentVersions)) + for _, dv := range result.BulkPublishDocuments.DocumentVersions { assert.Equal(t, "PUBLISHED", dv.Status) } @@ -720,6 +722,7 @@ func TestDocumentVersion_BulkRequestSignatures(t *testing.T) { require.NoError(t, err) assert.Equal(t, 2, len(result.BulkRequestSignatures.DocumentVersionSignatureEdges)) + for _, edge := range result.BulkRequestSignatures.DocumentVersionSignatureEdges { assert.Equal(t, "REQUESTED", edge.Node.State) } @@ -1028,6 +1031,7 @@ func TestDocumentVersion_VoidApproval(t *testing.T) { require.NotEmpty(t, quorumResult.Node.Versions.Edges[0].Node.ApprovalQuorums.Edges) decisions := quorumResult.Node.Versions.Edges[0].Node.ApprovalQuorums.Edges[0].Node.Decisions.Edges require.NotEmpty(t, decisions) + for _, d := range decisions { assert.Equal(t, "VOIDED", d.Node.State, "decisions should be VOIDED after voiding") } @@ -1382,6 +1386,7 @@ func TestDocumentVersion_DeleteDraft(t *testing.T) { } } ` + var updateResult struct { UpdateDocument struct { Document struct { @@ -1393,6 +1398,7 @@ func TestDocumentVersion_DeleteDraft(t *testing.T) { } `json:"documentVersion"` } `json:"updateDocument"` } + err := owner.Execute(updateQuery, map[string]any{ "input": map[string]any{ "id": docID, @@ -1411,6 +1417,7 @@ func TestDocumentVersion_DeleteDraft(t *testing.T) { } `json:"document"` } `json:"deleteDocumentDraft"` } + err = owner.Execute(query, map[string]any{ "input": map[string]any{"documentId": docID}, }, &result) @@ -1427,6 +1434,7 @@ func TestDocumentVersion_DeleteDraft(t *testing.T) { docID, _ := createTestDocument(t, owner) var result struct{} + err := owner.Execute(query, map[string]any{ "input": map[string]any{"documentId": docID}, }, &result) @@ -1443,6 +1451,7 @@ func TestDocumentVersion_DeleteDraft(t *testing.T) { approveTestDocument(t, owner, docID) var result struct{} + err := owner.Execute(query, map[string]any{ "input": map[string]any{"documentId": docID}, }, &result) @@ -1469,6 +1478,7 @@ func TestDocumentVersion_DeleteDraft(t *testing.T) { } } ` + var updateResult struct { UpdateDocument struct { Document struct { @@ -1479,6 +1489,7 @@ func TestDocumentVersion_DeleteDraft(t *testing.T) { } `json:"documentVersion"` } `json:"updateDocument"` } + err := owner.Execute(updateQuery, map[string]any{ "input": map[string]any{ "id": docID, @@ -1488,6 +1499,7 @@ func TestDocumentVersion_DeleteDraft(t *testing.T) { require.NoError(t, err) var result struct{} + err = viewer.Execute(query, map[string]any{ "input": map[string]any{"documentId": docID}, }, &result) diff --git a/e2e/console/employee_document_test.go b/e2e/console/employee_document_test.go index dd73ed94b..a5795be76 100644 --- a/e2e/console/employee_document_test.go +++ b/e2e/console/employee_document_test.go @@ -505,12 +505,14 @@ func TestEmployeeDocument_FilterModeIsolation(t *testing.T) { require.NoError(t, err) var found bool + for _, edge := range result.Viewer.SignableDocuments.Edges { if edge.Node.ID == docID { found = true break } } + assert.True(t, found, "signer should see document in signableDocuments list") }) @@ -613,12 +615,14 @@ func TestEmployeeDocument_ApproverFilterModeIsolation(t *testing.T) { require.NoError(t, err) var found bool + for _, edge := range result.Viewer.ApprovableDocuments.Edges { if edge.Node.ID == docID { found = true break } } + assert.True(t, found, "approver should see document in approvableDocuments list") }) diff --git a/e2e/console/finding_publish_test.go b/e2e/console/finding_publish_test.go index 4567d3bcc..ca16ff994 100644 --- a/e2e/console/finding_publish_test.go +++ b/e2e/console/finding_publish_test.go @@ -237,6 +237,7 @@ func TestFinding_PublishFindingList(t *testing.T) { ver1Major := result1.PublishFindingList.DocumentVersionEdge.Node.Major ver2Major := result2.PublishFindingList.DocumentVersionEdge.Node.Major + assert.Equal(t, 1, ver1Major) assert.Equal(t, 2, ver2Major) }, diff --git a/e2e/console/finding_test.go b/e2e/console/finding_test.go index f3db05e71..2e38876db 100644 --- a/e2e/console/finding_test.go +++ b/e2e/console/finding_test.go @@ -186,6 +186,7 @@ func TestFinding_Update(t *testing.T) { }, }, &createResult) require.NoError(t, err) + findingID := createResult.CreateFinding.FindingEdge.Node.ID query := ` @@ -274,6 +275,7 @@ func TestFinding_Delete(t *testing.T) { }, }, &createResult) require.NoError(t, err) + findingID := createResult.CreateFinding.FindingEdge.Node.ID query := ` @@ -510,6 +512,7 @@ func TestFinding_CreateAuditMapping(t *testing.T) { }, }, &createResult) require.NoError(t, err) + findingID := createResult.CreateFinding.FindingEdge.Node.ID // Link finding to audit @@ -635,6 +638,7 @@ func TestFinding_DeleteAuditMapping(t *testing.T) { }, }, &createResult) require.NoError(t, err) + findingID := createResult.CreateFinding.FindingEdge.Node.ID // Link finding to audit @@ -753,6 +757,7 @@ func TestFinding_StatusAndPriorityValues(t *testing.T) { "status values", func(t *testing.T) { t.Parallel() + riskID := factory.CreateRisk(owner) statuses := []string{"OPEN", "IN_PROGRESS", "CLOSED", "RISK_ACCEPTED", "MITIGATED", "FALSE_POSITIVE"} @@ -761,6 +766,7 @@ func TestFinding_StatusAndPriorityValues(t *testing.T) { status, func(t *testing.T) { t.Parallel() + var result struct { CreateFinding struct { FindingEdge struct { @@ -799,6 +805,7 @@ func TestFinding_StatusAndPriorityValues(t *testing.T) { "priority values", func(t *testing.T) { t.Parallel() + priorities := []string{"LOW", "MEDIUM", "HIGH"} for _, priority := range priorities { @@ -806,6 +813,7 @@ func TestFinding_StatusAndPriorityValues(t *testing.T) { priority, func(t *testing.T) { t.Parallel() + var result struct { CreateFinding struct { FindingEdge struct { diff --git a/e2e/console/framework_test.go b/e2e/console/framework_test.go index c959d283f..3d5806a2b 100644 --- a/e2e/console/framework_test.go +++ b/e2e/console/framework_test.go @@ -215,6 +215,7 @@ func TestFramework_Create_Validation(t *testing.T) { if !tt.skipOrganization { input["organizationId"] = owner.GetOrganizationID().String() } + maps.Copy(input, tt.input) _, err := owner.Do(query, map[string]any{"input": input}) @@ -301,6 +302,7 @@ func TestFramework_Update(t *testing.T) { require.NoError(t, err) framework := result.UpdateFramework.Framework + switch tt.assertField { case "name": assert.Equal(t, tt.assertValue, framework.Name) @@ -637,6 +639,7 @@ func TestFramework_Timestamps(t *testing.T) { t.Run("updatedAt changes on update", func(t *testing.T) { t.Skip("Skipped: server may not update timestamp immediately or has caching") + frameworkID := factory.NewFramework(owner).WithName("Timestamp Update Test").Create() getQuery := ` @@ -1216,6 +1219,7 @@ func TestFramework_SubResolvers_WithData(t *testing.T) { for i, edge := range result.Node.Controls.Edges { controlIDs[i] = edge.Node.ID } + assert.Contains(t, controlIDs, control1ID) assert.Contains(t, controlIDs, control2ID) }) @@ -1281,6 +1285,7 @@ func TestFramework_Pagination(t *testing.T) { assert.GreaterOrEqual(t, result.Node.Frameworks.TotalCount, 5) testutil.AssertHasMorePages(t, result.Node.Frameworks.PageInfo) + queryAfter := ` query($id: ID!, $after: CursorKey) { node(id: $id) { @@ -1464,7 +1469,6 @@ func TestFramework_TenantIsolation(t *testing.T) { err := org2Owner.Execute(query, map[string]any{ "id": org1Owner.GetOrganizationID().String(), }, &result) - if err == nil { for _, edge := range result.Node.Frameworks.Edges { assert.NotEqual(t, frameworkID, edge.Node.ID, "Should not see framework from another org") @@ -1524,6 +1528,7 @@ func TestFramework_Ordering(t *testing.T) { for i, edge := range result.Node.Frameworks.Edges { times[i] = edge.Node.CreatedAt } + testutil.AssertTimesOrderedDescending(t, times, "createdAt") }) } diff --git a/e2e/console/main_test.go b/e2e/console/main_test.go index 78dd6b2ca..7916cd15c 100644 --- a/e2e/console/main_test.go +++ b/e2e/console/main_test.go @@ -23,7 +23,9 @@ import ( func TestMain(m *testing.M) { testutil.Setup() + code := m.Run() + testutil.Teardown() os.Exit(code) } diff --git a/e2e/console/mapping_test.go b/e2e/console/mapping_test.go index 1ea2be65d..4ef7d111e 100644 --- a/e2e/console/mapping_test.go +++ b/e2e/console/mapping_test.go @@ -37,6 +37,7 @@ func TestControlMeasureMapping_CreateDelete(t *testing.T) { } `json:"frameworkEdge"` } `json:"createFramework"` } + err := owner.Execute(` mutation($input: CreateFrameworkInput!) { createFramework(input: $input) { @@ -54,6 +55,7 @@ func TestControlMeasureMapping_CreateDelete(t *testing.T) { }, }, &createFrameworkResult) require.NoError(t, err) + frameworkID := createFrameworkResult.CreateFramework.FrameworkEdge.Node.ID // Create a control @@ -66,6 +68,7 @@ func TestControlMeasureMapping_CreateDelete(t *testing.T) { } `json:"controlEdge"` } `json:"createControl"` } + err = owner.Execute(` mutation($input: CreateControlInput!) { createControl(input: $input) { @@ -87,6 +90,7 @@ func TestControlMeasureMapping_CreateDelete(t *testing.T) { }, }, &createControlResult) require.NoError(t, err) + controlID := createControlResult.CreateControl.ControlEdge.Node.ID // Create a measure @@ -99,6 +103,7 @@ func TestControlMeasureMapping_CreateDelete(t *testing.T) { } `json:"measureEdge"` } `json:"createMeasure"` } + err = owner.Execute(` mutation($input: CreateMeasureInput!) { createMeasure(input: $input) { @@ -117,6 +122,7 @@ func TestControlMeasureMapping_CreateDelete(t *testing.T) { }, }, &createMeasureResult) require.NoError(t, err) + measureID := createMeasureResult.CreateMeasure.MeasureEdge.Node.ID t.Run("create mapping", func(t *testing.T) { @@ -134,6 +140,7 @@ func TestControlMeasureMapping_CreateDelete(t *testing.T) { } `json:"measureEdge"` } `json:"createControlMeasureMapping"` } + err := owner.Execute(` mutation($input: CreateControlMeasureMappingInput!) { createControlMeasureMapping(input: $input) { @@ -192,6 +199,7 @@ func TestRiskMeasureMapping_CreateDelete(t *testing.T) { } `json:"riskEdge"` } `json:"createRisk"` } + err := owner.Execute(` mutation($input: CreateRiskInput!) { createRisk(input: $input) { @@ -213,6 +221,7 @@ func TestRiskMeasureMapping_CreateDelete(t *testing.T) { }, }, &createRiskResult) require.NoError(t, err) + riskID := createRiskResult.CreateRisk.RiskEdge.Node.ID // Create a measure @@ -225,6 +234,7 @@ func TestRiskMeasureMapping_CreateDelete(t *testing.T) { } `json:"measureEdge"` } `json:"createMeasure"` } + err = owner.Execute(` mutation($input: CreateMeasureInput!) { createMeasure(input: $input) { @@ -243,6 +253,7 @@ func TestRiskMeasureMapping_CreateDelete(t *testing.T) { }, }, &createMeasureResult) require.NoError(t, err) + measureID := createMeasureResult.CreateMeasure.MeasureEdge.Node.ID t.Run("create mapping", func(t *testing.T) { @@ -260,6 +271,7 @@ func TestRiskMeasureMapping_CreateDelete(t *testing.T) { } `json:"measureEdge"` } `json:"createRiskMeasureMapping"` } + err := owner.Execute(` mutation($input: CreateRiskMeasureMappingInput!) { createRiskMeasureMapping(input: $input) { @@ -318,6 +330,7 @@ func TestControlDocumentMapping_CreateDelete(t *testing.T) { } `json:"frameworkEdge"` } `json:"createFramework"` } + err := owner.Execute(` mutation($input: CreateFrameworkInput!) { createFramework(input: $input) { @@ -335,6 +348,7 @@ func TestControlDocumentMapping_CreateDelete(t *testing.T) { }, }, &createFrameworkResult) require.NoError(t, err) + frameworkID := createFrameworkResult.CreateFramework.FrameworkEdge.Node.ID var createControlResult struct { @@ -346,6 +360,7 @@ func TestControlDocumentMapping_CreateDelete(t *testing.T) { } `json:"controlEdge"` } `json:"createControl"` } + err = owner.Execute(` mutation($input: CreateControlInput!) { createControl(input: $input) { @@ -367,6 +382,7 @@ func TestControlDocumentMapping_CreateDelete(t *testing.T) { }, }, &createControlResult) require.NoError(t, err) + controlID := createControlResult.CreateControl.ControlEdge.Node.ID // Create a document @@ -379,6 +395,7 @@ func TestControlDocumentMapping_CreateDelete(t *testing.T) { } `json:"documentEdge"` } `json:"createDocument"` } + err = owner.Execute(` mutation($input: CreateDocumentInput!) { createDocument(input: $input) { @@ -399,6 +416,7 @@ func TestControlDocumentMapping_CreateDelete(t *testing.T) { }, }, &createDocumentResult) require.NoError(t, err) + documentID := createDocumentResult.CreateDocument.DocumentEdge.Node.ID t.Run("create mapping", func(t *testing.T) { @@ -458,6 +476,7 @@ func TestControlAuditMapping_CreateDelete(t *testing.T) { } `json:"frameworkEdge"` } `json:"createFramework"` } + err := owner.Execute(` mutation($input: CreateFrameworkInput!) { createFramework(input: $input) { @@ -475,6 +494,7 @@ func TestControlAuditMapping_CreateDelete(t *testing.T) { }, }, &createFrameworkResult) require.NoError(t, err) + frameworkID := createFrameworkResult.CreateFramework.FrameworkEdge.Node.ID var createControlResult struct { @@ -486,6 +506,7 @@ func TestControlAuditMapping_CreateDelete(t *testing.T) { } `json:"controlEdge"` } `json:"createControl"` } + err = owner.Execute(` mutation($input: CreateControlInput!) { createControl(input: $input) { @@ -507,6 +528,7 @@ func TestControlAuditMapping_CreateDelete(t *testing.T) { }, }, &createControlResult) require.NoError(t, err) + controlID := createControlResult.CreateControl.ControlEdge.Node.ID // Create an audit @@ -519,6 +541,7 @@ func TestControlAuditMapping_CreateDelete(t *testing.T) { } `json:"auditEdge"` } `json:"createAudit"` } + err = owner.Execute(` mutation($input: CreateAuditInput!) { createAudit(input: $input) { @@ -537,6 +560,7 @@ func TestControlAuditMapping_CreateDelete(t *testing.T) { }, }, &createAuditResult) require.NoError(t, err) + auditID := createAuditResult.CreateAudit.AuditEdge.Node.ID t.Run("create mapping", func(t *testing.T) { @@ -596,6 +620,7 @@ func TestRiskDocumentMapping_CreateDelete(t *testing.T) { } `json:"riskEdge"` } `json:"createRisk"` } + err := owner.Execute(` mutation($input: CreateRiskInput!) { createRisk(input: $input) { @@ -617,6 +642,7 @@ func TestRiskDocumentMapping_CreateDelete(t *testing.T) { }, }, &createRiskResult) require.NoError(t, err) + riskID := createRiskResult.CreateRisk.RiskEdge.Node.ID // Create a document @@ -629,6 +655,7 @@ func TestRiskDocumentMapping_CreateDelete(t *testing.T) { } `json:"documentEdge"` } `json:"createDocument"` } + err = owner.Execute(` mutation($input: CreateDocumentInput!) { createDocument(input: $input) { @@ -649,6 +676,7 @@ func TestRiskDocumentMapping_CreateDelete(t *testing.T) { }, }, &createDocumentResult) require.NoError(t, err) + documentID := createDocumentResult.CreateDocument.DocumentEdge.Node.ID t.Run("create mapping", func(t *testing.T) { @@ -708,6 +736,7 @@ func TestRiskObligationMapping_CreateDelete(t *testing.T) { } `json:"riskEdge"` } `json:"createRisk"` } + err := owner.Execute(` mutation($input: CreateRiskInput!) { createRisk(input: $input) { @@ -729,10 +758,12 @@ func TestRiskObligationMapping_CreateDelete(t *testing.T) { }, }, &createRiskResult) require.NoError(t, err) + riskID := createRiskResult.CreateRisk.RiskEdge.Node.ID // Create an obligation profileID := factory.CreateUser(owner) + var createObligationResult struct { CreateObligation struct { ObligationEdge struct { @@ -742,6 +773,7 @@ func TestRiskObligationMapping_CreateDelete(t *testing.T) { } `json:"obligationEdge"` } `json:"createObligation"` } + err = owner.Execute(` mutation($input: CreateObligationInput!) { createObligation(input: $input) { @@ -763,6 +795,7 @@ func TestRiskObligationMapping_CreateDelete(t *testing.T) { }, }, &createObligationResult) require.NoError(t, err) + obligationID := createObligationResult.CreateObligation.ObligationEdge.Node.ID t.Run("create mapping", func(t *testing.T) { @@ -815,6 +848,7 @@ func TestMeasureDocumentMapping_CreateDelete(t *testing.T) { t.Run("create mapping", func(t *testing.T) { t.Parallel() + documentID := factory.NewDocument(owner).Create() var result struct { @@ -831,6 +865,7 @@ func TestMeasureDocumentMapping_CreateDelete(t *testing.T) { } `json:"documentEdge"` } `json:"createMeasureDocumentMapping"` } + err := owner.Execute(` mutation($input: CreateMeasureDocumentMappingInput!) { createMeasureDocumentMapping(input: $input) { @@ -859,6 +894,7 @@ func TestMeasureDocumentMapping_CreateDelete(t *testing.T) { t.Run("delete mapping", func(t *testing.T) { t.Parallel() + documentID := factory.NewDocument(owner).Create() // Create the mapping first diff --git a/e2e/console/measure_test.go b/e2e/console/measure_test.go index 709bc0c53..a836db4cb 100644 --- a/e2e/console/measure_test.go +++ b/e2e/console/measure_test.go @@ -300,6 +300,7 @@ func TestMeasure_Create_Validation(t *testing.T) { if !tt.skipOrganization { input["organizationId"] = owner.GetOrganizationID().String() } + maps.Copy(input, tt.input) _, err := owner.Do(query, map[string]any{"input": input}) @@ -403,6 +404,7 @@ func TestMeasure_Update(t *testing.T) { require.NoError(t, err) measure := result.UpdateMeasure.Measure + switch tt.assertField { case "name": assert.Equal(t, tt.assertValue, measure.Name) @@ -1349,6 +1351,7 @@ func TestMeasure_SubResolvers_WithData(t *testing.T) { for i, edge := range result.Node.Tasks.Edges { taskIDs[i] = edge.Node.ID } + assert.Contains(t, taskIDs, task1ID) assert.Contains(t, taskIDs, task2ID) }) @@ -1543,6 +1546,7 @@ func TestMeasure_Pagination(t *testing.T) { // Get next page using cursor testutil.AssertHasMorePages(t, result.Node.Measures.PageInfo) + queryAfter := ` query($id: ID!, $after: CursorKey) { node(id: $id) { @@ -1698,12 +1702,14 @@ func TestMeasure_Filtering(t *testing.T) { // Should contain our implemented measure found := false + for _, edge := range result.Node.Measures.Edges { if edge.Node.ID == measure1ID { found = true break } } + assert.True(t, found, "Expected to find implemented measure in filtered results") }) @@ -1749,6 +1755,7 @@ func TestMeasure_Filtering(t *testing.T) { for i, edge := range result.Node.Measures.Edges { foundIDs[i] = edge.Node.ID } + assert.Contains(t, foundIDs, measure1ID) assert.Contains(t, foundIDs, measure2ID) }) @@ -1804,17 +1811,20 @@ func TestMeasure_FilterByCategory(t *testing.T) { require.NoError(t, err) assert.GreaterOrEqual(t, result.Node.Measures.TotalCount, 1) + for _, edge := range result.Node.Measures.Edges { assert.Equal(t, "POLICY", edge.Node.Category) } found := false + for _, edge := range result.Node.Measures.Edges { if edge.Node.ID == policyID { found = true break } } + assert.True(t, found, "Expected to find POLICY measure in filtered results") }) @@ -2127,6 +2137,7 @@ func TestMeasure_Ordering(t *testing.T) { for i, edge := range result.Node.Measures.Edges { names[i] = edge.Node.Name } + testutil.AssertOrderedAscending(t, names, "name") }) @@ -2175,6 +2186,7 @@ func TestMeasure_Ordering(t *testing.T) { for i, edge := range result.Node.Measures.Edges { names[i] = edge.Node.Name } + testutil.AssertOrderedDescending(t, names, "name") }) @@ -2223,6 +2235,7 @@ func TestMeasure_Ordering(t *testing.T) { for i, edge := range result.Node.Measures.Edges { times[i] = edge.Node.CreatedAt } + testutil.AssertTimesOrderedDescending(t, times, "createdAt") }) } diff --git a/e2e/console/oauth2_test.go b/e2e/console/oauth2_test.go index 73377425d..089f1cbdb 100644 --- a/e2e/console/oauth2_test.go +++ b/e2e/console/oauth2_test.go @@ -314,6 +314,7 @@ func TestOAuth2_AuthorizationCodeFlow(t *testing.T) { require.NoError(t, err) var code string + if testutil.IsConsentRedirect(authResp) { consentID, err := testutil.ExtractConsentIDFromResponse(authResp) require.NoError(t, err) @@ -365,6 +366,7 @@ func TestOAuth2_AuthorizationCodeFlow(t *testing.T) { require.NoError(t, err) var code string + if testutil.IsConsentRedirect(authResp) { consentID, err := testutil.ExtractConsentIDFromResponse(authResp) require.NoError(t, err) @@ -1564,6 +1566,7 @@ func TestOAuth2_Security(t *testing.T) { require.NoError(t, err) var code string + if testutil.IsConsentRedirect(authResp) { consentID, err := testutil.ExtractConsentIDFromResponse(authResp) require.NoError(t, err) @@ -1617,6 +1620,7 @@ func TestOAuth2_Security(t *testing.T) { require.NoError(t, err) var redirectLoc string + if testutil.IsConsentRedirect(authResp) { consentID, err := testutil.ExtractConsentIDFromResponse(authResp) require.NoError(t, err) @@ -1696,6 +1700,7 @@ func TestOAuth2_Security(t *testing.T) { require.NoError(t, err) var code string + if testutil.IsConsentRedirect(authResp) { consentID, err := testutil.ExtractConsentIDFromResponse(authResp) require.NoError(t, err) @@ -1822,6 +1827,7 @@ func TestOAuth2_Security(t *testing.T) { require.NoError(t, err) _, challenge2 := testutil.GeneratePKCE() + params.Set("state", "consent-second") params.Set("code_challenge", challenge2) @@ -1862,6 +1868,7 @@ func TestOAuth2_Security(t *testing.T) { require.NoError(t, err) var code string + if testutil.IsConsentRedirect(authResp) { consentID, err := testutil.ExtractConsentIDFromResponse(authResp) require.NoError(t, err) @@ -2003,12 +2010,14 @@ func TestOAuth2_Security(t *testing.T) { require.NotNil(t, jwks) var matchingKey map[string]any + for _, k := range jwks.Keys { if kid, ok := k["kid"].(string); ok && kid == header.Kid { matchingKey = k break } } + require.NotNil(t, matchingKey, "JWKS must contain key matching kid=%s", header.Kid) // Reconstruct the RSA public key from JWK. @@ -2157,6 +2166,7 @@ func TestOAuth2_Security(t *testing.T) { require.NoError(t, err) var code string + if testutil.IsConsentRedirect(authResp) { consentID, err := testutil.ExtractConsentIDFromResponse(authResp) require.NoError(t, err) @@ -2222,6 +2232,7 @@ func TestOAuth2_ClientSecretPost(t *testing.T) { require.NoError(t, err) var code string + if testutil.IsConsentRedirect(authResp) { consentID, err := testutil.ExtractConsentIDFromResponse(authResp) require.NoError(t, err) @@ -2309,6 +2320,7 @@ func TestOAuth2_PublicClientAuthCodeFlow(t *testing.T) { require.NoError(t, err) var code string + if testutil.IsConsentRedirect(authResp) { consentID, err := testutil.ExtractConsentIDFromResponse(authResp) require.NoError(t, err) @@ -2486,6 +2498,7 @@ func TestOAuth2_IDTokenClaims(t *testing.T) { require.NoError(t, err) var code string + if testutil.IsConsentRedirect(authResp) { consentID, err := testutil.ExtractConsentIDFromResponse(authResp) require.NoError(t, err) @@ -2585,6 +2598,7 @@ func TestOAuth2_CacheHeaders(t *testing.T) { require.NoError(t, err) var code string + if testutil.IsConsentRedirect(authResp) { consentID, err := testutil.ExtractConsentIDFromResponse(authResp) require.NoError(t, err) @@ -2825,6 +2839,7 @@ func TestOAuth2_Expiry(t *testing.T) { require.NoError(t, err) var code string + if testutil.IsConsentRedirect(authResp) { consentID, err := testutil.ExtractConsentIDFromResponse(authResp) require.NoError(t, err) @@ -3010,6 +3025,7 @@ func TestOAuth2_OfflineAccessScope(t *testing.T) { require.NoError(t, err) var code string + if testutil.IsConsentRedirect(authResp) { consentID, err := testutil.ExtractConsentIDFromResponse(authResp) require.NoError(t, err) @@ -3065,6 +3081,7 @@ func TestOAuth2_OfflineAccessScope(t *testing.T) { require.NoError(t, err) var code string + if testutil.IsConsentRedirect(authResp) { consentID, err := testutil.ExtractConsentIDFromResponse(authResp) require.NoError(t, err) @@ -3470,6 +3487,7 @@ func TestOAuth2_AuthorizationCodeReplayRevokesTokens(t *testing.T) { require.NoError(t, err) var code string + if testutil.IsConsentRedirect(authResp) { consentID, err := testutil.ExtractConsentIDFromResponse(authResp) require.NoError(t, err) diff --git a/e2e/console/obligation_publish_test.go b/e2e/console/obligation_publish_test.go index 8b9146196..9977e2ffd 100644 --- a/e2e/console/obligation_publish_test.go +++ b/e2e/console/obligation_publish_test.go @@ -237,6 +237,7 @@ func TestObligation_PublishObligationList(t *testing.T) { ver1Major := result1.PublishObligationList.DocumentVersionEdge.Node.Major ver2Major := result2.PublishObligationList.DocumentVersionEdge.Node.Major + assert.Equal(t, 1, ver1Major) assert.Equal(t, 2, ver2Major) }, diff --git a/e2e/console/processing_activity_test.go b/e2e/console/processing_activity_test.go index 820f0543c..4f44e0067 100644 --- a/e2e/console/processing_activity_test.go +++ b/e2e/console/processing_activity_test.go @@ -192,6 +192,7 @@ func TestProcessingActivity_Create_Validation(t *testing.T) { if !tt.skipOrganization { input["organizationId"] = owner.GetOrganizationID().String() } + maps.Copy(input, tt.input) _, err := owner.Do(query, map[string]any{"input": input}) @@ -290,6 +291,7 @@ func TestProcessingActivity_Update(t *testing.T) { require.NoError(t, err) pa := result.UpdateProcessingActivity.ProcessingActivity + switch tt.assertField { case "name": assert.Equal(t, tt.assertValue, pa.Name) @@ -954,6 +956,7 @@ func TestProcessingActivity_Pagination(t *testing.T) { assert.GreaterOrEqual(t, result.Node.ProcessingActivities.TotalCount, 5) testutil.AssertHasMorePages(t, result.Node.ProcessingActivities.PageInfo) + queryAfter := ` query($id: ID!, $after: CursorKey) { node(id: $id) { @@ -1137,7 +1140,6 @@ func TestProcessingActivity_TenantIsolation(t *testing.T) { err := org2Owner.Execute(query, map[string]any{ "id": org1Owner.GetOrganizationID().String(), }, &result) - if err == nil { for _, edge := range result.Node.ProcessingActivities.Edges { assert.NotEqual(t, paID, edge.Node.ID, "Should not see processing activity from another org") @@ -1197,6 +1199,7 @@ func TestProcessingActivity_Ordering(t *testing.T) { for i, edge := range result.Node.ProcessingActivities.Edges { times[i] = edge.Node.CreatedAt } + testutil.AssertTimesOrderedDescending(t, times, "createdAt") }) } @@ -1533,6 +1536,7 @@ func TestProcessingActivity_DPIA(t *testing.T) { } } ` + var createResult struct { CreateDataProtectionImpactAssessment struct { DataProtectionImpactAssessment struct { @@ -1540,6 +1544,7 @@ func TestProcessingActivity_DPIA(t *testing.T) { } `json:"dataProtectionImpactAssessment"` } `json:"createDataProtectionImpactAssessment"` } + err := owner.Execute(createQuery, map[string]any{ "input": map[string]any{ "processingActivityId": paID, @@ -1548,6 +1553,7 @@ func TestProcessingActivity_DPIA(t *testing.T) { }, }, &createResult) require.NoError(t, err) + dpiaID := createResult.CreateDataProtectionImpactAssessment.DataProtectionImpactAssessment.ID updateQuery := ` @@ -1596,6 +1602,7 @@ func TestProcessingActivity_DPIA(t *testing.T) { } } ` + var createResult struct { CreateDataProtectionImpactAssessment struct { DataProtectionImpactAssessment struct { @@ -1603,6 +1610,7 @@ func TestProcessingActivity_DPIA(t *testing.T) { } `json:"dataProtectionImpactAssessment"` } `json:"createDataProtectionImpactAssessment"` } + err := owner.Execute(createQuery, map[string]any{ "input": map[string]any{ "processingActivityId": paID, @@ -1610,6 +1618,7 @@ func TestProcessingActivity_DPIA(t *testing.T) { }, }, &createResult) require.NoError(t, err) + dpiaID := createResult.CreateDataProtectionImpactAssessment.DataProtectionImpactAssessment.ID deleteQuery := ` @@ -1643,6 +1652,7 @@ func TestProcessingActivity_DPIA(t *testing.T) { } } ` + var readResult struct { Node struct { DataProtectionImpactAssessment *struct { @@ -1650,6 +1660,7 @@ func TestProcessingActivity_DPIA(t *testing.T) { } `json:"dataProtectionImpactAssessment"` } `json:"node"` } + err = owner.Execute(readQuery, map[string]any{"id": paID}, &readResult) require.NoError(t, err) assert.Nil(t, readResult.Node.DataProtectionImpactAssessment) @@ -1816,6 +1827,7 @@ func TestProcessingActivity_TIA(t *testing.T) { } } ` + var createResult struct { CreateTransferImpactAssessment struct { TransferImpactAssessment struct { @@ -1823,6 +1835,7 @@ func TestProcessingActivity_TIA(t *testing.T) { } `json:"transferImpactAssessment"` } `json:"createTransferImpactAssessment"` } + err := owner.Execute(createQuery, map[string]any{ "input": map[string]any{ "processingActivityId": paID, @@ -1831,6 +1844,7 @@ func TestProcessingActivity_TIA(t *testing.T) { }, }, &createResult) require.NoError(t, err) + tiaID := createResult.CreateTransferImpactAssessment.TransferImpactAssessment.ID updateQuery := ` @@ -1883,6 +1897,7 @@ func TestProcessingActivity_TIA(t *testing.T) { } } ` + var createResult struct { CreateTransferImpactAssessment struct { TransferImpactAssessment struct { @@ -1890,6 +1905,7 @@ func TestProcessingActivity_TIA(t *testing.T) { } `json:"transferImpactAssessment"` } `json:"createTransferImpactAssessment"` } + err := owner.Execute(createQuery, map[string]any{ "input": map[string]any{ "processingActivityId": paID, @@ -1897,6 +1913,7 @@ func TestProcessingActivity_TIA(t *testing.T) { }, }, &createResult) require.NoError(t, err) + tiaID := createResult.CreateTransferImpactAssessment.TransferImpactAssessment.ID deleteQuery := ` @@ -1930,6 +1947,7 @@ func TestProcessingActivity_TIA(t *testing.T) { } } ` + var readResult struct { Node struct { TransferImpactAssessment *struct { @@ -1937,6 +1955,7 @@ func TestProcessingActivity_TIA(t *testing.T) { } `json:"transferImpactAssessment"` } `json:"node"` } + err = owner.Execute(readQuery, map[string]any{"id": paID}, &readResult) require.NoError(t, err) assert.Nil(t, readResult.Node.TransferImpactAssessment) @@ -1957,6 +1976,7 @@ func TestProcessingActivity_DPIA_RBAC(t *testing.T) { } `json:"dataProtectionImpactAssessment"` } `json:"createDataProtectionImpactAssessment"` } + err := owner.Execute(` mutation($input: CreateDataProtectionImpactAssessmentInput!) { createDataProtectionImpactAssessment(input: $input) { @@ -1970,6 +1990,7 @@ func TestProcessingActivity_DPIA_RBAC(t *testing.T) { }, }, &createResult) require.NoError(t, err, "owner should be able to create DPIA") + dpiaID := createResult.CreateDataProtectionImpactAssessment.DataProtectionImpactAssessment.ID _, err = owner.Do(` @@ -2012,6 +2033,7 @@ func TestProcessingActivity_DPIA_RBAC(t *testing.T) { } `json:"dataProtectionImpactAssessment"` } `json:"createDataProtectionImpactAssessment"` } + err := admin.Execute(` mutation($input: CreateDataProtectionImpactAssessmentInput!) { createDataProtectionImpactAssessment(input: $input) { @@ -2025,6 +2047,7 @@ func TestProcessingActivity_DPIA_RBAC(t *testing.T) { }, }, &createResult) require.NoError(t, err, "admin should be able to create DPIA") + dpiaID := createResult.CreateDataProtectionImpactAssessment.DataProtectionImpactAssessment.ID _, err = admin.Do(` @@ -2081,6 +2104,7 @@ func TestProcessingActivity_DPIA_RBAC(t *testing.T) { } `json:"dataProtectionImpactAssessment"` } `json:"createDataProtectionImpactAssessment"` } + err = owner.Execute(` mutation($input: CreateDataProtectionImpactAssessmentInput!) { createDataProtectionImpactAssessment(input: $input) { @@ -2094,6 +2118,7 @@ func TestProcessingActivity_DPIA_RBAC(t *testing.T) { }, }, &createResult) require.NoError(t, err) + dpiaID := createResult.CreateDataProtectionImpactAssessment.DataProtectionImpactAssessment.ID _, err = viewer.Do(` @@ -2139,6 +2164,7 @@ func TestProcessingActivity_TIA_RBAC(t *testing.T) { } `json:"transferImpactAssessment"` } `json:"createTransferImpactAssessment"` } + err := owner.Execute(` mutation($input: CreateTransferImpactAssessmentInput!) { createTransferImpactAssessment(input: $input) { @@ -2152,6 +2178,7 @@ func TestProcessingActivity_TIA_RBAC(t *testing.T) { }, }, &createResult) require.NoError(t, err, "owner should be able to create TIA") + tiaID := createResult.CreateTransferImpactAssessment.TransferImpactAssessment.ID _, err = owner.Do(` @@ -2194,6 +2221,7 @@ func TestProcessingActivity_TIA_RBAC(t *testing.T) { } `json:"transferImpactAssessment"` } `json:"createTransferImpactAssessment"` } + err := admin.Execute(` mutation($input: CreateTransferImpactAssessmentInput!) { createTransferImpactAssessment(input: $input) { @@ -2207,6 +2235,7 @@ func TestProcessingActivity_TIA_RBAC(t *testing.T) { }, }, &createResult) require.NoError(t, err, "admin should be able to create TIA") + tiaID := createResult.CreateTransferImpactAssessment.TransferImpactAssessment.ID _, err = admin.Do(` @@ -2263,6 +2292,7 @@ func TestProcessingActivity_TIA_RBAC(t *testing.T) { } `json:"transferImpactAssessment"` } `json:"createTransferImpactAssessment"` } + err = owner.Execute(` mutation($input: CreateTransferImpactAssessmentInput!) { createTransferImpactAssessment(input: $input) { @@ -2276,6 +2306,7 @@ func TestProcessingActivity_TIA_RBAC(t *testing.T) { }, }, &createResult) require.NoError(t, err) + tiaID := createResult.CreateTransferImpactAssessment.TransferImpactAssessment.ID _, err = viewer.Do(` diff --git a/e2e/console/rights_request_test.go b/e2e/console/rights_request_test.go index 0d3f9f836..dcbe13779 100644 --- a/e2e/console/rights_request_test.go +++ b/e2e/console/rights_request_test.go @@ -120,6 +120,7 @@ func TestRightsRequest_Update(t *testing.T) { }, }, &createResult) require.NoError(t, err) + rrID := createResult.CreateRightsRequest.RightsRequestEdge.Node.ID query := ` @@ -208,6 +209,7 @@ func TestRightsRequest_Delete(t *testing.T) { }, }, &createResult) require.NoError(t, err) + rrID := createResult.CreateRightsRequest.RightsRequestEdge.Node.ID query := ` diff --git a/e2e/console/risk_test.go b/e2e/console/risk_test.go index 09ad2d618..543330b26 100644 --- a/e2e/console/risk_test.go +++ b/e2e/console/risk_test.go @@ -402,6 +402,7 @@ func TestRisk_RequiredFields(t *testing.T) { if !tt.skipOrganization { input["organizationId"] = owner.GetOrganizationID().String() } + maps.Copy(input, tt.input) _, err := owner.Do(query, map[string]any{"input": input}) diff --git a/e2e/console/scim_test.go b/e2e/console/scim_test.go index 9b3f7059f..5a39a6287 100644 --- a/e2e/console/scim_test.go +++ b/e2e/console/scim_test.go @@ -112,9 +112,11 @@ func (sc *scimClient) doRequest(method, path string, payload any) (string, int) sc.t.Helper() var body io.Reader + if payload != nil { data, err := json.Marshal(payload) require.NoError(sc.t, err) + body = bytes.NewReader(data) } @@ -122,12 +124,14 @@ func (sc *scimClient) doRequest(method, path string, payload any) (string, int) require.NoError(sc.t, err) req.Header.Set("Authorization", "Bearer "+sc.token) + if payload != nil { req.Header.Set("Content-Type", "application/scim+json") } resp, err := sc.client.Do(req) require.NoError(sc.t, err) + defer func() { _ = resp.Body.Close() }() respBody, err := io.ReadAll(resp.Body) @@ -269,6 +273,7 @@ func TestSCIM_Unauthorized(t *testing.T) { resp, err := client.Do(req) require.NoError(t, err) + defer func() { _ = resp.Body.Close() }() assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) diff --git a/e2e/console/statement_of_applicability_test.go b/e2e/console/statement_of_applicability_test.go index ff9c631b0..f904899d9 100644 --- a/e2e/console/statement_of_applicability_test.go +++ b/e2e/console/statement_of_applicability_test.go @@ -75,12 +75,12 @@ func TestStatementOfApplicability_Create(t *testing.T) { ) require.NoError(t, err) + node := result.CreateStatementOfApplicability.StatementOfApplicabilityEdge.Node assert.NotEmpty(t, node.ID) assert.Equal(t, name, node.Name) }, ) - } func TestStatementOfApplicability_CreateDocument(t *testing.T) { @@ -310,6 +310,7 @@ func TestStatementOfApplicability_CreateDocument(t *testing.T) { ver1Major := result1.PublishStatementOfApplicability.DocumentVersionEdge.Node.Major ver2Major := result2.PublishStatementOfApplicability.DocumentVersionEdge.Node.Major + assert.Equal(t, 1, ver1Major) assert.Equal(t, 2, ver2Major) }, @@ -533,6 +534,7 @@ func TestStatementOfApplicability_UpdateDocumentMetadata(t *testing.T) { documentID, publishedVersionID := publishSOADocument(t) var result updateResult + err := owner.Execute( updateQuery, map[string]any{ @@ -620,6 +622,7 @@ func TestStatementOfApplicability_UpdateDocumentMetadata(t *testing.T) { } var firstPublish publishSOAResult + err := owner.Execute( publishSOAQuery, map[string]any{ @@ -631,11 +634,13 @@ func TestStatementOfApplicability_UpdateDocumentMetadata(t *testing.T) { &firstPublish, ) require.NoError(t, err) + documentID := firstPublish.PublishStatementOfApplicability.DocumentEdge.Node.ID require.Equal(t, "STATEMENT_OF_APPLICABILITY", firstPublish.PublishStatementOfApplicability.DocumentVersionEdge.Node.DocumentType) require.Equal(t, "CONFIDENTIAL", firstPublish.PublishStatementOfApplicability.DocumentVersionEdge.Node.Classification) var editResult updateResult + err = owner.Execute( updateQuery, map[string]any{ @@ -659,6 +664,7 @@ func TestStatementOfApplicability_UpdateDocumentMetadata(t *testing.T) { } } ` + var publishDraft struct { PublishDocument struct { DocumentVersion struct { @@ -672,6 +678,7 @@ func TestStatementOfApplicability_UpdateDocumentMetadata(t *testing.T) { } `json:"documentVersion"` } `json:"publishDocument"` } + err = owner.Execute( publishDraftQuery, map[string]any{ @@ -690,6 +697,7 @@ func TestStatementOfApplicability_UpdateDocumentMetadata(t *testing.T) { require.Equal(t, "INTERNAL", publishDraft.PublishDocument.DocumentVersion.Classification) var rePublish publishSOAResult + err = owner.Execute( publishSOAQuery, map[string]any{ @@ -701,6 +709,7 @@ func TestStatementOfApplicability_UpdateDocumentMetadata(t *testing.T) { &rePublish, ) require.NoError(t, err) + node := rePublish.PublishStatementOfApplicability.DocumentVersionEdge.Node assert.Equal(t, "Custom SOA Name", node.Title, "re-publish should preserve edited title") assert.Equal(t, "POLICY", node.DocumentType, "re-publish should preserve edited type") diff --git a/e2e/console/task_test.go b/e2e/console/task_test.go index 7129e2e57..ff928a74f 100644 --- a/e2e/console/task_test.go +++ b/e2e/console/task_test.go @@ -296,10 +296,12 @@ func TestTask_RequiredFields(t *testing.T) { if !tt.skipOrganization { input["organizationId"] = owner.GetOrganizationID().String() } + for k, v := range tt.input { if v == "placeholder" { continue // Skip placeholder values } + input[k] = v } diff --git a/e2e/console/third_party_test.go b/e2e/console/third_party_test.go index e7e8c35c9..cb7208e82 100644 --- a/e2e/console/third_party_test.go +++ b/e2e/console/third_party_test.go @@ -341,6 +341,7 @@ func TestThirdParty_RequiredFields(t *testing.T) { if !tt.skipOrganization { input["organizationId"] = owner.GetOrganizationID().String() } + maps.Copy(input, tt.input) _, err := owner.Do(query, map[string]any{"input": input}) @@ -1020,6 +1021,7 @@ func TestThirdParty_Assess(t *testing.T) { thirdPartyID := factory.NewThirdParty(owner).WithName("Unconfigured assess").Create() var result resultShape + err := owner.Execute(query, map[string]any{ "input": map[string]any{ "id": thirdPartyID, @@ -1037,6 +1039,7 @@ func TestThirdParty_Assess(t *testing.T) { thirdPartyID := factory.NewThirdParty(owner).WithName("Admin-assessed thirdParty").Create() var result resultShape + err := admin.Execute(query, map[string]any{ "input": map[string]any{ "id": thirdPartyID, @@ -1054,6 +1057,7 @@ func TestThirdParty_Assess(t *testing.T) { thirdPartyID := factory.NewThirdParty(owner).WithName("Viewer attempt").Create() var result resultShape + err := viewer.Execute(query, map[string]any{ "input": map[string]any{ "id": thirdPartyID, @@ -1071,6 +1075,7 @@ func TestThirdParty_Assess(t *testing.T) { thirdPartyID := factory.NewThirdParty(org1Owner).WithName("Org1 thirdParty").Create() var result resultShape + err := org2Owner.Execute(query, map[string]any{ "input": map[string]any{ "id": thirdPartyID, @@ -1087,6 +1092,7 @@ func TestThirdParty_Assess(t *testing.T) { thirdPartyID := factory.NewThirdParty(owner).WithName("Procedure test").Create() var result resultShape + err := owner.Execute(query, map[string]any{ "input": map[string]any{ "id": thirdPartyID, diff --git a/e2e/console/tracker_pattern_test.go b/e2e/console/tracker_pattern_test.go index ad5e1022b..d06b6793e 100644 --- a/e2e/console/tracker_pattern_test.go +++ b/e2e/console/tracker_pattern_test.go @@ -91,6 +91,7 @@ func TestTrackerPattern_Create(t *testing.T) { }, &result) require.NoError(t, err) + node := result.CreateTrackerPattern.TrackerPatternEdge.Node assert.NotEmpty(t, node.ID) assert.Equal(t, "_ga", node.Pattern) @@ -151,6 +152,7 @@ func TestTrackerPattern_Create(t *testing.T) { }, &result) require.NoError(t, err) + node := result.CreateTrackerPattern.TrackerPatternEdge.Node assert.Equal(t, "_gat_*", node.Pattern) assert.Equal(t, "GLOB", node.MatchType) diff --git a/e2e/console/tracker_resource_test.go b/e2e/console/tracker_resource_test.go index 544393fe1..4937a0d75 100644 --- a/e2e/console/tracker_resource_test.go +++ b/e2e/console/tracker_resource_test.go @@ -85,6 +85,7 @@ func TestTrackerResource_Create(t *testing.T) { }, &result) require.NoError(t, err) + node := result.CreateTrackerResource.TrackerResourceEdge.Node assert.NotEmpty(t, node.ID) assert.Equal(t, "SCRIPT", node.Type) diff --git a/e2e/console/user_test.go b/e2e/console/user_test.go index aa291d028..d5e199261 100644 --- a/e2e/console/user_test.go +++ b/e2e/console/user_test.go @@ -71,12 +71,14 @@ func TestUser_UpdateMembership(t *testing.T) { // Find the admin var adminMembershipID string + for _, edge := range result.Node.Profiles.Edges { if edge.Node.Membership.Role == "ADMIN" { adminMembershipID = edge.Node.Membership.ID break } } + require.NotEmpty(t, adminMembershipID, "Should find admin member") // Update the member role to VIEWER @@ -162,12 +164,14 @@ func TestUser_RemoveUser(t *testing.T) { // Find a viewer user to remove var userID string + for _, edge := range result.Node.Profiles.Edges { if edge.Node.Membership.Role == "VIEWER" { userID = edge.Node.ID break } } + assert.NotEmpty(t, userID, "Should find viewer member") // Remove the member @@ -251,12 +255,14 @@ func TestUser_RemoveOwner(t *testing.T) { // Find the other owner (not the calling owner) var targetProfileID string + for _, edge := range result.Node.Profiles.Edges { if edge.Node.Membership.Role == "OWNER" && edge.Node.Identity.ID != owner.GetUserID().String() { targetProfileID = edge.Node.ID break } } + require.NotEmpty(t, targetProfileID, "Should find another owner to remove") mutation := ` diff --git a/e2e/internal/factory/factory.go b/e2e/internal/factory/factory.go index d2779b427..b651cae0c 100644 --- a/e2e/internal/factory/factory.go +++ b/e2e/internal/factory/factory.go @@ -42,9 +42,11 @@ func (a Attrs) get(key string, defaultVal any) any { if a == nil { return defaultVal } + if v, ok := a[key]; ok { return v } + return defaultVal } @@ -52,6 +54,7 @@ func (a Attrs) getString(key string, defaultVal string) string { if v, ok := a.get(key, defaultVal).(string); ok { return v } + return defaultVal } @@ -59,11 +62,13 @@ func (a Attrs) getStringPtr(key string) *string { if a == nil { return nil } + if v, ok := a[key]; ok { if s, ok := v.(string); ok { return &s } } + return nil } @@ -71,6 +76,7 @@ func (a Attrs) getInt(key string, defaultVal int) int { if a == nil { return defaultVal } + if v, ok := a[key]; ok { switch val := v.(type) { case int: @@ -81,6 +87,7 @@ func (a Attrs) getInt(key string, defaultVal int) int { return int(val) } } + return defaultVal } @@ -88,11 +95,13 @@ func (a Attrs) getBool(key string, defaultVal bool) bool { if a == nil { return defaultVal } + if v, ok := a[key]; ok { if b, ok := v.(bool); ok { return b } } + return defaultVal } @@ -167,9 +176,11 @@ func CreateThirdParty(c *testutil.Client, attrs ...Attrs) string { if desc := a.getStringPtr("description"); desc != nil { input["description"] = *desc } + if url := a.getStringPtr("websiteUrl"); url != nil { input["websiteUrl"] = *url } + if cat := a.getStringPtr("category"); cat != nil { input["category"] = *cat } @@ -348,6 +359,7 @@ func CreateTask(c *testutil.Client, measureID *string, attrs ...Attrs) string { if measureID != nil { input["measureId"] = *measureID } + if desc := a.getStringPtr("description"); desc != nil { input["description"] = *desc } @@ -951,6 +963,7 @@ func CreateAccessSource(c *testutil.Client, organizationID string, attrs ...Attr if csvData := a.getStringPtr("csvData"); csvData != nil { input["csvData"] = *csvData } + if connectorID := a.getStringPtr("connectorId"); connectorID != nil { input["connectorId"] = *connectorID } @@ -1446,7 +1459,9 @@ func ReportDetectedResources(c *testutil.Client, bannerID string, count int) { url := fmt.Sprintf("%s/api/cookie-banner/v1/%s/report", c.BaseURL(), bannerID) resp, err := http.Post(url, "application/json", bytes.NewReader(body)) require.NoError(c.T, err, "report detected resources request failed") + defer func() { _ = resp.Body.Close() }() + require.Equal(c.T, http.StatusNoContent, resp.StatusCode, "report detected resources unexpected status") } diff --git a/e2e/internal/testutil/assert.go b/e2e/internal/testutil/assert.go index 43f719f89..25ccbbc73 100644 --- a/e2e/internal/testutil/assert.go +++ b/e2e/internal/testutil/assert.go @@ -80,6 +80,7 @@ func AssertTimestampsOnUpdate(t *testing.T, createdAt, updatedAt, originalCreate func AssertOptionalStringEqual(t *testing.T, expected, actual *string, fieldName string) { t.Helper() + if expected == nil { assert.Nil(t, actual, "%s should be nil", fieldName) } else { @@ -95,6 +96,7 @@ func AssertOrderedAscending[T cmp.Ordered](t *testing.T, values []T, fieldName s func AssertOrderedDescending[T cmp.Ordered](t *testing.T, values []T, fieldName string) { t.Helper() + reversed := slices.Clone(values) slices.Reverse(reversed) assert.True(t, slices.IsSorted(reversed), "%s should be in descending order, got: %v", fieldName, values) @@ -102,6 +104,7 @@ func AssertOrderedDescending[T cmp.Ordered](t *testing.T, values []T, fieldName func AssertTimesOrderedAscending(t *testing.T, times []time.Time, fieldName string) { t.Helper() + isSorted := slices.IsSortedFunc(times, func(a, b time.Time) int { return a.Compare(b) }) @@ -110,6 +113,7 @@ func AssertTimesOrderedAscending(t *testing.T, times []time.Time, fieldName stri func AssertTimesOrderedDescending(t *testing.T, times []time.Time, fieldName string) { t.Helper() + isSorted := slices.IsSortedFunc(times, func(a, b time.Time) int { return b.Compare(a) }) @@ -118,9 +122,11 @@ func AssertTimesOrderedDescending(t *testing.T, times []time.Time, fieldName str func AssertNodeNotAccessible(t *testing.T, err error, nodeIsNil bool, resourceType string) { t.Helper() + if err == nil { assert.True(t, nodeIsNil, "should not be able to access %s from another org", resourceType) } + // If there's an error, that's also acceptable (access denied) } diff --git a/e2e/internal/testutil/client.go b/e2e/internal/testutil/client.go index f652f5607..900c2fdc3 100644 --- a/e2e/internal/testutil/client.go +++ b/e2e/internal/testutil/client.go @@ -32,6 +32,7 @@ import ( func generateUniqueID() string { randomBytes := make([]byte, 4) _, _ = rand.Read(randomBytes) + return fmt.Sprintf("%d-%s", time.Now().UnixNano(), hex.EncodeToString(randomBytes)) } @@ -139,6 +140,7 @@ func (c *Client) SetupTestUserInOrg(ownerClient *Client) { c.userID = identityID c.profileID = profileID ownerClient.inviteUser(profileID) + token := c.getActivationToken(email) passwordToken := c.activateUser(token) c.resetPassword(password, passwordToken) @@ -285,12 +287,14 @@ func (c *Client) updateOwnMembershipRole(role coredata.MembershipRole) { require.NoError(c.T, err, "cannot query organization members") var membershipID string + for _, edge := range qResult.Node.Members.Edges { if edge.Node.Identity.ID == c.userID.String() { membershipID = edge.Node.ID break } } + require.NotEmpty(c.T, membershipID, "membership not found for user") // Update the role @@ -422,6 +426,7 @@ func (c *Client) getActivationToken(email string) string { c.T.Logf("activation token not found") c.T.FailNow() + return "" } diff --git a/e2e/internal/testutil/graphql.go b/e2e/internal/testutil/graphql.go index 3f0a22dd1..521798b53 100644 --- a/e2e/internal/testutil/graphql.go +++ b/e2e/internal/testutil/graphql.go @@ -50,9 +50,11 @@ func (e GraphQLError) Code() string { if e.Extensions == nil { return "" } + if code, ok := e.Extensions["code"].(string); ok { return code } + return "" } @@ -62,9 +64,11 @@ func (e GraphQLErrors) Error() string { if len(e) == 0 { return "" } + if len(e) == 1 { return e[0].Message } + return fmt.Sprintf("%s (and %d more errors)", e[0].Message, len(e)-1) } @@ -83,12 +87,14 @@ func (c *Client) doWithEndpoint(endpoint string, query string, variables map[str if err != nil { return nil, fmt.Errorf("cannot create request: %w", err) } + req.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } + defer func() { _ = resp.Body.Close() }() respBody, err := io.ReadAll(resp.Body) @@ -160,6 +166,7 @@ func (c *Client) ExecuteShouldFail(query string, variables map[string]any) error c.T.Helper() _, err := c.Do(query, variables) require.Error(c.T, err, "expected GraphQL request to fail but it succeeded") + return err } @@ -188,6 +195,7 @@ func (c *Client) ExecuteWithFiles(query string, variables map[string]any, files func (c *Client) executeMultipart(query string, variables map[string]any, files map[string]UploadFile, result any) error { // Create multipart writer using standard library var buf bytes.Buffer + writer := multipart.NewWriter(&buf) // Build the operations JSON @@ -195,6 +203,7 @@ func (c *Client) executeMultipart(query string, variables map[string]any, files "query": query, "variables": variables, } + operationsJSON, err := json.Marshal(operations) if err != nil { return fmt.Errorf("cannot marshal operations: %w", err) @@ -207,14 +216,17 @@ func (c *Client) executeMultipart(query string, variables map[string]any, files // Build the map for file variables (sorted for deterministic order) fileMap := make(map[string][]string) + fileOrder := make([]string, 0, len(files)) for path := range files { fileOrder = append(fileOrder, path) } + // Sort for deterministic ordering for i, path := range fileOrder { fileMap[fmt.Sprintf("%d", i)] = []string{"variables." + path} } + mapJSON, err := json.Marshal(fileMap) if err != nil { return fmt.Errorf("cannot marshal map: %w", err) @@ -239,6 +251,7 @@ func (c *Client) executeMultipart(query string, variables map[string]any, files if err != nil { return fmt.Errorf("cannot create file part %s: %w", path, err) } + if _, err := part.Write(file.Content); err != nil { return fmt.Errorf("cannot write file content %s: %w", path, err) } @@ -253,6 +266,7 @@ func (c *Client) executeMultipart(query string, variables map[string]any, files if err != nil { return fmt.Errorf("cannot create request: %w", err) } + req.Header.Set("Content-Type", writer.FormDataContentType()) // Execute request @@ -260,6 +274,7 @@ func (c *Client) executeMultipart(query string, variables map[string]any, files if err != nil { return fmt.Errorf("request failed: %w", err) } + defer func() { _ = resp.Body.Close() }() respBody, err := io.ReadAll(resp.Body) diff --git a/e2e/internal/testutil/mailpit.go b/e2e/internal/testutil/mailpit.go index 9fdfc1d09..ad8466929 100644 --- a/e2e/internal/testutil/mailpit.go +++ b/e2e/internal/testutil/mailpit.go @@ -45,12 +45,14 @@ func (c *Client) SearchMails(query string) (*MailpitSearchResponse, error) { if err != nil { return nil, fmt.Errorf("cannot create request: %w", err) } + req.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } + defer func() { _ = resp.Body.Close() }() respBody, err := io.ReadAll(resp.Body) @@ -75,12 +77,14 @@ func (c *Client) CheckMessageLinks(messageID string) (*MailpitLinkCheckResponse, if err != nil { return nil, fmt.Errorf("cannot create request: %w", err) } + req.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } + defer func() { _ = resp.Body.Close() }() respBody, err := io.ReadAll(resp.Body) diff --git a/e2e/internal/testutil/mcp.go b/e2e/internal/testutil/mcp.go index 0ef1e3d6d..355352147 100644 --- a/e2e/internal/testutil/mcp.go +++ b/e2e/internal/testutil/mcp.go @@ -128,6 +128,7 @@ func (mc *MCPClient) doRequest(method string, params any) (json.RawMessage, erro req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json, text/event-stream") req.Header.Set("Authorization", "Bearer "+mc.apiToken) + if mc.sessionID != "" { req.Header.Set("Mcp-Session-Id", mc.sessionID) } @@ -136,6 +137,7 @@ func (mc *MCPClient) doRequest(method string, params any) (json.RawMessage, erro if err != nil { return nil, fmt.Errorf("request failed: %w", err) } + defer func() { _ = resp.Body.Close() }() respBody, err := io.ReadAll(resp.Body) @@ -198,6 +200,7 @@ func (mc *MCPClient) CallTool(toolName string, args map[string]any) *MCPToolResu require.NoError(mc.t, err, "MCP tools/call %s failed", toolName) var toolResult MCPToolResult + err = json.Unmarshal(result, &toolResult) require.NoError(mc.t, err, "cannot unmarshal tool result for %s", toolName) @@ -212,6 +215,7 @@ func (mc *MCPClient) CallToolExpectToolError(toolName string, args map[string]an require.NotEmpty(mc.t, tr.Content, "tool %s returned no content", toolName) var text string + err := json.Unmarshal(tr.Content[0].Text, &text) require.NoError(mc.t, err, "cannot unmarshal error text for %s", toolName) @@ -227,6 +231,7 @@ func (mc *MCPClient) CallToolInto(toolName string, args map[string]any, dest any // The text field in MCP content is a JSON-encoded string of the output. // First unmarshal the raw JSON to get the string. var textStr string + err := json.Unmarshal(tr.Content[0].Text, &textStr) require.NoError(mc.t, err, "cannot unmarshal text content for %s", toolName) diff --git a/e2e/internal/testutil/oauth2.go b/e2e/internal/testutil/oauth2.go index 7ac50d78b..752b7b6cb 100644 --- a/e2e/internal/testutil/oauth2.go +++ b/e2e/internal/testutil/oauth2.go @@ -129,6 +129,7 @@ func postForm( if err != nil { return nil, fmt.Errorf("cannot post form: %w", err) } + defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) @@ -153,6 +154,7 @@ func postJSON( if err != nil { return nil, fmt.Errorf("cannot post json: %w", err) } + defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) @@ -181,6 +183,7 @@ func getJSON( if err != nil { return nil, fmt.Errorf("cannot execute request: %w", err) } + defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) @@ -213,6 +216,7 @@ func postFormWithBasicAuth( if err != nil { return nil, fmt.Errorf("cannot execute request: %w", err) } + defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) @@ -298,10 +302,12 @@ func OAuth2Authorize( } reqURL := oauth2BaseURL(c) + "/authorize?" + params.Encode() + resp, err := noRedirectClient.Get(reqURL) if err != nil { return nil, fmt.Errorf("cannot get authorize: %w", err) } + defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) @@ -521,6 +527,7 @@ func OAuth2TokenWithDeviceCode( if err := json.Unmarshal(raw.Body, &result); err != nil { return nil, nil, raw, fmt.Errorf("cannot decode token response: %w", err) } + return &result, nil, raw, nil } @@ -799,6 +806,7 @@ func GeneratePKCE() (verifier, challenge string) { for i := range b { b[i] = charset[rand.IntN(len(charset))] } + verifier = string(b) h := sha256.Sum256([]byte(verifier)) @@ -814,11 +822,14 @@ func IsConsentRedirect(resp *OAuth2HTTPResponse) bool { if resp.StatusCode != http.StatusFound { return false } + loc := resp.Header.Get("Location") + u, err := url.Parse(loc) if err != nil { return false } + return u.Query().Get("consent_id") != "" } @@ -831,14 +842,17 @@ func ExtractConsentIDFromResponse(resp *OAuth2HTTPResponse) (string, error) { if loc == "" { return "", fmt.Errorf("no Location header in redirect response") } + u, err := url.Parse(loc) if err != nil { return "", fmt.Errorf("cannot parse redirect url: %w", err) } + consentID := u.Query().Get("consent_id") if consentID == "" { return "", fmt.Errorf("no consent_id in redirect url: %s", loc) } + return consentID, nil } @@ -850,12 +864,14 @@ func ExtractConsentID(body []byte) (string, error) { s := string(body) needle := `name="consent_id" value="` + idx := strings.Index(s, needle) if idx == -1 { return "", fmt.Errorf("consent_id not found in page") } start := idx + len(needle) + end := strings.Index(s[start:], `"`) if end == -1 { return "", fmt.Errorf("malformed consent_id value") @@ -890,6 +906,7 @@ func OAuth2PerformAuthorizationCodeFlow( require.NoError(t, err) var code string + if IsConsentRedirect(authResp) { consentID, err := ExtractConsentIDFromResponse(authResp) require.NoError(t, err) diff --git a/e2e/internal/testutil/prosemirror.go b/e2e/internal/testutil/prosemirror.go index 90efe8c36..38c33064d 100644 --- a/e2e/internal/testutil/prosemirror.go +++ b/e2e/internal/testutil/prosemirror.go @@ -32,9 +32,11 @@ func ProseMirrorTextDoc(text string) string { }, }, } + b, err := json.Marshal(doc) if err != nil { panic(err) } + return string(b) } diff --git a/e2e/internal/testutil/testutil.go b/e2e/internal/testutil/testutil.go index c78bc22d2..d69c8ec5f 100644 --- a/e2e/internal/testutil/testutil.go +++ b/e2e/internal/testutil/testutil.go @@ -54,6 +54,7 @@ func (s *switchableWriter) Write(p []byte) (int, error) { s.mu.Lock() w := s.w s.mu.Unlock() + return w.Write(p) } @@ -104,6 +105,7 @@ func Setup() { cmd.Stderr = os.Stderr } else { var buf bytes.Buffer + testEnv.outputBuf = &buf sw := &switchableWriter{w: &buf} testEnv.outputWriter = sw @@ -128,14 +130,18 @@ func Setup() { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() + if err := waitForServer(ctx, testEnv.BaseURL+"/api/console/v1/graphql", 30*time.Second); err != nil { testEnv.dumpOutputOnFailure("API server failed to start", err) _ = testEnv.cmd.Process.Kill() + os.Exit(1) } + if err := waitForServer(ctx, testEnv.MailpitBaseURL+"/api/v1/messages", 30*time.Second); err != nil { testEnv.dumpOutputOnFailure("MailPit server failed to start", err) _ = testEnv.cmd.Process.Kill() + os.Exit(1) } @@ -161,11 +167,13 @@ func (e *TestEnv) dumpOutputOnFailure(context string, err error) { if e.outputBuf != nil && e.outputBuf.Len() > 0 { output := e.outputBuf.Bytes() + const maxTail = 10_000 if len(output) > maxTail { fmt.Fprintf(os.Stderr, "e2etest: (showing last %d bytes of output)\n", maxTail) output = output[len(output)-maxTail:] } + fmt.Fprintf(os.Stderr, "--- probod output start ---\n%s\n--- probod output end ---\n", output) } else { fmt.Fprintf(os.Stderr, "e2etest: no captured output available\n") @@ -224,6 +232,7 @@ func GetBaseURL() string { if testEnv == nil { return "http://localhost:8080" } + return testEnv.BaseURL } @@ -231,6 +240,7 @@ func GetMailpitBaseURL() string { if testEnv == nil { return "http://localhost:8025" } + return testEnv.MailpitBaseURL } @@ -305,6 +315,7 @@ func generateConfig() (string, error) { if v, ok := env[key]; ok { return v } + return os.Getenv(key) }) @@ -317,6 +328,7 @@ func generateConfig() (string, error) { if err != nil { return "", fmt.Errorf("create temp dir: %w", err) } + path := filepath.Join(tmpDir, "probod.yml") if err := bootstrap.WriteConfig(cfg, path); err != nil { diff --git a/e2e/mcp/main_test.go b/e2e/mcp/main_test.go index 369e3cfe8..eb978a62b 100644 --- a/e2e/mcp/main_test.go +++ b/e2e/mcp/main_test.go @@ -23,7 +23,9 @@ import ( func TestMain(m *testing.M) { testutil.Setup() + code := m.Run() + testutil.Teardown() os.Exit(code) } diff --git a/e2e/mcp/third_party_contact_test.go b/e2e/mcp/third_party_contact_test.go index 41105c075..d8e766cfb 100644 --- a/e2e/mcp/third_party_contact_test.go +++ b/e2e/mcp/third_party_contact_test.go @@ -127,6 +127,7 @@ func TestMCP_ListThirdPartyContacts(t *testing.T) { "email": factory.SafeEmail(), }, &result) require.NotEmpty(t, result.ThirdPartyContact.ID) + _ = i } diff --git a/e2e/mcp/third_party_service_test.go b/e2e/mcp/third_party_service_test.go index 82c6a7027..77aed8ebf 100644 --- a/e2e/mcp/third_party_service_test.go +++ b/e2e/mcp/third_party_service_test.go @@ -120,6 +120,7 @@ func TestMCP_ListThirdPartyServices(t *testing.T) { "name": factory.SafeName("Service"), }, &result) require.NotEmpty(t, result.ThirdPartyService.ID) + _ = i } diff --git a/e2e/mcp/third_party_test.go b/e2e/mcp/third_party_test.go index 080e11267..b9fdc841a 100644 --- a/e2e/mcp/third_party_test.go +++ b/e2e/mcp/third_party_test.go @@ -36,6 +36,7 @@ func TestMCP_ThirdParty_CRUD(t *testing.T) { Name string `json:"name"` } `json:"third_party"` } + name := factory.SafeName("ThirdParty") mc.CallToolInto("addThirdParty", map[string]any{ "organizationId": orgID, diff --git a/e2e/mcp/trust_center_test.go b/e2e/mcp/trust_center_test.go index f3554fca8..819d36b84 100644 --- a/e2e/mcp/trust_center_test.go +++ b/e2e/mcp/trust_center_test.go @@ -220,6 +220,7 @@ func TestMCP_ListTrustCenterReferences(t *testing.T) { "url": "https://example.com/" + factory.SafeName("path"), }, &result) require.NotEmpty(t, result.TrustCenterReference.ID) + _ = i } @@ -395,6 +396,7 @@ func TestMCP_ListComplianceExternalURLs(t *testing.T) { "url": "https://example.com/" + factory.SafeName("path"), }, &result) require.NotEmpty(t, result.ComplianceExternalURL.ID) + _ = i } diff --git a/internal/cmd/genmodels/main.go b/internal/cmd/genmodels/main.go index 7be707fe4..7d67a4e6a 100644 --- a/internal/cmd/genmodels/main.go +++ b/internal/cmd/genmodels/main.go @@ -96,6 +96,7 @@ func main() { fmt.Fprintf(os.Stderr, "cannot fetch models: %v\n", err) os.Exit(1) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { @@ -126,6 +127,7 @@ var generatedModels = map[string]ModelDefinition{ ) var count int + for _, m := range result.Data { provider := providerFromID(m.ID) if _, ok := includedProviders[provider]; !ok { @@ -148,6 +150,7 @@ var generatedModels = map[string]ModelDefinition{ m.TopProvider.MaxCompletionTokens, buildSupports(m.SupportedParams), ) + count++ } @@ -172,11 +175,13 @@ func providerFromID(id string) string { if ok { return provider } + return "" } func buildSupports(params []string) string { fields := make(map[string]bool) + for _, p := range params { if field, ok := paramFieldMap[p]; ok { fields[field] = true @@ -184,6 +189,7 @@ func buildSupports(params []string) string { } var buf strings.Builder + fieldNames := []string{ "Temperature", "TopP", "TopK", "FrequencyPenalty", "PresencePenalty", "Stop", "Seed", "MaxTokens", "ToolChoice", "ParallelToolCalls", @@ -192,5 +198,6 @@ func buildSupports(params []string) string { for _, f := range fieldNames { fmt.Fprintf(&buf, " %s: %t,\n", f, fields[f]) } + return buf.String() } diff --git a/packages/emails/emails.go b/packages/emails/emails.go index 9fcd5cad7..19c0bf915 100644 --- a/packages/emails/emails.go +++ b/packages/emails/emails.go @@ -179,6 +179,7 @@ func UploadStaticAssets(ctx context.Context, s3Client *s3.Client, staticAssetsBu if err != nil { return err } + defer func() { _ = file.Close() }() _, err = s3Client.PutObject( @@ -200,7 +201,6 @@ func UploadStaticAssets(ctx context.Context, s3Client *s3.Client, staticAssetsBu return nil }) - if err != nil { return fmt.Errorf("cannot generate asset URLs: %w", err) } @@ -261,6 +261,7 @@ func (p *Presenter) getCommonVariables(ctx context.Context) (*CommonVariables, e if err != nil { return nil, fmt.Errorf("cannot generate probo logo URL: %w", err) } + senderCompanyLogoURL, err := p.fm.GenerateFileUrl(ctx, &p.config.SenderCompanyLogo, staticAssetsDuration) if err != nil { return nil, fmt.Errorf("cannot generate sender logo URL: %w", err) @@ -298,6 +299,7 @@ func (p *Presenter) RenderConfirmEmail(ctx context.Context, confirmationURLPath } textBody, htmlBody, err = renderEmail(confirmEmailTextTemplate, confirmEmailHTMLTemplate, data) + return subjectConfirmEmail, textBody, htmlBody, err } @@ -322,6 +324,7 @@ func (p *Presenter) RenderPasswordReset(ctx context.Context, resetPasswordURLPat } textBody, htmlBody, err = renderEmail(passwordResetTextTemplate, passwordResetHTMLTemplate, data) + return subjectPasswordReset, textBody, htmlBody, err } @@ -348,6 +351,7 @@ func (p *Presenter) RenderInvitation(ctx context.Context, invitationURLPath stri } textBody, htmlBody, err = renderEmail(invitationTextTemplate, invitationHTMLTemplate, data) + return fmt.Sprintf(subjectInvitation, organizationName), textBody, htmlBody, err } @@ -381,6 +385,7 @@ func (p *Presenter) RenderDocumentApproval( } textBody, htmlBody, err = renderEmail(documentApprovalTextTemplate, documentApprovalHTMLTemplate, data) + return fmt.Sprintf(subjectDocumentApproval, documentName), textBody, htmlBody, err } @@ -411,6 +416,7 @@ func (p *Presenter) RenderDocumentSigning( } textBody, htmlBody, err = renderEmail(documentSigningTextTemplate, documentSigningHTMLTemplate, data) + return fmt.Sprintf(subjectDocumentSigning, organizationName), textBody, htmlBody, err } @@ -429,6 +435,7 @@ func (p *Presenter) RenderDocumentExport(ctx context.Context, downloadUrl string } textBody, htmlBody, err = renderEmail(documentExportTextTemplate, documentExportHTMLTemplate, data) + return subjectDocumentExport, textBody, htmlBody, err } @@ -447,6 +454,7 @@ func (p *Presenter) RenderFrameworkExport(ctx context.Context, downloadUrl strin } textBody, htmlBody, err = renderEmail(frameworkExportTextTemplate, frameworkExportHTMLTemplate, data) + return subjectFrameworkExport, textBody, htmlBody, err } @@ -465,6 +473,7 @@ func (p *Presenter) RenderTrustCenterAccess(ctx context.Context, organizationNam } textBody, htmlBody, err = renderEmail(trustCenterAccessTextTemplate, trustCenterAccessHTMLTemplate, data) + return fmt.Sprintf(subjectTrustCenterAccess, organizationName), textBody, htmlBody, err } @@ -489,6 +498,7 @@ func (p *Presenter) RenderTrustCenterDocumentAccessRejected( } textBody, htmlBody, err = renderEmail(trustCenterDocumentAccessRejectedTextTemplate, trustCenterDocumentAccessRejectedHTMLTemplate, data) + return fmt.Sprintf(subjectTrustCenterDocumentAccessRejected, organizationName), textBody, htmlBody, err } @@ -511,6 +521,7 @@ func (p *Presenter) RenderMagicLink(ctx context.Context, magicLinkUrlPath string } textBody, htmlBody, err = renderEmail(magicLinkTextTemplate, magicLinkHTMLTemplate, data) + return fmt.Sprintf(subjectMagicLink, organizationName), textBody, htmlBody, err } @@ -531,6 +542,7 @@ func (p *Presenter) RenderElectronicSignatureCertificate(ctx context.Context, si } textBody, htmlBody, err = renderEmail(electronicSignatureCertificateTextTemplate, electronicSignatureCertificateHTMLTemplate, data) + return fmt.Sprintf(subjectElectronicSignatureCertificate, documentName), textBody, htmlBody, err } @@ -617,12 +629,14 @@ func renderEmail(textTemplate *texttemplate.Template, htmlTemplate *htmltemplate if err := textTemplate.Execute(&textBuf, data); err != nil { return "", nil, fmt.Errorf("cannot execute text template: %w", err) } + textBody = textBuf.String() var htmlBuf bytes.Buffer if err := htmlTemplate.Execute(&htmlBuf, data); err != nil { return "", nil, fmt.Errorf("cannot execute html template: %w", err) } + htmlBodyStr := htmlBuf.String() htmlBody = &htmlBodyStr diff --git a/pkg/accessreview/access_entry_service.go b/pkg/accessreview/access_entry_service.go index d20a991e5..dae3e4e26 100644 --- a/pkg/accessreview/access_entry_service.go +++ b/pkg/accessreview/access_entry_service.go @@ -102,13 +102,16 @@ func (s AccessEntryService) RecordDecision( entry.DecisionNote = req.DecisionNote entry.DecidedBy = req.DecidedByID entry.DecidedAt = &now + entry.UpdatedAt = now if entry.Flags == nil { entry.Flags = []coredata.AccessEntryFlag{} } + if entry.FlagReasons == nil { entry.FlagReasons = []string{} } + if req.Decision == coredata.AccessEntryDecisionRevoke || req.Decision == coredata.AccessEntryDecisionEscalate { if len(entry.Flags) == 0 { entry.Flags = []coredata.AccessEntryFlag{coredata.AccessEntryFlagExcessive} @@ -156,6 +159,7 @@ func (s AccessEntryService) RecordDecisions( if d.Decision == coredata.AccessEntryDecisionPending { return nil, fmt.Errorf("cannot bulk decide access entries: invalid decision %q", d.Decision) } + if d.Decision != coredata.AccessEntryDecisionApproved { if d.DecisionNote == nil || strings.TrimSpace(*d.DecisionNote) == "" { return nil, fmt.Errorf( @@ -189,9 +193,11 @@ func (s AccessEntryService) RecordDecisions( if err := campaign.LoadByID(ctx, conn, s.scope, entry.AccessReviewCampaignID); err != nil { return fmt.Errorf("cannot load campaign: %w", err) } + if campaign.Status != coredata.AccessReviewCampaignStatusPendingActions { return fmt.Errorf("cannot decide access entry: campaign status is %s, expected PENDING_ACTIONS", campaign.Status) } + verifiedCampaigns[entry.AccessReviewCampaignID] = true } @@ -200,13 +206,16 @@ func (s AccessEntryService) RecordDecisions( entry.DecisionNote = d.DecisionNote entry.DecidedBy = d.DecidedByID entry.DecidedAt = &now + entry.UpdatedAt = now if entry.Flags == nil { entry.Flags = []coredata.AccessEntryFlag{} } + if entry.FlagReasons == nil { entry.FlagReasons = []string{} } + if d.Decision == coredata.AccessEntryDecisionRevoke || d.Decision == coredata.AccessEntryDecisionEscalate { if len(entry.Flags) == 0 { entry.Flags = []coredata.AccessEntryFlag{coredata.AccessEntryFlagExcessive} @@ -245,6 +254,7 @@ func (s AccessEntryService) RecordDecisions( if err != nil { return nil, fmt.Errorf("cannot reload access entry %s: %w", id, err) } + entries[i] = entry } @@ -274,14 +284,17 @@ func (s AccessEntryService) FlagEntry( } now := time.Now() + entry.Flags = req.Flags if entry.Flags == nil { entry.Flags = []coredata.AccessEntryFlag{} } + entry.FlagReasons = req.FlagReasons if entry.FlagReasons == nil { entry.FlagReasons = []string{} } + entry.UpdatedAt = now return entry.UpdateFlags(ctx, conn, s.scope) @@ -348,10 +361,12 @@ func (s AccessEntryService) CountForCampaignID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { entries := coredata.AccessEntries{} + count, err = entries.CountByCampaignID(ctx, conn, s.scope, campaignID, filter) if err != nil { return fmt.Errorf("cannot count access entries by campaign: %w", err) } + return nil }, ) @@ -374,10 +389,12 @@ func (s AccessEntryService) CountForCampaignIDAndSourceID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { entries := coredata.AccessEntries{} + count, err = entries.CountByCampaignIDAndSourceID(ctx, conn, s.scope, campaignID, sourceID, filter) if err != nil { return fmt.Errorf("cannot count access entries by campaign and source: %w", err) } + return nil }, ) @@ -398,10 +415,12 @@ func (s AccessEntryService) CountPendingForCampaignID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { entries := coredata.AccessEntries{} + count, err = entries.CountPendingByCampaignID(ctx, conn, s.scope, campaignID) if err != nil { return fmt.Errorf("cannot count pending access entries: %w", err) } + return nil }, ) diff --git a/pkg/accessreview/access_source_service.go b/pkg/accessreview/access_source_service.go index 75d0656d3..d119c83c7 100644 --- a/pkg/accessreview/access_source_service.go +++ b/pkg/accessreview/access_source_service.go @@ -188,6 +188,7 @@ func (s AccessSourceService) Update( return fmt.Errorf("cannot load connector: %w", err) } } + source.ConnectorID = *req.ConnectorID } @@ -256,6 +257,7 @@ func (s AccessSourceService) CountForOrganizationID( func(ctx context.Context, conn pg.Querier) (err error) { sources := coredata.AccessSources{} count, err = sources.CountByOrganizationID(ctx, conn, s.scope, organizationID) + return err }, ) @@ -300,6 +302,7 @@ func (s AccessSourceService) ConnectorHTTPClient( if err := dbConnector.LoadByID(ctx, conn, s.scope, connectorID, s.encryptionKey); err != nil { return fmt.Errorf("cannot load connector: %w", err) } + return nil }, ) @@ -308,16 +311,19 @@ func (s AccessSourceService) ConnectorHTTPClient( } var tokenBefore string + oauth2Conn, isOAuth2 := dbConnector.Connection.(*connector.OAuth2Connection) if isOAuth2 { tokenBefore = oauth2Conn.AccessToken } var httpClient *http.Client + if isOAuth2 && s.connectorRegistry != nil { refreshCfg := s.connectorRegistry.GetOAuth2RefreshConfig(string(dbConnector.Provider)) if refreshCfg != nil { var err error + httpClient, err = oauth2Conn.RefreshableClient(ctx, *refreshCfg) if err != nil { return nil, nil, fmt.Errorf("cannot create refreshable HTTP client: %w", err) @@ -327,6 +333,7 @@ func (s AccessSourceService) ConnectorHTTPClient( if httpClient == nil { var err error + httpClient, err = dbConnector.Connection.Client(ctx) if err != nil { return nil, nil, fmt.Errorf("cannot create HTTP client: %w", err) @@ -336,6 +343,7 @@ func (s AccessSourceService) ConnectorHTTPClient( // Persist refreshed token if it changed. if isOAuth2 && oauth2Conn.AccessToken != tokenBefore { dbConnector.UpdatedAt = time.Now() + if err := s.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { diff --git a/pkg/accessreview/campaign_service.go b/pkg/accessreview/campaign_service.go index 442e151b9..ed858d799 100644 --- a/pkg/accessreview/campaign_service.go +++ b/pkg/accessreview/campaign_service.go @@ -105,6 +105,7 @@ func (s *CampaignService) Get( if err := campaign.LoadByID(ctx, conn, s.scope, campaignID); err != nil { return fmt.Errorf("cannot load campaign: %w", err) } + return nil }, ) @@ -192,6 +193,7 @@ func (s *CampaignService) Delete( if err := campaign.Delete(ctx, conn, s.scope); err != nil { return fmt.Errorf("cannot delete campaign: %w", err) } + return nil }, ) @@ -359,6 +361,7 @@ func (s *CampaignService) Close( } entries := coredata.AccessEntries{} + pendingCount, err := entries.CountPendingByCampaignID(ctx, conn, s.scope, campaignID) if err != nil { return fmt.Errorf("cannot count pending entries: %w", err) @@ -392,6 +395,7 @@ func lockCampaignForUpdate(ctx context.Context, tx pg.Tx, scope coredata.Scoper, if err := c.LockForUpdate(ctx, tx, scope); err != nil { return fmt.Errorf("cannot lock campaign for update: %w", err) } + return nil } @@ -402,6 +406,7 @@ func (s *CampaignService) enqueueSourceFetches( sources coredata.AccessSources, ) error { now := time.Now() + for _, source := range sources { fetch := &coredata.AccessReviewCampaignSourceFetch{ AccessReviewCampaignID: campaignID, @@ -469,6 +474,7 @@ func (s *CampaignService) ListForOrganizationID( if err := campaigns.LoadByOrganizationID(ctx, conn, s.scope, organizationID, cursor); err != nil { return fmt.Errorf("cannot load campaigns by organization: %w", err) } + return nil }, ) @@ -491,6 +497,7 @@ func (s *CampaignService) ListSourceFetches( if err := fetches.LoadByCampaignID(ctx, conn, s.scope, campaignID); err != nil { return fmt.Errorf("cannot load source fetches by campaign: %w", err) } + return nil }, ) @@ -511,6 +518,7 @@ func (s *CampaignService) CountForOrganizationID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { campaigns := coredata.AccessReviewCampaigns{} + count, err = campaigns.CountByOrganizationID(ctx, conn, s.scope, organizationID) if err != nil { return fmt.Errorf("cannot count campaigns by organization: %w", err) diff --git a/pkg/accessreview/drivers/asana.go b/pkg/accessreview/drivers/asana.go index 428902801..1b4f3374e 100644 --- a/pkg/accessreview/drivers/asana.go +++ b/pkg/accessreview/drivers/asana.go @@ -98,6 +98,7 @@ func (d *AsanaDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error) if page.NextPage == nil || page.NextPage.URI == "" { return records, nil } + next = page.NextPage.URI } @@ -109,12 +110,14 @@ func (d *AsanaDriver) queryUsers(ctx context.Context, endpoint string) (*asanaUs if err != nil { return nil, fmt.Errorf("cannot create asana users request: %w", err) } + req.Header.Set("Accept", "application/json") httpResp, err := d.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("cannot execute asana users request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { diff --git a/pkg/accessreview/drivers/bitbucket.go b/pkg/accessreview/drivers/bitbucket.go index 01e4119f8..7f0bbdd97 100644 --- a/pkg/accessreview/drivers/bitbucket.go +++ b/pkg/accessreview/drivers/bitbucket.go @@ -110,12 +110,14 @@ func (d *BitbucketDriver) queryMembers(ctx context.Context, endpoint string) (*b if err != nil { return nil, fmt.Errorf("cannot create bitbucket members request: %w", err) } + req.Header.Set("Accept", "application/json") httpResp, err := d.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("cannot execute bitbucket members request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { diff --git a/pkg/accessreview/drivers/brex.go b/pkg/accessreview/drivers/brex.go index 5f799ad7f..64249c9ca 100644 --- a/pkg/accessreview/drivers/brex.go +++ b/pkg/accessreview/drivers/brex.go @@ -84,6 +84,7 @@ func (d *BrexDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error) if resp.NextCursor == "" { return records, nil } + nextCursor := resp.NextCursor cursor = &nextCursor } @@ -110,6 +111,7 @@ func (d *BrexDriver) queryUsers(ctx context.Context, cursor *string) (*brexUsers if err != nil { return nil, fmt.Errorf("cannot execute brex users request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() diff --git a/pkg/accessreview/drivers/cassette_safety_test.go b/pkg/accessreview/drivers/cassette_safety_test.go index eacce9d0e..2a2101de9 100644 --- a/pkg/accessreview/drivers/cassette_safety_test.go +++ b/pkg/accessreview/drivers/cassette_safety_test.go @@ -82,6 +82,7 @@ func TestCassettesUseSyntheticEmails(t *testing.T) { if seen[email] { continue } + seen[email] = true domain := email[strings.IndexByte(email, '@')+1:] @@ -90,6 +91,7 @@ func TestCassettesUseSyntheticEmails(t *testing.T) { } ok := false + for _, suffix := range allowedDomainSuffixes { if strings.HasSuffix("."+domain, suffix) || domain == strings.TrimPrefix(suffix, ".") { ok = true diff --git a/pkg/accessreview/drivers/clickup.go b/pkg/accessreview/drivers/clickup.go index 10862ae81..6d2b41f6e 100644 --- a/pkg/accessreview/drivers/clickup.go +++ b/pkg/accessreview/drivers/clickup.go @@ -77,12 +77,14 @@ func (d *ClickUpDriver) ListAccounts(ctx context.Context) ([]AccountRecord, erro if err != nil { return nil, fmt.Errorf("cannot create clickup team request: %w", err) } + req.Header.Set("Accept", "application/json") httpResp, err := d.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("cannot execute clickup team request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { @@ -159,5 +161,6 @@ func parseClickUpTime(raw string) (time.Time, error) { if err != nil { return time.Time{}, fmt.Errorf("cannot parse clickup time %q: %w", raw, err) } + return time.UnixMilli(ms).UTC(), nil } diff --git a/pkg/accessreview/drivers/cloudflare.go b/pkg/accessreview/drivers/cloudflare.go index a0a721dbc..f0dbda8d3 100644 --- a/pkg/accessreview/drivers/cloudflare.go +++ b/pkg/accessreview/drivers/cloudflare.go @@ -131,6 +131,7 @@ func (d *CloudflareDriver) queryAccounts(ctx context.Context, page int) (*cloudf if err != nil { return nil, fmt.Errorf("cannot execute cloudflare accounts request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() @@ -168,6 +169,7 @@ func (d *CloudflareDriver) queryAllMembers(ctx context.Context, accountID string } isAdmin := false + for _, r := range m.Roles { if r.Name == "Super Administrator - All Privileges" || r.Name == "Administrator" { isAdmin = true @@ -224,6 +226,7 @@ func (d *CloudflareDriver) queryMembers(ctx context.Context, accountID string, p if err != nil { return nil, fmt.Errorf("cannot execute cloudflare members request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() diff --git a/pkg/accessreview/drivers/csv.go b/pkg/accessreview/drivers/csv.go index 3e0945e95..e6cef5d85 100644 --- a/pkg/accessreview/drivers/csv.go +++ b/pkg/accessreview/drivers/csv.go @@ -51,6 +51,7 @@ func (d *CSVDriver) ListAccounts(_ context.Context) ([]AccountRecord, error) { for i, col := range header { colIndex[strings.TrimSpace(strings.ToLower(col))] = i } + if _, ok := colIndex["email"]; !ok { return nil, fmt.Errorf("cannot parse CSV: missing required column email") } @@ -62,6 +63,7 @@ func (d *CSVDriver) ListAccounts(_ context.Context) ([]AccountRecord, error) { if err == io.EOF { break } + if err != nil { return nil, fmt.Errorf("cannot read CSV row: %w", err) } @@ -75,24 +77,31 @@ func (d *CSVDriver) ListAccounts(_ context.Context) ([]AccountRecord, error) { if idx, ok := colIndex["email"]; ok && idx < len(row) { record.Email = strings.TrimSpace(row[idx]) } + if idx, ok := colIndex["full_name"]; ok && idx < len(row) { record.FullName = strings.TrimSpace(row[idx]) } + if idx, ok := colIndex["role"]; ok && idx < len(row) { record.Role = strings.TrimSpace(row[idx]) } + if idx, ok := colIndex["job_title"]; ok && idx < len(row) { record.JobTitle = strings.TrimSpace(row[idx]) } + if idx, ok := colIndex["is_admin"]; ok && idx < len(row) { record.IsAdmin = strings.TrimSpace(strings.ToLower(row[idx])) == "true" } + if idx, ok := colIndex["active"]; ok && idx < len(row) { record.Active = new(strings.TrimSpace(strings.ToLower(row[idx])) == "true") } + if idx, ok := colIndex["external_id"]; ok && idx < len(row) { record.ExternalID = strings.TrimSpace(row[idx]) } + if idx, ok := colIndex["account_type"]; ok && idx < len(row) { if strings.TrimSpace(strings.ToUpper(row[idx])) == "SERVICE_ACCOUNT" { record.AccountType = coredata.AccessEntryAccountTypeServiceAccount diff --git a/pkg/accessreview/drivers/csv_test.go b/pkg/accessreview/drivers/csv_test.go index 145085ce5..23bd48013 100644 --- a/pkg/accessreview/drivers/csv_test.go +++ b/pkg/accessreview/drivers/csv_test.go @@ -24,6 +24,7 @@ func TestCSVDriverRequiresEmailHeader(t *testing.T) { t.Parallel() driver := NewCSVDriver(strings.NewReader("full_name,role\nJane Doe,Admin\n")) + _, err := driver.ListAccounts(context.Background()) if err == nil { t.Fatalf("expected error when email header is missing") @@ -36,16 +37,20 @@ func TestCSVDriverParsesRequiredAndOptionalColumns(t *testing.T) { driver := NewCSVDriver(strings.NewReader( "email,full_name,role,external_id\njane@example.com,Jane Doe,Admin,42\n", )) + records, err := driver.ListAccounts(context.Background()) if err != nil { t.Fatalf("unexpected error: %v", err) } + if len(records) != 1 { t.Fatalf("expected 1 record, got %d", len(records)) } + if records[0].Email != "jane@example.com" { t.Fatalf("unexpected email: %s", records[0].Email) } + if records[0].ExternalID != "42" { t.Fatalf("unexpected external id: %s", records[0].ExternalID) } diff --git a/pkg/accessreview/drivers/docusign.go b/pkg/accessreview/drivers/docusign.go index b4e2008b1..aa16b211c 100644 --- a/pkg/accessreview/drivers/docusign.go +++ b/pkg/accessreview/drivers/docusign.go @@ -79,6 +79,7 @@ func (d *DocuSignDriver) ListAccounts(ctx context.Context) ([]AccountRecord, err } var records []AccountRecord + startPosition := 0 for range maxPaginationPages { @@ -143,12 +144,14 @@ func (d *DocuSignDriver) discoverAccount(ctx context.Context) (accountID string, if err != nil { return "", "", fmt.Errorf("cannot create docusign userinfo request: %w", err) } + req.Header.Set("Accept", "application/json") httpResp, err := d.httpClient.Do(req) if err != nil { return "", "", fmt.Errorf("cannot execute docusign userinfo request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() @@ -183,12 +186,14 @@ func (d *DocuSignDriver) queryUsers(ctx context.Context, baseURI string, account if err != nil { return nil, fmt.Errorf("cannot create docusign users request: %w", err) } + req.Header.Set("Accept", "application/json") httpResp, err := d.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("cannot execute docusign users request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() diff --git a/pkg/accessreview/drivers/github.go b/pkg/accessreview/drivers/github.go index 9bb5e42bd..7c5c25487 100644 --- a/pkg/accessreview/drivers/github.go +++ b/pkg/accessreview/drivers/github.go @@ -85,6 +85,7 @@ func (d *GitHubDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error d.logger.WarnCtx(ctx, "cannot fetch github membership, skipping member", log.Error(err), ) + continue } @@ -93,6 +94,7 @@ func (d *GitHubDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error d.logger.WarnCtx(ctx, "cannot fetch github user profile, skipping member", log.Error(err), ) + continue } @@ -107,6 +109,7 @@ func (d *GitHubDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error } mfaStatus := coredata.MFAStatusUnknown + if no2FASet != nil { if no2FASet[m.Login] { mfaStatus = coredata.MFAStatusDisabled @@ -158,6 +161,7 @@ func (d *GitHubDriver) fetchAllMembers(ctx context.Context) ([]githubMember, err if nextURL == "" { return members, nil } + url = nextURL } @@ -176,6 +180,7 @@ func (d *GitHubDriver) fetchMembersPage(ctx context.Context, url string) ([]gith if err != nil { return nil, "", fmt.Errorf("cannot execute github members request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() @@ -215,6 +220,7 @@ func (d *GitHubDriver) fetchAll2FADisabledLogins(ctx context.Context) (map[strin if nextURL == "" { return set, nil } + url = nextURL } @@ -239,6 +245,7 @@ func (d *GitHubDriver) fetchMembership(ctx context.Context, login string) (*gith if err != nil { return nil, fmt.Errorf("cannot execute github membership request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() @@ -269,6 +276,7 @@ func (d *GitHubDriver) fetchUserProfile(ctx context.Context, login string) (*git if err != nil { return nil, fmt.Errorf("cannot execute github user profile request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() diff --git a/pkg/accessreview/drivers/gitlab.go b/pkg/accessreview/drivers/gitlab.go index 9c170f7d5..0c24fd516 100644 --- a/pkg/accessreview/drivers/gitlab.go +++ b/pkg/accessreview/drivers/gitlab.go @@ -117,12 +117,14 @@ func (d *GitLabDriver) queryMembers(ctx context.Context, endpoint string) ([]git if err != nil { return nil, "", fmt.Errorf("cannot create gitlab members request: %w", err) } + req.Header.Set("Accept", "application/json") httpResp, err := d.httpClient.Do(req) if err != nil { return nil, "", fmt.Errorf("cannot execute gitlab members request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { diff --git a/pkg/accessreview/drivers/google_workspace.go b/pkg/accessreview/drivers/google_workspace.go index c90670957..6f681ca3f 100644 --- a/pkg/accessreview/drivers/google_workspace.go +++ b/pkg/accessreview/drivers/google_workspace.go @@ -59,6 +59,7 @@ func (rt *retryRoundTripper) RoundTrip(req *http.Request) (*http.Response, error } var lastResp *http.Response + for attempt := range rt.maxRetries { resp, err := transport.RoundTrip(req) if err != nil { @@ -94,6 +95,7 @@ func (d *GoogleWorkspaceDriver) ListAccounts(ctx context.Context) ([]AccountReco } var records []AccountRecord + pageToken := "" for range maxPaginationPages { diff --git a/pkg/accessreview/drivers/heroku.go b/pkg/accessreview/drivers/heroku.go index 0c81b25ca..fe9937774 100644 --- a/pkg/accessreview/drivers/heroku.go +++ b/pkg/accessreview/drivers/heroku.go @@ -124,6 +124,7 @@ func (d *HerokuDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error if nextRange == "" { return records, nil } + rangeHeader = nextRange } @@ -135,7 +136,9 @@ func (d *HerokuDriver) queryMembers(ctx context.Context, endpoint, rangeHeader s if err != nil { return nil, "", fmt.Errorf("cannot create heroku members request: %w", err) } + req.Header.Set("Accept", "application/vnd.heroku+json; version=3") + if rangeHeader != "" { req.Header.Set("Range", rangeHeader) } @@ -144,6 +147,7 @@ func (d *HerokuDriver) queryMembers(ctx context.Context, endpoint, rangeHeader s if err != nil { return nil, "", fmt.Errorf("cannot execute heroku members request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() // Heroku returns 206 Partial Content for ranged responses with more diff --git a/pkg/accessreview/drivers/hubspot.go b/pkg/accessreview/drivers/hubspot.go index e81c23a5f..2edd12366 100644 --- a/pkg/accessreview/drivers/hubspot.go +++ b/pkg/accessreview/drivers/hubspot.go @@ -83,6 +83,7 @@ func (d *HubSpotDriver) ListAccounts(ctx context.Context) ([]AccountRecord, erro for _, u := range resp.Results { role := "User" + if roleMap != nil && u.RoleID != "" { if name, ok := roleMap[u.RoleID]; ok { role = name @@ -114,6 +115,7 @@ func (d *HubSpotDriver) ListAccounts(ctx context.Context) ([]AccountRecord, erro if resp.Paging == nil || resp.Paging.Next == nil || resp.Paging.Next.After == "" { return records, nil } + after = resp.Paging.Next.After } @@ -128,9 +130,11 @@ func (d *HubSpotDriver) fetchUsers(ctx context.Context, after string) (*hubspotU q := req.URL.Query() q.Set("limit", "100") + if after != "" { q.Set("after", after) } + req.URL.RawQuery = q.Encode() req.Header.Set("Accept", "application/json") @@ -139,6 +143,7 @@ func (d *HubSpotDriver) fetchUsers(ctx context.Context, after string) (*hubspotU if err != nil { return nil, fmt.Errorf("cannot execute hubspot users request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() @@ -167,6 +172,7 @@ func (d *HubSpotDriver) fetchRoles(ctx context.Context) (map[string]string, erro if err != nil { return nil, fmt.Errorf("cannot execute hubspot roles request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() diff --git a/pkg/accessreview/drivers/intercom.go b/pkg/accessreview/drivers/intercom.go index 465343b3b..2e916e1c7 100644 --- a/pkg/accessreview/drivers/intercom.go +++ b/pkg/accessreview/drivers/intercom.go @@ -61,6 +61,7 @@ func (d *IntercomDriver) ListAccounts(ctx context.Context) ([]AccountRecord, err } var records []AccountRecord + for _, a := range resp.Admins { record := AccountRecord{ Email: a.Email, @@ -95,6 +96,7 @@ func (d *IntercomDriver) fetchAdmins(ctx context.Context) (*intercomAdminsRespon if err != nil { return nil, fmt.Errorf("cannot execute intercom admins request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() @@ -119,5 +121,6 @@ func intercomRole(hasInboxSeat bool) string { if hasInboxSeat { return "Agent" } + return "Viewer" } diff --git a/pkg/accessreview/drivers/linear.go b/pkg/accessreview/drivers/linear.go index ce774aa21..3fca384e6 100644 --- a/pkg/accessreview/drivers/linear.go +++ b/pkg/accessreview/drivers/linear.go @@ -125,6 +125,7 @@ func (d *LinearDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error if !resp.Data.Users.PageInfo.HasNextPage || resp.Data.Users.PageInfo.EndCursor == "" { return records, nil } + nextCursor := resp.Data.Users.PageInfo.EndCursor after = &nextCursor } @@ -170,6 +171,7 @@ query AccessReviewLinearUsers($after: String) { if err != nil { return nil, fmt.Errorf("cannot create linear users request: %w", err) } + req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") @@ -177,6 +179,7 @@ query AccessReviewLinearUsers($after: String) { if err != nil { return nil, fmt.Errorf("cannot execute linear users request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() @@ -189,6 +192,7 @@ query AccessReviewLinearUsers($after: String) { if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil { return nil, fmt.Errorf("cannot decode linear users response: %w", err) } + if len(resp.Errors) > 0 { return nil, fmt.Errorf("linear graphql error: %s", resp.Errors[0].Message) } diff --git a/pkg/accessreview/drivers/microsoft_365.go b/pkg/accessreview/drivers/microsoft_365.go index 598c71191..220b0287f 100644 --- a/pkg/accessreview/drivers/microsoft_365.go +++ b/pkg/accessreview/drivers/microsoft_365.go @@ -120,15 +120,18 @@ func (d *Microsoft365Driver) ListAccounts(ctx context.Context) ([]AccountRecord, } rolesByUser := make(map[string][]string) + for _, role := range roles { members, err := d.listRoleMembers(ctx, role.ID) if err != nil { return nil, fmt.Errorf("cannot list members of role %q: %w", role.DisplayName, err) } + for _, m := range members { if m.ODataType != "" && m.ODataType != "#microsoft.graph.user" { continue } + rolesByUser[m.ID] = append(rolesByUser[m.ID], role.DisplayName) } } @@ -147,6 +150,7 @@ func (d *Microsoft365Driver) ListAccounts(ctx context.Context) ([]AccountRecord, userRoles := rolesByUser[u.ID] isAdmin := false + for _, r := range userRoles { if adminRoleDisplayNames[r] { isAdmin = true @@ -214,6 +218,7 @@ func pickHighestRole(roles []string) string { if len(roles) > 0 { return roles[0] } + return "" } @@ -224,15 +229,18 @@ func (d *Microsoft365Driver) listUsers(ctx context.Context) ([]microsoft365User, } var all []microsoft365User + for range microsoft365MaxPaginationOK { var page microsoft365UsersPage if err := d.fetchJSON(ctx, pageURL, &page); err != nil { return nil, err } + all = append(all, page.Value...) if page.NextLink == "" { return all, nil } + pageURL = page.NextLink } @@ -258,15 +266,18 @@ func (d *Microsoft365Driver) listDirectoryRoles(ctx context.Context) ([]microsof url := fmt.Sprintf("%s/directoryRoles", microsoft365GraphBaseURL) var all []microsoft365DirectoryRole + for range microsoft365MaxPaginationOK { var page microsoft365RolesPage if err := d.fetchJSON(ctx, url, &page); err != nil { return nil, err } + all = append(all, page.Value...) if page.NextLink == "" { return all, nil } + url = page.NextLink } @@ -277,15 +288,18 @@ func (d *Microsoft365Driver) listRoleMembers(ctx context.Context, roleID string) url := fmt.Sprintf("%s/directoryRoles/%s/members", microsoft365GraphBaseURL, roleID) var all []microsoft365RoleMember + for range microsoft365MaxPaginationOK { var page microsoft365MembersPage if err := d.fetchJSON(ctx, url, &page); err != nil { return nil, err } + all = append(all, page.Value...) if page.NextLink == "" { return all, nil } + url = page.NextLink } @@ -297,12 +311,14 @@ func (d *Microsoft365Driver) fetchJSON(ctx context.Context, url string, dst any) if err != nil { return fmt.Errorf("cannot create graph request: %w", err) } + req.Header.Set("Accept", "application/json") resp, err := d.httpClient.Do(req) if err != nil { return fmt.Errorf("cannot execute graph request: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { diff --git a/pkg/accessreview/drivers/monday.go b/pkg/accessreview/drivers/monday.go index 53ace43ab..92be4c066 100644 --- a/pkg/accessreview/drivers/monday.go +++ b/pkg/accessreview/drivers/monday.go @@ -144,6 +144,7 @@ func (d *MondayDriver) queryUsers(ctx context.Context, page int) ([]mondayUser, if err != nil { return nil, fmt.Errorf("cannot create monday users request: %w", err) } + req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") @@ -151,6 +152,7 @@ func (d *MondayDriver) queryUsers(ctx context.Context, page int) ([]mondayUser, if err != nil { return nil, fmt.Errorf("cannot execute monday users request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { diff --git a/pkg/accessreview/drivers/name_resolver.go b/pkg/accessreview/drivers/name_resolver.go index 6f6ceda20..eb4310125 100644 --- a/pkg/accessreview/drivers/name_resolver.go +++ b/pkg/accessreview/drivers/name_resolver.go @@ -69,6 +69,7 @@ func ProviderDisplayName(provider coredata.ConnectorProvider) string { if name, ok := providerDisplayNames[provider]; ok { return name } + return string(provider) } @@ -91,6 +92,7 @@ func (r *slackNameResolver) ResolveInstanceName(ctx context.Context) (string, er if err != nil { return "", fmt.Errorf("cannot execute slack auth.test request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() var resp struct { @@ -156,6 +158,7 @@ func (r *linearNameResolver) ResolveInstanceName(ctx context.Context) (string, e if err != nil { return "", fmt.Errorf("cannot create linear organization request: %w", err) } + req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") @@ -163,6 +166,7 @@ func (r *linearNameResolver) ResolveInstanceName(ctx context.Context) (string, e if err != nil { return "", fmt.Errorf("cannot execute linear organization request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { @@ -182,6 +186,7 @@ func (r *linearNameResolver) ResolveInstanceName(ctx context.Context) (string, e if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil { return "", fmt.Errorf("cannot decode linear organization response: %w", err) } + if len(resp.Errors) > 0 { return "", fmt.Errorf("linear graphql error: %s", resp.Errors[0].Message) } @@ -208,12 +213,14 @@ func (r *cloudflareNameResolver) ResolveInstanceName(ctx context.Context) (strin if err != nil { return "", fmt.Errorf("cannot create cloudflare accounts request: %w", err) } + req.Header.Set("Accept", "application/json") httpResp, err := r.httpClient.Do(req) if err != nil { return "", fmt.Errorf("cannot execute cloudflare accounts request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { @@ -255,12 +262,14 @@ func (r *brexNameResolver) ResolveInstanceName(ctx context.Context) (string, err if err != nil { return "", fmt.Errorf("cannot create brex company request: %w", err) } + req.Header.Set("Accept", "application/json") httpResp, err := r.httpClient.Do(req) if err != nil { return "", fmt.Errorf("cannot execute brex company request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { @@ -297,12 +306,14 @@ func (r *tallyNameResolver) ResolveInstanceName(ctx context.Context) (string, er if err != nil { return "", fmt.Errorf("cannot create tally organization request: %w", err) } + req.Header.Set("Accept", "application/json") httpResp, err := r.httpClient.Do(req) if err != nil { return "", fmt.Errorf("cannot execute tally organization request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { @@ -338,12 +349,14 @@ func (r *hubspotNameResolver) ResolveInstanceName(ctx context.Context) (string, if err != nil { return "", fmt.Errorf("cannot create hubspot account-info request: %w", err) } + req.Header.Set("Accept", "application/json") httpResp, err := r.httpClient.Do(req) if err != nil { return "", fmt.Errorf("cannot execute hubspot account-info request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { @@ -375,12 +388,14 @@ func (r *docusignNameResolver) ResolveInstanceName(ctx context.Context) (string, if err != nil { return "", fmt.Errorf("cannot create docusign userinfo request: %w", err) } + req.Header.Set("Accept", "application/json") httpResp, err := r.httpClient.Do(req) if err != nil { return "", fmt.Errorf("cannot execute docusign userinfo request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { @@ -429,12 +444,14 @@ func (r *openaiNameResolver) ResolveInstanceName(ctx context.Context) (string, e if err != nil { return "", fmt.Errorf("cannot create openai organization request: %w", err) } + req.Header.Set("Accept", "application/json") httpResp, err := r.httpClient.Do(req) if err != nil { return "", fmt.Errorf("cannot execute openai organization request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { @@ -473,12 +490,14 @@ func (r *sentryNameResolver) ResolveInstanceName(ctx context.Context) (string, e if err != nil { return "", fmt.Errorf("cannot create sentry organization request: %w", err) } + req.Header.Set("Accept", "application/json") httpResp, err := r.httpClient.Do(req) if err != nil { return "", fmt.Errorf("cannot execute sentry organization request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { @@ -512,12 +531,14 @@ func (r *githubNameResolver) ResolveInstanceName(ctx context.Context) (string, e if err != nil { return "", fmt.Errorf("cannot create github organization request: %w", err) } + req.Header.Set("Accept", "application/vnd.github+json") httpResp, err := r.httpClient.Do(req) if err != nil { return "", fmt.Errorf("cannot execute github organization request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { @@ -565,6 +586,7 @@ func (r *intercomNameResolver) ResolveInstanceName(ctx context.Context) (string, if err != nil { return "", fmt.Errorf("cannot create intercom me request: %w", err) } + req.Header.Set("Accept", "application/json") req.Header.Set("Intercom-Version", "2.11") @@ -572,6 +594,7 @@ func (r *intercomNameResolver) ResolveInstanceName(ctx context.Context) (string, if err != nil { return "", fmt.Errorf("cannot execute intercom me request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { @@ -622,12 +645,14 @@ func (r *gitlabNameResolver) ResolveInstanceName(ctx context.Context) (string, e if err != nil { return "", fmt.Errorf("cannot create gitlab group request: %w", err) } + req.Header.Set("Accept", "application/json") httpResp, err := r.httpClient.Do(req) if err != nil { return "", fmt.Errorf("cannot execute gitlab group request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { @@ -645,6 +670,7 @@ func (r *gitlabNameResolver) ResolveInstanceName(ctx context.Context) (string, e if resp.Name != "" { return resp.Name, nil } + return resp.FullPath, nil } @@ -669,12 +695,14 @@ func (r *bitbucketNameResolver) ResolveInstanceName(ctx context.Context) (string if err != nil { return "", fmt.Errorf("cannot create bitbucket workspace request: %w", err) } + req.Header.Set("Accept", "application/json") httpResp, err := r.httpClient.Do(req) if err != nil { return "", fmt.Errorf("cannot execute bitbucket workspace request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { @@ -692,6 +720,7 @@ func (r *bitbucketNameResolver) ResolveInstanceName(ctx context.Context) (string if resp.Name != "" { return resp.Name, nil } + return resp.Slug, nil } @@ -716,12 +745,14 @@ func (r *herokuNameResolver) ResolveInstanceName(ctx context.Context) (string, e if err != nil { return "", fmt.Errorf("cannot create heroku team request: %w", err) } + req.Header.Set("Accept", "application/vnd.heroku+json; version=3") httpResp, err := r.httpClient.Do(req) if err != nil { return "", fmt.Errorf("cannot execute heroku team request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { @@ -774,12 +805,14 @@ func (r *asanaNameResolver) ResolveInstanceName(ctx context.Context) (string, er if err != nil { return "", fmt.Errorf("cannot create asana workspace request: %w", err) } + req.Header.Set("Accept", "application/json") httpResp, err := r.httpClient.Do(req) if err != nil { return "", fmt.Errorf("cannot execute asana workspace request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { @@ -819,12 +852,14 @@ func (r *netlifyNameResolver) ResolveInstanceName(ctx context.Context) (string, if err != nil { return "", fmt.Errorf("cannot create netlify account request: %w", err) } + req.Header.Set("Accept", "application/json") httpResp, err := r.httpClient.Do(req) if err != nil { return "", fmt.Errorf("cannot execute netlify account request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { @@ -862,12 +897,14 @@ func (r *clickupNameResolver) ResolveInstanceName(ctx context.Context) (string, if err != nil { return "", fmt.Errorf("cannot create clickup team request: %w", err) } + req.Header.Set("Accept", "application/json") httpResp, err := r.httpClient.Do(req) if err != nil { return "", fmt.Errorf("cannot execute clickup team request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { @@ -905,16 +942,19 @@ func (r *vercelNameResolver) ResolveInstanceName(ctx context.Context) (string, e } teamURL := fmt.Sprintf("https://api.vercel.com/v2/teams/%s", url.PathEscape(r.teamID)) + teamReq, err := http.NewRequestWithContext(ctx, http.MethodGet, teamURL, nil) if err != nil { return "", fmt.Errorf("cannot create vercel team request: %w", err) } + teamReq.Header.Set("Accept", "application/json") teamResp, err := r.httpClient.Do(teamReq) if err != nil { return "", fmt.Errorf("cannot execute vercel team request: %w", err) } + defer func() { _ = teamResp.Body.Close() }() if teamResp.StatusCode == http.StatusOK { @@ -925,9 +965,11 @@ func (r *vercelNameResolver) ResolveInstanceName(ctx context.Context) (string, e if err := json.NewDecoder(teamResp.Body).Decode(&body); err != nil { return "", fmt.Errorf("cannot decode vercel team response: %w", err) } + if body.Name != "" { return body.Name, nil } + return body.Slug, nil } @@ -941,9 +983,11 @@ func (r *vercelNameResolver) ResolveInstanceName(ctx context.Context) (string, e if err != nil { return "", err } + if user.Username != "" { return user.Username, nil } + return user.Name, nil } @@ -972,6 +1016,7 @@ func (r *mondayNameResolver) ResolveInstanceName(ctx context.Context) (string, e if err != nil { return "", fmt.Errorf("cannot create monday account request: %w", err) } + req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") @@ -979,6 +1024,7 @@ func (r *mondayNameResolver) ResolveInstanceName(ctx context.Context) (string, e if err != nil { return "", fmt.Errorf("cannot execute monday account request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { @@ -1023,6 +1069,7 @@ func (r *notionNameResolver) ResolveInstanceName(ctx context.Context) (string, e if err != nil { return "", fmt.Errorf("cannot create notion users/me request: %w", err) } + req.Header.Set("Accept", "application/json") req.Header.Set("Notion-Version", notionAPIVersion) @@ -1030,6 +1077,7 @@ func (r *notionNameResolver) ResolveInstanceName(ctx context.Context) (string, e if err != nil { return "", fmt.Errorf("cannot execute notion users/me request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { @@ -1068,12 +1116,14 @@ func (r *microsoft365NameResolver) ResolveInstanceName(ctx context.Context) (str if err != nil { return "", fmt.Errorf("cannot create microsoft 365 organization request: %w", err) } + req.Header.Set("Accept", "application/json") httpResp, err := r.httpClient.Do(req) if err != nil { return "", fmt.Errorf("cannot execute microsoft 365 organization request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { @@ -1101,13 +1151,16 @@ func (r *microsoft365NameResolver) ResolveInstanceName(ctx context.Context) (str if org.DisplayName != "" { return org.DisplayName, nil } + for _, d := range org.VerifiedDomains { if d.IsDefault { return d.Name, nil } } + if len(org.VerifiedDomains) > 0 { return org.VerifiedDomains[0].Name, nil } + return "", nil } diff --git a/pkg/accessreview/drivers/name_resolver_test.go b/pkg/accessreview/drivers/name_resolver_test.go index 1f0b0a482..e32279adf 100644 --- a/pkg/accessreview/drivers/name_resolver_test.go +++ b/pkg/accessreview/drivers/name_resolver_test.go @@ -37,9 +37,11 @@ func (h *hostRewriter) RoundTrip(r *http.Request) (*http.Response, error) { if err != nil { return nil, err } + r2 := r.Clone(r.Context()) r2.URL.Scheme = u.Scheme r2.URL.Host = u.Host + return http.DefaultTransport.RoundTrip(r2) } @@ -88,11 +90,13 @@ func TestNotionNameResolver(t *testing.T) { defer srv.Close() client := &http.Client{Transport: &hostRewriter{target: srv.URL}} + got, err := NewNotionNameResolver(client).ResolveInstanceName(context.Background()) if tc.wantErr { require.Error(t, err) return } + require.NoError(t, err) assert.Equal(t, tc.want, got) }) diff --git a/pkg/accessreview/drivers/netlify.go b/pkg/accessreview/drivers/netlify.go index 5b29f58d1..66b4f1b5b 100644 --- a/pkg/accessreview/drivers/netlify.go +++ b/pkg/accessreview/drivers/netlify.go @@ -94,12 +94,14 @@ func (d *NetlifyDriver) queryMembers(ctx context.Context, endpoint string) ([]ne if err != nil { return nil, "", fmt.Errorf("cannot create netlify members request: %w", err) } + req.Header.Set("Accept", "application/json") httpResp, err := d.httpClient.Do(req) if err != nil { return nil, "", fmt.Errorf("cannot execute netlify members request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { diff --git a/pkg/accessreview/drivers/notion.go b/pkg/accessreview/drivers/notion.go index 4f8a96230..1f5779e21 100644 --- a/pkg/accessreview/drivers/notion.go +++ b/pkg/accessreview/drivers/notion.go @@ -96,6 +96,7 @@ func (d *NotionDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error if !resp.HasMore || resp.NextCursor == "" { return records, nil } + nextCursor := resp.NextCursor startCursor = &nextCursor } @@ -114,15 +115,18 @@ func (d *NotionDriver) queryUsers(ctx context.Context, startCursor *string) (*no q := req.URL.Query() q.Set("page_size", "100") + if startCursor != nil { q.Set("start_cursor", *startCursor) } + req.URL.RawQuery = q.Encode() httpResp, err := d.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("cannot execute notion users request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() diff --git a/pkg/accessreview/drivers/onepassword.go b/pkg/accessreview/drivers/onepassword.go index 054398672..46e7c6476 100644 --- a/pkg/accessreview/drivers/onepassword.go +++ b/pkg/accessreview/drivers/onepassword.go @@ -71,6 +71,7 @@ func NewOnePasswordDriver(httpClient *http.Client, baseURL string) *OnePasswordD func (d *OnePasswordDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error) { var records []AccountRecord + startIndex := 1 for range maxPaginationPages { @@ -103,6 +104,7 @@ func (d *OnePasswordDriver) ListAccounts(ctx context.Context) ([]AccountRecord, if record.FullName == "" && u.Name.Formatted != "" { record.FullName = u.Name.Formatted } + if record.FullName == "" && (u.Name.GivenName != "" || u.Name.FamilyName != "") { record.FullName = u.Name.GivenName + " " + u.Name.FamilyName } @@ -128,6 +130,7 @@ func (d *OnePasswordDriver) ListAccounts(ctx context.Context) ([]AccountRecord, if len(resp.Resources) == 0 || resp.ItemsPerPage <= 0 || startIndex+resp.ItemsPerPage > resp.TotalResults { return records, nil } + startIndex += resp.ItemsPerPage } @@ -139,6 +142,7 @@ func (d *OnePasswordDriver) queryUsers(ctx context.Context, startIndex int) (*on if err != nil { return nil, fmt.Errorf("cannot parse 1password base url: %w", err) } + u = u.JoinPath("scim", "v2", "Users") q := u.Query() q.Set("startIndex", strconv.Itoa(startIndex)) @@ -149,12 +153,14 @@ func (d *OnePasswordDriver) queryUsers(ctx context.Context, startIndex int) (*on if err != nil { return nil, fmt.Errorf("cannot create 1password users request: %w", err) } + req.Header.Set("Accept", "application/scim+json") httpResp, err := d.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("cannot execute 1password users request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() diff --git a/pkg/accessreview/drivers/onepassword_users_api.go b/pkg/accessreview/drivers/onepassword_users_api.go index 273f87fb1..e14c37198 100644 --- a/pkg/accessreview/drivers/onepassword_users_api.go +++ b/pkg/accessreview/drivers/onepassword_users_api.go @@ -107,6 +107,7 @@ func (d *OnePasswordUsersAPIDriver) ListAccounts(ctx context.Context) ([]Account if resp.NextPageToken == "" { return records, nil } + pageToken = resp.NextPageToken } @@ -118,6 +119,7 @@ func (d *OnePasswordUsersAPIDriver) queryUsers(ctx context.Context, pageToken st if err != nil { return nil, fmt.Errorf("cannot parse 1password users api base url: %w", err) } + u = u.JoinPath("v1beta1", "accounts", d.accountID, "users") req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) @@ -127,9 +129,11 @@ func (d *OnePasswordUsersAPIDriver) queryUsers(ctx context.Context, pageToken st q := req.URL.Query() q.Set("max_page_size", "100") + if pageToken != "" { q.Set("page_token", pageToken) } + req.URL.RawQuery = q.Encode() req.Header.Set("Accept", "application/json") @@ -138,6 +142,7 @@ func (d *OnePasswordUsersAPIDriver) queryUsers(ctx context.Context, pageToken st if err != nil { return nil, fmt.Errorf("cannot execute 1password users api request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() diff --git a/pkg/accessreview/drivers/openai.go b/pkg/accessreview/drivers/openai.go index 2dee0a4ce..a082c4e4d 100644 --- a/pkg/accessreview/drivers/openai.go +++ b/pkg/accessreview/drivers/openai.go @@ -89,6 +89,7 @@ func (d *OpenAIDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error if !resp.HasMore || resp.LastID == "" { return records, nil } + after = resp.LastID } @@ -103,9 +104,11 @@ func (d *OpenAIDriver) fetchUsers(ctx context.Context, after string) (*openaiUse q := req.URL.Query() q.Set("limit", "100") + if after != "" { q.Set("after", after) } + req.URL.RawQuery = q.Encode() req.Header.Set("Accept", "application/json") @@ -114,6 +117,7 @@ func (d *OpenAIDriver) fetchUsers(ctx context.Context, after string) (*openaiUse if err != nil { return nil, fmt.Errorf("cannot execute openai users request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() diff --git a/pkg/accessreview/drivers/organizations.go b/pkg/accessreview/drivers/organizations.go index 46d0ff0a3..583ceddb9 100644 --- a/pkg/accessreview/drivers/organizations.go +++ b/pkg/accessreview/drivers/organizations.go @@ -38,12 +38,14 @@ func ListGitHubOrganizations(ctx context.Context, httpClient *http.Client) ([]Or if err != nil { return nil, fmt.Errorf("cannot create github organizations request: %w", err) } + req.Header.Set("Accept", "application/json") resp, err := httpClient.Do(req) if err != nil { return nil, fmt.Errorf("cannot fetch github organizations: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { @@ -64,8 +66,10 @@ func ListGitHubOrganizations(ctx context.Context, httpClient *http.Client) ([]Or if displayName == "" { displayName = org.Login } + result[i] = Organization{Slug: org.Login, DisplayName: displayName} } + return result, nil } @@ -81,12 +85,14 @@ func ListSentryOrganizations(ctx context.Context, httpClient *http.Client) ([]Or if err != nil { return nil, fmt.Errorf("cannot create sentry organizations request: %w", err) } + req.Header.Set("Accept", "application/json") resp, err := httpClient.Do(req) if err != nil { return nil, fmt.Errorf("cannot fetch sentry organizations: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { @@ -107,8 +113,10 @@ func ListSentryOrganizations(ctx context.Context, httpClient *http.Client) ([]Or if displayName == "" { displayName = org.Slug } + result[i] = Organization{Slug: org.Slug, DisplayName: displayName} } + return result, nil } @@ -125,12 +133,14 @@ func ListGitLabOrganizations(ctx context.Context, httpClient *http.Client) ([]Or if err != nil { return nil, fmt.Errorf("cannot create gitlab organizations request: %w", err) } + req.Header.Set("Accept", "application/json") resp, err := httpClient.Do(req) if err != nil { return nil, fmt.Errorf("cannot fetch gitlab organizations: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { @@ -152,11 +162,13 @@ func ListGitLabOrganizations(ctx context.Context, httpClient *http.Client) ([]Or if displayName == "" { displayName = g.FullPath } + result[i] = Organization{ Slug: strconv.FormatInt(g.ID, 10), DisplayName: displayName, } } + return result, nil } @@ -174,6 +186,7 @@ func ListBitbucketOrganizations(ctx context.Context, httpClient *http.Client) ([ if err != nil { return nil, fmt.Errorf("cannot create bitbucket organizations request: %w", err) } + req.Header.Set("Accept", "application/json") resp, err := httpClient.Do(req) @@ -203,6 +216,7 @@ func ListBitbucketOrganizations(ctx context.Context, httpClient *http.Client) ([ _ = resp.Body.Close() return nil, fmt.Errorf("cannot decode bitbucket organizations response: %w", err) } + _ = resp.Body.Close() for _, v := range body.Values { @@ -211,18 +225,22 @@ func ListBitbucketOrganizations(ctx context.Context, httpClient *http.Client) ([ slug = v.Workspace.Slug name = v.Workspace.Name } + displayName := name if displayName == "" { displayName = slug } + result = append(result, Organization{Slug: slug, DisplayName: displayName}) } if body.Next == "" { return result, nil } + pageURL = body.Next } + return nil, fmt.Errorf("cannot list all bitbucket organizations: %w", ErrPaginationLimitReached) } @@ -233,12 +251,14 @@ func ListHerokuOrganizations(ctx context.Context, httpClient *http.Client) ([]Or if err != nil { return nil, fmt.Errorf("cannot create heroku organizations request: %w", err) } + req.Header.Set("Accept", "application/vnd.heroku+json; version=3") resp, err := httpClient.Do(req) if err != nil { return nil, fmt.Errorf("cannot fetch heroku organizations: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { @@ -259,8 +279,10 @@ func ListHerokuOrganizations(ctx context.Context, httpClient *http.Client) ([]Or if displayName == "" { displayName = t.ID } + result[i] = Organization{Slug: t.ID, DisplayName: displayName} } + return result, nil } @@ -276,12 +298,14 @@ func ListAsanaOrganizations(ctx context.Context, httpClient *http.Client) ([]Org if err != nil { return nil, fmt.Errorf("cannot create asana organizations request: %w", err) } + req.Header.Set("Accept", "application/json") resp, err := httpClient.Do(req) if err != nil { return nil, fmt.Errorf("cannot fetch asana organizations: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { @@ -304,8 +328,10 @@ func ListAsanaOrganizations(ctx context.Context, httpClient *http.Client) ([]Org if displayName == "" { displayName = w.GID } + result[i] = Organization{Slug: w.GID, DisplayName: displayName} } + return result, nil } @@ -321,12 +347,14 @@ func ListNetlifyOrganizations(ctx context.Context, httpClient *http.Client) ([]O if err != nil { return nil, fmt.Errorf("cannot create netlify organizations request: %w", err) } + req.Header.Set("Accept", "application/json") resp, err := httpClient.Do(req) if err != nil { return nil, fmt.Errorf("cannot fetch netlify organizations: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { @@ -348,8 +376,10 @@ func ListNetlifyOrganizations(ctx context.Context, httpClient *http.Client) ([]O if displayName == "" { displayName = a.Slug } + result[i] = Organization{Slug: a.Slug, DisplayName: displayName} } + return result, nil } @@ -365,12 +395,14 @@ func ListClickUpOrganizations(ctx context.Context, httpClient *http.Client) ([]O if err != nil { return nil, fmt.Errorf("cannot create clickup organizations request: %w", err) } + req.Header.Set("Accept", "application/json") resp, err := httpClient.Do(req) if err != nil { return nil, fmt.Errorf("cannot fetch clickup organizations: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { @@ -393,7 +425,9 @@ func ListClickUpOrganizations(ctx context.Context, httpClient *http.Client) ([]O if displayName == "" { displayName = t.ID } + result[i] = Organization{Slug: t.ID, DisplayName: displayName} } + return result, nil } diff --git a/pkg/accessreview/drivers/pagerduty.go b/pkg/accessreview/drivers/pagerduty.go index 4a7aad54d..021131b4f 100644 --- a/pkg/accessreview/drivers/pagerduty.go +++ b/pkg/accessreview/drivers/pagerduty.go @@ -65,6 +65,7 @@ func (d *PagerDutyDriver) ListAccounts(ctx context.Context) ([]AccountRecord, er var records []AccountRecord const limit = 100 + offset := 0 for range maxPaginationPages { @@ -113,6 +114,7 @@ func (d *PagerDutyDriver) ListAccounts(ctx context.Context) ([]AccountRecord, er if pageSize <= 0 { pageSize = limit } + offset += pageSize } @@ -130,12 +132,14 @@ func (d *PagerDutyDriver) queryUsers(ctx context.Context, offset, limit int) (*p if err != nil { return nil, fmt.Errorf("cannot create pagerduty users request: %w", err) } + req.Header.Set("Accept", "application/vnd.pagerduty+json;version=2") httpResp, err := d.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("cannot execute pagerduty users request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { diff --git a/pkg/accessreview/drivers/resend.go b/pkg/accessreview/drivers/resend.go index d25482e4b..d5374353f 100644 --- a/pkg/accessreview/drivers/resend.go +++ b/pkg/accessreview/drivers/resend.go @@ -54,6 +54,7 @@ func (d *ResendDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error } var records []AccountRecord + for _, k := range resp.Data { record := AccountRecord{ FullName: k.Name, @@ -96,6 +97,7 @@ func (d *ResendDriver) fetchAPIKeys(ctx context.Context) (*resendAPIKeysResponse if err != nil { return nil, fmt.Errorf("cannot execute resend api-keys request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() diff --git a/pkg/accessreview/drivers/sentry.go b/pkg/accessreview/drivers/sentry.go index 443151f21..eb9e0b4b0 100644 --- a/pkg/accessreview/drivers/sentry.go +++ b/pkg/accessreview/drivers/sentry.go @@ -67,12 +67,14 @@ func (d *SentryDriver) resolveOrgSlug(ctx context.Context) (string, error) { if err != nil { return "", fmt.Errorf("cannot create sentry organizations request: %w", err) } + req.Header.Set("Accept", "application/json") resp, err := d.httpClient.Do(req) if err != nil { return "", fmt.Errorf("cannot fetch sentry organizations: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { @@ -100,6 +102,7 @@ func (d *SentryDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error if err != nil { return nil, fmt.Errorf("cannot resolve sentry organization slug: %w", err) } + orgSlug = slug } @@ -130,6 +133,7 @@ func (d *SentryDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error isAdmin := m.OrgRole == "admin" || m.OrgRole == "owner" mfaStatus := coredata.MFAStatusUnknown + if m.User != nil { if m.User.Has2FA { mfaStatus = coredata.MFAStatusEnabled @@ -188,6 +192,7 @@ func (d *SentryDriver) queryMembers(ctx context.Context, url string) ([]sentryMe if err != nil { return nil, "", fmt.Errorf("cannot execute sentry members request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() @@ -221,8 +226,10 @@ func sentryAuthMethod(flags map[string]bool, user *sentryUser) coredata.AccessEn if flags["sso:linked"] { return coredata.AccessEntryAuthMethodSSO } + if user != nil && user.HasPasswordAuth { return coredata.AccessEntryAuthMethodPassword } + return coredata.AccessEntryAuthMethodUnknown } diff --git a/pkg/accessreview/drivers/slack.go b/pkg/accessreview/drivers/slack.go index 7fdfe18ef..7c825bbf3 100644 --- a/pkg/accessreview/drivers/slack.go +++ b/pkg/accessreview/drivers/slack.go @@ -120,6 +120,7 @@ func (d *SlackDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error) if resp.ResponseMetadata.NextCursor == "" { return records, nil } + cursor = resp.ResponseMetadata.NextCursor } @@ -134,15 +135,18 @@ func (d *SlackDriver) queryUsers(ctx context.Context, cursor string) (*slackUser q := req.URL.Query() q.Set("limit", "200") + if cursor != "" { q.Set("cursor", cursor) } + req.URL.RawQuery = q.Encode() httpResp, err := d.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("cannot execute slack users.list request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() @@ -180,5 +184,6 @@ func slackMFAStatus(has2FA bool) coredata.MFAStatus { if has2FA { return coredata.MFAStatusEnabled } + return coredata.MFAStatusDisabled } diff --git a/pkg/accessreview/drivers/slack_test.go b/pkg/accessreview/drivers/slack_test.go index 07fedf425..fd2b4ea46 100644 --- a/pkg/accessreview/drivers/slack_test.go +++ b/pkg/accessreview/drivers/slack_test.go @@ -36,12 +36,14 @@ func TestSlackDriver(t *testing.T) { // Find the first human user (bots may not have email). var r AccountRecord + for _, rec := range records { if rec.Email != "" { r = rec break } } + require.NotEmpty(t, r.Email, "expected at least one record with an email") assert.NotEmpty(t, r.ExternalID) assert.NotEmpty(t, r.Role) diff --git a/pkg/accessreview/drivers/supabase.go b/pkg/accessreview/drivers/supabase.go index 2ead919e3..c13b3febb 100644 --- a/pkg/accessreview/drivers/supabase.go +++ b/pkg/accessreview/drivers/supabase.go @@ -53,6 +53,7 @@ func (d *SupabaseDriver) ListAccounts(ctx context.Context) ([]AccountRecord, err } var records []AccountRecord + for _, m := range members { mfaStatus := coredata.MFAStatusDisabled if m.MFAEnabled { @@ -96,6 +97,7 @@ func (d *SupabaseDriver) queryMembers(ctx context.Context) ([]supabaseMember, er if err != nil { return nil, fmt.Errorf("cannot execute supabase members request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() diff --git a/pkg/accessreview/drivers/tally.go b/pkg/accessreview/drivers/tally.go index a4d5d8d1c..1c63f5b2e 100644 --- a/pkg/accessreview/drivers/tally.go +++ b/pkg/accessreview/drivers/tally.go @@ -89,6 +89,7 @@ func (d *TallyDriver) listUsers(ctx context.Context) ([]AccountRecord, error) { if err != nil { return nil, fmt.Errorf("cannot execute tally users request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() @@ -106,6 +107,7 @@ func (d *TallyDriver) listUsers(ctx context.Context) ([]AccountRecord, error) { } var records []AccountRecord + for _, u := range users { mfaStatus := coredata.MFAStatusDisabled if u.HasTwoFactorEnabled { @@ -149,6 +151,7 @@ func (d *TallyDriver) listInvites(ctx context.Context) ([]AccountRecord, error) if err != nil { return nil, fmt.Errorf("cannot execute tally invites request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() @@ -166,6 +169,7 @@ func (d *TallyDriver) listInvites(ctx context.Context) ([]AccountRecord, error) } var records []AccountRecord + for _, inv := range invites { record := AccountRecord{ Email: inv.Email, diff --git a/pkg/accessreview/drivers/vcr_test.go b/pkg/accessreview/drivers/vcr_test.go index 2fd0872cf..465eecb8c 100644 --- a/pkg/accessreview/drivers/vcr_test.go +++ b/pkg/accessreview/drivers/vcr_test.go @@ -57,6 +57,7 @@ func newRecorder(t *testing.T, cassettePath string, envVar string) *recorder.Rec if mode == recorder.ModeReplayOnly { t.Skipf("cassette not found (record with %s env var): %v", envVar, err) } + t.Fatalf("cannot create vcr recorder: %v", err) } @@ -81,6 +82,7 @@ func (rt *authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) if rt.authValue != "" { req.Header.Set("Authorization", rt.authValue) } + return rt.transport.RoundTrip(req) } @@ -89,6 +91,7 @@ func bearerAuth(token string) string { if token == "" { return "" } + return "Bearer " + token } @@ -104,5 +107,6 @@ func newVCRClient(rec *recorder.Recorder, authValue string) *http.Client { transport: transport, } } + return &http.Client{Transport: transport} } diff --git a/pkg/accessreview/drivers/vercel.go b/pkg/accessreview/drivers/vercel.go index 69463c67c..0f3b0ca0b 100644 --- a/pkg/accessreview/drivers/vercel.go +++ b/pkg/accessreview/drivers/vercel.go @@ -111,6 +111,7 @@ func (d *VercelDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error if page.Pagination.Next == nil { return records, nil } + cursor = strconv.FormatInt(*page.Pagination.Next, 10) } @@ -120,9 +121,11 @@ func (d *VercelDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error func (d *VercelDriver) queryMembers(ctx context.Context, cursor string) (*vercelMembersPage, error) { q := url.Values{} q.Set("limit", "100") + if cursor != "" { q.Set("until", cursor) } + u := url.URL{ Scheme: "https", Host: "api.vercel.com", @@ -134,12 +137,14 @@ func (d *VercelDriver) queryMembers(ctx context.Context, cursor string) (*vercel if err != nil { return nil, fmt.Errorf("cannot create vercel members request: %w", err) } + req.Header.Set("Accept", "application/json") httpResp, err := d.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("cannot execute vercel members request: %w", err) } + defer func() { _ = httpResp.Body.Close() }() if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { diff --git a/pkg/accessreview/review_engine.go b/pkg/accessreview/review_engine.go index 91f342214..85ec10e47 100644 --- a/pkg/accessreview/review_engine.go +++ b/pkg/accessreview/review_engine.go @@ -80,11 +80,13 @@ func (e *ReviewEngine) FetchSource( if err := source.LoadByID(ctx, tx, e.scope, sourceID); err != nil { return fmt.Errorf("cannot load access source %s: %w", sourceID, err) } + if source.OrganizationID != campaign.OrganizationID { return fmt.Errorf("cannot process access source: %s does not belong to campaign organization", sourceID) } var err error + driver, err = e.resolveDriver(ctx, tx, source) if err != nil { return fmt.Errorf("cannot resolve driver for source %s: %w", source.Name, err) @@ -97,6 +99,7 @@ func (e *ReviewEngine) FetchSource( } } else { entries := &coredata.AccessEntries{} + baseline, err = entries.LoadBaselineBySourceID(ctx, tx, e.scope, lastCompletedCampaign.ID, sourceID) if err != nil { return fmt.Errorf("cannot load baseline entries by source: %w", err) @@ -117,10 +120,13 @@ func (e *ReviewEngine) FetchSource( sourceCtx, cancel := context.WithTimeout(ctx, 30*time.Second) accounts, err := driver.ListAccounts(sourceCtx) + cancel() + if err != nil { return 0, fmt.Errorf("cannot list accounts from source %s: %w", source.Name, err) } + fetchedCount = len(accounts) err = e.pg.WithTx( @@ -132,6 +138,7 @@ func (e *ReviewEngine) FetchSource( for _, account := range accounts { accountKey := normalizeAccountKey(account.Email, account.ExternalID) seenAccountKeys[accountKey] = struct{}{} + incrementalTag := coredata.AccessEntryIncrementalTagNew if _, ok := previousByAccountKey[accountKey]; ok { incrementalTag = coredata.AccessEntryIncrementalTagUnchanged @@ -210,6 +217,7 @@ func (e *ReviewEngine) FetchSource( func normalizeAccountKey(email, externalID string) string { emailKey := strings.ToLower(strings.TrimSpace(email)) + externalID = strings.TrimSpace(externalID) if externalID != "" { return emailKey + "|" + externalID @@ -231,6 +239,7 @@ func (e *ReviewEngine) oauthClient( return conn.RefreshableClient(ctx, *refreshCfg) } } + return conn.Client(ctx) } @@ -245,6 +254,7 @@ func (e *ReviewEngine) connectorHTTPClient( if oauth2Conn, ok := dbConnector.Connection.(*connector.OAuth2Connection); ok { return e.oauthClient(ctx, oauth2Conn, dbConnector.Provider) } + return dbConnector.Connection.Client(ctx) } @@ -312,15 +322,19 @@ func (e *ReviewEngine) resolveDriver( if err != nil { return nil, fmt.Errorf("cannot read 1password users api settings: %w", err) } + return drivers.NewOnePasswordUsersAPIDriver(httpClient, settings.AccountID, settings.Region), nil } + onePasswordSettings, err := coredata.ConnectorSettings[coredata.OnePasswordConnectorSettings](dbConnector) if err != nil { return nil, fmt.Errorf("cannot read 1password connector settings: %w", err) } + if onePasswordSettings.SCIMBridgeURL == "" { return nil, fmt.Errorf("1password connector requires scim_bridge_url in settings") } + return drivers.NewOnePasswordDriver(httpClient, onePasswordSettings.SCIMBridgeURL), nil case coredata.ConnectorProviderHubSpot: return drivers.NewHubSpotDriver(httpClient), nil @@ -335,9 +349,11 @@ func (e *ReviewEngine) resolveDriver( if err != nil { return nil, fmt.Errorf("cannot read tally connector settings: %w", err) } + if tallySettings.OrganizationID == "" { return nil, fmt.Errorf("tally connector requires organization_id in settings") } + return drivers.NewTallyDriver(httpClient, tallySettings.OrganizationID), nil case coredata.ConnectorProviderCloudflare: return drivers.NewCloudflareDriver(httpClient), nil @@ -348,6 +364,7 @@ func (e *ReviewEngine) resolveDriver( if err != nil { return nil, fmt.Errorf("cannot read sentry connector settings: %w", err) } + // OrganizationSlug may be empty for OAuth connections; the driver auto-discovers it. return drivers.NewSentryDriver(httpClient, sentrySettings.OrganizationSlug), nil case coredata.ConnectorProviderSupabase: @@ -355,18 +372,22 @@ func (e *ReviewEngine) resolveDriver( if err != nil { return nil, fmt.Errorf("cannot read supabase connector settings: %w", err) } + if supabaseSettings.OrganizationSlug == "" { return nil, fmt.Errorf("supabase connector requires organization_slug in settings") } + return drivers.NewSupabaseDriver(httpClient, supabaseSettings.OrganizationSlug), nil case coredata.ConnectorProviderGitHub: githubSettings, err := coredata.ConnectorSettings[coredata.GitHubConnectorSettings](dbConnector) if err != nil { return nil, fmt.Errorf("cannot read github connector settings: %w", err) } + if githubSettings.Organization == "" { return nil, fmt.Errorf("github connector requires organization in settings") } + return drivers.NewGitHubDriver(httpClient, githubSettings.Organization, e.logger.Named("github")), nil case coredata.ConnectorProviderIntercom: return drivers.NewIntercomDriver(httpClient), nil @@ -379,27 +400,33 @@ func (e *ReviewEngine) resolveDriver( if err != nil { return nil, fmt.Errorf("cannot read gitlab connector settings: %w", err) } + if gitlabSettings.GroupID == "" { return nil, fmt.Errorf("gitlab connector requires group_id in settings") } + return drivers.NewGitLabDriver(httpClient, gitlabSettings.GroupID), nil case coredata.ConnectorProviderBitbucket: bitbucketSettings, err := coredata.ConnectorSettings[coredata.BitbucketConnectorSettings](dbConnector) if err != nil { return nil, fmt.Errorf("cannot read bitbucket connector settings: %w", err) } + if bitbucketSettings.Workspace == "" { return nil, fmt.Errorf("bitbucket connector requires workspace in settings") } + return drivers.NewBitbucketDriver(httpClient, bitbucketSettings.Workspace), nil case coredata.ConnectorProviderHeroku: herokuSettings, err := coredata.ConnectorSettings[coredata.HerokuConnectorSettings](dbConnector) if err != nil { return nil, fmt.Errorf("cannot read heroku connector settings: %w", err) } + if herokuSettings.TeamID == "" { return nil, fmt.Errorf("heroku connector requires team_id in settings") } + return drivers.NewHerokuDriver(httpClient, herokuSettings.TeamID), nil case coredata.ConnectorProviderPagerDuty: // PagerDuty's REST API uses the regional api.pagerduty.com host; @@ -413,36 +440,44 @@ func (e *ReviewEngine) resolveDriver( if err != nil { return nil, fmt.Errorf("cannot read asana connector settings: %w", err) } + if asanaSettings.WorkspaceGID == "" { return nil, fmt.Errorf("asana connector requires workspace_gid in settings") } + return drivers.NewAsanaDriver(httpClient, asanaSettings.WorkspaceGID), nil case coredata.ConnectorProviderNetlify: netlifySettings, err := coredata.ConnectorSettings[coredata.NetlifyConnectorSettings](dbConnector) if err != nil { return nil, fmt.Errorf("cannot read netlify connector settings: %w", err) } + if netlifySettings.AccountSlug == "" { return nil, fmt.Errorf("netlify connector requires account_slug in settings") } + return drivers.NewNetlifyDriver(httpClient, netlifySettings.AccountSlug), nil case coredata.ConnectorProviderClickUp: clickupSettings, err := coredata.ConnectorSettings[coredata.ClickUpConnectorSettings](dbConnector) if err != nil { return nil, fmt.Errorf("cannot read clickup connector settings: %w", err) } + if clickupSettings.TeamID == "" { return nil, fmt.Errorf("clickup connector requires team_id in settings") } + return drivers.NewClickUpDriver(httpClient, clickupSettings.TeamID), nil case coredata.ConnectorProviderVercel: vercelSettings, err := coredata.ConnectorSettings[coredata.VercelConnectorSettings](dbConnector) if err != nil { return nil, fmt.Errorf("cannot read vercel connector settings: %w", err) } + if vercelSettings.TeamID == "" { return nil, fmt.Errorf("vercel connector requires team_id in settings") } + return drivers.NewVercelDriver(httpClient, vercelSettings.TeamID), nil case coredata.ConnectorProviderMonday: return drivers.NewMondayDriver(httpClient), nil diff --git a/pkg/accessreview/service.go b/pkg/accessreview/service.go index ab6f8a226..13812bda7 100644 --- a/pkg/accessreview/service.go +++ b/pkg/accessreview/service.go @@ -78,6 +78,7 @@ func NewService( } else { fetchWorkerOpts = append(fetchWorkerOpts, worker.WithInterval(30*time.Second)) } + fetchWorkerOpts = append(fetchWorkerOpts, worker.WithMaxConcurrency(20)) s.fetchWorker = NewSourceFetchWorker( @@ -137,11 +138,14 @@ func (s *Service) ResolveEntryOrganizationID(ctx context.Context, entryID gid.GI ctx, func(ctx context.Context, conn pg.Querier) error { var err error + entry := &coredata.AccessEntry{} + organizationID, err = entry.LoadOrganizationID(ctx, conn, entryID) if err != nil { return fmt.Errorf("cannot load organization id: %w", err) } + return nil }, ) diff --git a/pkg/accessreview/source_name_worker.go b/pkg/accessreview/source_name_worker.go index e41f555bd..ed7b33d0a 100644 --- a/pkg/accessreview/source_name_worker.go +++ b/pkg/accessreview/source_name_worker.go @@ -79,6 +79,7 @@ func (h *sourceNameHandler) Claim(ctx context.Context) (coredata.AccessSource, e if errors.Is(err, coredata.ErrNoAccessSourceNameSyncAvailable) { return coredata.AccessSource{}, worker.ErrNoTask } + return coredata.AccessSource{}, err } @@ -128,6 +129,7 @@ func (h *sourceNameHandler) Process(ctx context.Context, source coredata.AccessS } resolver = h.buildResolver(&dbConnector, httpClient) + return nil }, ) @@ -136,6 +138,7 @@ func (h *sourceNameHandler) Process(ctx context.Context, source coredata.AccessS log.String("source_id", source.ID.String()), log.Error(err), ) + return nil } @@ -144,6 +147,7 @@ func (h *sourceNameHandler) Process(ctx context.Context, source coredata.AccessS log.String("source_id", source.ID.String()), log.String("provider", dbConnector.Provider.String()), ) + return h.markNameSynced(ctx, &source) } @@ -157,6 +161,7 @@ func (h *sourceNameHandler) Process(ctx context.Context, source coredata.AccessS log.String("provider", dbConnector.Provider.String()), log.Error(err), ) + return fmt.Errorf("cannot resolve instance name for source %s: %w", source.ID, err) } @@ -165,6 +170,7 @@ func (h *sourceNameHandler) Process(ctx context.Context, source coredata.AccessS log.String("source_id", source.ID.String()), log.String("provider", dbConnector.Provider.String()), ) + return h.markNameSynced(ctx, &source) } @@ -178,6 +184,7 @@ func (h *sourceNameHandler) Process(ctx context.Context, source coredata.AccessS ) source.Name = newName + return h.markNameSynced(ctx, &source) } @@ -247,6 +254,7 @@ func (h *sourceNameHandler) buildResolver( h.logger.Error("cannot read tally connector settings", log.Error(err)) return nil } + return drivers.NewTallyNameResolver(httpClient, tallySettings.OrganizationID) case coredata.ConnectorProviderHubSpot: return drivers.NewHubSpotNameResolver(httpClient) @@ -260,6 +268,7 @@ func (h *sourceNameHandler) buildResolver( h.logger.Error("cannot read sentry connector settings", log.Error(err)) return nil } + return drivers.NewSentryNameResolver(httpClient, sentrySettings.OrganizationSlug) case coredata.ConnectorProviderGitHub: githubSettings, err := coredata.ConnectorSettings[coredata.GitHubConnectorSettings](dbConnector) @@ -267,6 +276,7 @@ func (h *sourceNameHandler) buildResolver( h.logger.Error("cannot read github connector settings", log.Error(err)) return nil } + return drivers.NewGitHubNameResolver(httpClient, githubSettings.Organization) case coredata.ConnectorProviderSupabase: supabaseSettings, err := coredata.ConnectorSettings[coredata.SupabaseConnectorSettings](dbConnector) @@ -274,6 +284,7 @@ func (h *sourceNameHandler) buildResolver( h.logger.Error("cannot read supabase connector settings", log.Error(err)) return nil } + return drivers.NewSupabaseNameResolver(supabaseSettings.OrganizationSlug) case coredata.ConnectorProviderIntercom: return drivers.NewIntercomNameResolver(httpClient) @@ -289,6 +300,7 @@ func (h *sourceNameHandler) buildResolver( h.logger.Error("cannot read gitlab connector settings", log.Error(err)) return nil } + return drivers.NewGitLabNameResolver(httpClient, gitlabSettings.GroupID) case coredata.ConnectorProviderBitbucket: bitbucketSettings, err := coredata.ConnectorSettings[coredata.BitbucketConnectorSettings](dbConnector) @@ -296,6 +308,7 @@ func (h *sourceNameHandler) buildResolver( h.logger.Error("cannot read bitbucket connector settings", log.Error(err)) return nil } + return drivers.NewBitbucketNameResolver(httpClient, bitbucketSettings.Workspace) case coredata.ConnectorProviderHeroku: herokuSettings, err := coredata.ConnectorSettings[coredata.HerokuConnectorSettings](dbConnector) @@ -303,6 +316,7 @@ func (h *sourceNameHandler) buildResolver( h.logger.Error("cannot read heroku connector settings", log.Error(err)) return nil } + return drivers.NewHerokuNameResolver(httpClient, herokuSettings.TeamID) case coredata.ConnectorProviderPagerDuty: pdSettings, err := coredata.ConnectorSettings[coredata.PagerDutyConnectorSettings](dbConnector) @@ -310,6 +324,7 @@ func (h *sourceNameHandler) buildResolver( h.logger.Error("cannot read pagerduty connector settings", log.Error(err)) return nil } + return drivers.NewPagerDutyNameResolver(pdSettings.Subdomain) case coredata.ConnectorProviderAsana: asanaSettings, err := coredata.ConnectorSettings[coredata.AsanaConnectorSettings](dbConnector) @@ -317,6 +332,7 @@ func (h *sourceNameHandler) buildResolver( h.logger.Error("cannot read asana connector settings", log.Error(err)) return nil } + return drivers.NewAsanaNameResolver(httpClient, asanaSettings.WorkspaceGID) case coredata.ConnectorProviderNetlify: netlifySettings, err := coredata.ConnectorSettings[coredata.NetlifyConnectorSettings](dbConnector) @@ -324,6 +340,7 @@ func (h *sourceNameHandler) buildResolver( h.logger.Error("cannot read netlify connector settings", log.Error(err)) return nil } + return drivers.NewNetlifyNameResolver(httpClient, netlifySettings.AccountSlug) case coredata.ConnectorProviderClickUp: clickupSettings, err := coredata.ConnectorSettings[coredata.ClickUpConnectorSettings](dbConnector) @@ -331,6 +348,7 @@ func (h *sourceNameHandler) buildResolver( h.logger.Error("cannot read clickup connector settings", log.Error(err)) return nil } + return drivers.NewClickUpNameResolver(httpClient, clickupSettings.TeamID) case coredata.ConnectorProviderVercel: vercelSettings, err := coredata.ConnectorSettings[coredata.VercelConnectorSettings](dbConnector) @@ -338,6 +356,7 @@ func (h *sourceNameHandler) buildResolver( h.logger.Error("cannot read vercel connector settings", log.Error(err)) return nil } + return drivers.NewVercelNameResolver(httpClient, vercelSettings.TeamID) case coredata.ConnectorProviderMonday: return drivers.NewMondayNameResolver(httpClient) diff --git a/pkg/accessreview/worker.go b/pkg/accessreview/worker.go index 3a08fd112..f524fe487 100644 --- a/pkg/accessreview/worker.go +++ b/pkg/accessreview/worker.go @@ -77,12 +77,14 @@ func (h *sourceFetchHandler) Claim(ctx context.Context) (coredata.AccessReviewCa if err := sourceFetch.Update(ctx, tx, scope); err != nil { return fmt.Errorf("cannot update source fetch status: %w", err) } + return nil }, ); err != nil { if errors.Is(err, coredata.ErrNoAccessReviewCampaignSourceFetchAvailable) { return coredata.AccessReviewCampaignSourceFetch{}, worker.ErrNoTask } + return coredata.AccessReviewCampaignSourceFetch{}, fmt.Errorf("cannot claim source fetch: %w", err) } @@ -101,6 +103,7 @@ func (h *sourceFetchHandler) RecoverStale(ctx context.Context) error { ctx, func(ctx context.Context, tx pg.Tx) error { var fetches coredata.AccessReviewCampaignSourceFetches + count, err := fetches.RecoverStale(ctx, tx, staleThreshold, now) if err != nil { return fmt.Errorf("cannot recover stale source fetches: %w", err) @@ -135,6 +138,7 @@ func (h *sourceFetchHandler) handle( if commitErr != nil { return fmt.Errorf("cannot load campaign: %w, and cannot commit failed source fetch: %w", err, commitErr) } + return fmt.Errorf("cannot load campaign: %w", err) } @@ -148,6 +152,7 @@ func (h *sourceFetchHandler) handle( if finalizeErr := h.finalizeCampaignFetchLifecycle(ctx, sourceFetch.TenantID, sourceFetch.AccessReviewCampaignID); finalizeErr != nil { return fmt.Errorf("cannot finalize campaign after failed source fetch: %w", finalizeErr) } + return fmt.Errorf("cannot fetch source: %w", err) } @@ -250,6 +255,7 @@ func (h *sourceFetchHandler) finalizeCampaignFetchLifecycle( campaign.Status = coredata.AccessReviewCampaignStatusPendingActions campaign.UpdatedAt = time.Now() + return campaign.Update(ctx, tx, scope) }, ) diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go index c5d1fc036..c49fd7ab4 100644 --- a/pkg/agent/agent.go +++ b/pkg/agent/agent.go @@ -124,6 +124,7 @@ func (a *Agent) Clone(opts ...Option) *Agent { copy(newApproval.ToolNames, a.approval.ToolNames) newApproval.toolNameSet = buildToolNameSet(newApproval.ToolNames) } + cp.approval = &newApproval } @@ -205,6 +206,7 @@ func WithMaxTurns(n int) Option { if n < 1 { n = 1 } + a.maxTurns = n } } @@ -217,6 +219,7 @@ func WithMaxEmptyOutputRetries(n int) Option { if n < 0 { n = 0 } + a.maxEmptyOutputRetries = n } } @@ -226,6 +229,7 @@ func WithMaxToolDepth(n int) Option { if n < 1 { n = 1 } + a.maxToolDepth = n } } @@ -348,6 +352,7 @@ func WithMCPServers(servers ...*MCPServer) Option { func WithApproval(config ApprovalConfig) Option { config.toolNameSet = buildToolNameSet(config.ToolNames) + return func(a *Agent) { a.approval = &config } @@ -369,6 +374,7 @@ func (a *Agent) resolveTools(ctx context.Context) ([]ToolDescriptor, map[string] if err != nil { return nil, nil, fmt.Errorf("cannot resolve MCP tools from %q: %w", s.name, err) } + for _, t := range mcpTools { all = append(all, t) } @@ -380,6 +386,7 @@ func (a *Agent) resolveTools(ctx context.Context) ([]ToolDescriptor, map[string] if _, exists := toolMap[name]; exists { return nil, nil, fmt.Errorf("cannot resolve tools: duplicate tool name %q", name) } + toolMap[name] = t } diff --git a/pkg/agent/agent_test.go b/pkg/agent/agent_test.go index a71270c49..931668429 100644 --- a/pkg/agent/agent_test.go +++ b/pkg/agent/agent_test.go @@ -35,8 +35,10 @@ func (m *mockProvider) ChatCompletion(_ context.Context, _ *llm.ChatCompletionRe if m.calls >= len(m.responses) { return nil, errors.New("no more mock responses") } + resp := m.responses[m.calls] m.calls++ + return resp, nil } @@ -56,6 +58,7 @@ func (s *mockChatStream) Next() bool { func (s *mockChatStream) Event() llm.ChatCompletionStreamEvent { ev := s.events[s.pos] s.pos++ + return ev } @@ -89,8 +92,10 @@ func (p *mockMultiStreamProvider) ChatCompletionStream(_ context.Context, _ *llm if p.calls >= len(p.streams) { return nil, errors.New("no more mock streams") } + s := p.streams[p.calls] p.calls++ + return s, nil } @@ -115,6 +120,7 @@ func (g *blockingGuardrail) Check(_ context.Context, messages []llm.Message) (*a } } } + return nil, nil } @@ -129,6 +135,7 @@ func (g *outputBlocker) Check(_ context.Context, message llm.Message) (*agent.Gu Message: "output blocked", }, nil } + return nil, nil } @@ -192,6 +199,7 @@ func (s *testSession) Load(_ context.Context, sessionID string) ([]llm.Message, msgs := s.messages[sessionID] cp := make([]llm.Message, len(msgs)) copy(cp, msgs) + return cp, nil } @@ -199,6 +207,7 @@ func (s *testSession) Save(_ context.Context, sessionID string, messages []llm.M cp := make([]llm.Message, len(messages)) copy(cp, messages) s.messages[sessionID] = cp + return nil } @@ -410,6 +419,7 @@ func TestRun(t *testing.T) { } type Params struct{} + noopTool := agent.FunctionTool[Params]( "noop", "No-op", @@ -432,6 +442,7 @@ func TestRun(t *testing.T) { ) require.Error(t, err) + var maxTurnsErr *agent.MaxTurnsExceededError require.ErrorAs(t, err, &maxTurnsErr) assert.Equal(t, 2, maxTurnsErr.MaxTurns) @@ -444,6 +455,7 @@ func TestRun(t *testing.T) { t.Parallel() type Params struct{} + makeTool := func(name string) agent.Tool { tool := agent.FunctionTool[Params]( name, @@ -452,6 +464,7 @@ func TestRun(t *testing.T) { return agent.ToolResult{Content: "ok"}, nil }, ) + return tool } @@ -600,11 +613,13 @@ func TestRun(t *testing.T) { assert.Equal(t, "Both done.", result.FinalMessage().Text()) var toolMsgs []llm.Message + for _, m := range result.Messages { if m.Role == llm.RoleTool { toolMsgs = append(toolMsgs, m) } } + require.Len(t, toolMsgs, 2) assert.Equal(t, "tc_1", toolMsgs[0].ToolCallID) assert.Equal(t, "result_1", toolMsgs[0].Text()) @@ -667,11 +682,13 @@ func TestRun(t *testing.T) { assert.Equal(t, "Handled both.", result.FinalMessage().Text()) var toolMsgs []llm.Message + for _, m := range result.Messages { if m.Role == llm.RoleTool { toolMsgs = append(toolMsgs, m) } } + require.Len(t, toolMsgs, 2) assert.Equal(t, "tc_ok", toolMsgs[0].ToolCallID) assert.Equal(t, "success_result", toolMsgs[0].Text()) @@ -692,12 +709,14 @@ func TestRun(t *testing.T) { var capturedTenantID string type Params struct{} + tool := agent.FunctionTool[Params]( "check_tenant", "Check current tenant", func(ctx context.Context, _ Params) (agent.ToolResult, error) { rc := agent.RunContextFrom[*RequestContext](ctx) capturedTenantID = rc.TenantID + return agent.ToolResult{Content: "tenant: " + rc.TenantID}, nil }, ) @@ -912,11 +931,13 @@ func TestRun_Handoff(t *testing.T) { specialist, agent.WithHandoffInputFilter(func(data agent.HandoffInputData) []llm.Message { var filtered []llm.Message + for _, m := range data.NewItems { if m.Role == llm.RoleUser { filtered = append(filtered, m) } } + return filtered }), ), @@ -1017,6 +1038,7 @@ func TestRun_Guardrails(t *testing.T) { ) require.Error(t, err) + var tripErr *agent.InputGuardrailTrippedError require.ErrorAs(t, err, &tripErr) assert.Equal(t, "blocker", tripErr.Guardrail) @@ -1048,6 +1070,7 @@ func TestRun_Guardrails(t *testing.T) { ) require.Error(t, err) + var tripErr *agent.OutputGuardrailTrippedError require.ErrorAs(t, err, &tripErr) assert.Equal(t, "output_blocker", tripErr.Guardrail) @@ -1064,6 +1087,7 @@ func TestRun_Hooks(t *testing.T) { t.Parallel() type Params struct{} + noopTool := agent.FunctionTool[Params]( "noop", "No-op", @@ -1348,6 +1372,7 @@ func TestRun_ToolUseBehavior(t *testing.T) { t.Parallel() type Params struct{} + tool := agent.FunctionTool[Params]( "compute", "Compute something", @@ -1440,6 +1465,7 @@ func TestRun_ToolUseBehavior(t *testing.T) { t.Parallel() type Params struct{} + tool := agent.FunctionTool[Params]( "noop", "No-op", @@ -1482,6 +1508,7 @@ func TestRun_ToolUseBehavior(t *testing.T) { t.Parallel() type Params struct{} + tool := agent.FunctionTool[Params]( "compute", "Compute something", @@ -1594,6 +1621,7 @@ func TestRun_Approval(t *testing.T) { ) require.Error(t, err) + var interrupted *agent.InterruptedError require.ErrorAs(t, err, &interrupted) assert.Len(t, interrupted.ToolCalls, 1) @@ -2107,8 +2135,10 @@ func TestRunStreamed(t *testing.T) { []llm.Message{userMessage("Hi")}, ) - var deltas []string - var gotComplete bool + var ( + deltas []string + gotComplete bool + ) for ev := range sr.Events { switch ev.Type { @@ -2134,6 +2164,7 @@ func TestRunStreamed(t *testing.T) { t.Parallel() type Params struct{} + tool := agent.FunctionTool[Params]( "noop", "No-op", @@ -2186,6 +2217,7 @@ func TestRunStreamed(t *testing.T) { ) var gotToolStart, gotToolEnd, gotComplete bool + for ev := range sr.Events { switch ev.Type { case agent.StreamEventToolStart: @@ -2238,6 +2270,7 @@ func TestRunStreamed(t *testing.T) { ) var gotComplete, gotError bool + for ev := range sr.Events { switch ev.Type { case agent.StreamEventComplete: @@ -2287,22 +2320,29 @@ func TestRunStreamed(t *testing.T) { ) var collected []agent.StreamEvent + done := make(chan struct{}) + go func() { defer close(done) + for ev := range sr.Events { collected = append(collected, ev) } }() result, err := sr.Wait() + <-done require.NoError(t, err) assert.Equal(t, "Hello world!", result.FinalMessage().Text()) - var deltaCount int - var gotAgentStart, gotAgentEnd, gotComplete bool + var ( + deltaCount int + gotAgentStart, gotAgentEnd, gotComplete bool + ) + for _, ev := range collected { switch ev.Type { case agent.StreamEventLLMDelta: @@ -2359,6 +2399,7 @@ func TestClone(t *testing.T) { t.Parallel() type Params struct{} + tool1 := agent.FunctionTool[Params]( "t1", "desc", @@ -2474,6 +2515,7 @@ func TestGenerateSchema_EmbeddedStruct(t *testing.T) { ID string `json:"id" jsonschema:"unique identifier"` Kind string `json:"kind"` } + type Params struct { Base Name string `json:"name"` @@ -2653,6 +2695,7 @@ func TestRun_UnknownToolCall(t *testing.T) { } type Params struct{} + tool := agent.FunctionTool[Params]( "real_tool", "A real tool", @@ -2746,6 +2789,7 @@ func TestClone_WithApprovalConfig(t *testing.T) { ) require.Error(t, err) + var interrupted *agent.InterruptedError require.ErrorAs(t, err, &interrupted) assert.Len(t, interrupted.PendingApprovals, 1) @@ -2771,6 +2815,7 @@ func TestRun_HandoffWithPreHandoffTools(t *testing.T) { var executionOrder []string type Params struct{} + tool1 := agent.FunctionTool[Params]( "prepare", "Prepare data", @@ -2830,6 +2875,7 @@ func TestRun_HandoffWithPreHandoffTools(t *testing.T) { t.Parallel() type Params struct{} + tool1 := agent.FunctionTool[Params]( "prepare", "Prepare data", @@ -2892,6 +2938,7 @@ func TestRun_HandoffWithPreHandoffTools(t *testing.T) { assert.Equal(t, "specialist", result.LastAgent.Name()) var toolMsgs []llm.Message + for _, m := range result.Messages { if m.Role == llm.RoleTool { toolMsgs = append(toolMsgs, m) @@ -2914,6 +2961,7 @@ func TestRun_HandoffWithPreHandoffTools(t *testing.T) { t.Parallel() type Params struct{} + failingTool := agent.FunctionTool[Params]( "prepare", "Prepare data", diff --git a/pkg/agent/agent_tool.go b/pkg/agent/agent_tool.go index 040dbcce8..440228284 100644 --- a/pkg/agent/agent_tool.go +++ b/pkg/agent/agent_tool.go @@ -47,6 +47,7 @@ func agentToolDepth(ctx context.Context) int { if v, ok := ctx.Value(agentToolDepthKey{}).(int); ok { return v } + return 0 } @@ -124,6 +125,7 @@ func (t *agentTool) Execute(ctx context.Context, arguments string) (ToolResult, if len(preview) > 500 { preview = preview[:500] + "... (truncated)" } + return ToolResult{ Content: fmt.Sprintf("Sub-agent %q returned invalid JSON. Raw output:\n%s", t.agent.name, preview), IsError: true, diff --git a/pkg/agent/agent_tool_test.go b/pkg/agent/agent_tool_test.go index f93bec5d1..dd4b4e57f 100644 --- a/pkg/agent/agent_tool_test.go +++ b/pkg/agent/agent_tool_test.go @@ -273,12 +273,14 @@ func TestAgentTool_Execute(t *testing.T) { var captured string type Params struct{} + tenantTool := agent.FunctionTool[Params]( "get_tenant", "Get tenant", func(ctx context.Context, _ Params) (agent.ToolResult, error) { rc := agent.RunContextFrom[*AppCtx](ctx) captured = rc.TenantID + return agent.ToolResult{Content: rc.TenantID}, nil }, ) @@ -483,6 +485,7 @@ func TestAgentTool_Execute_NestedApproval(t *testing.T) { ) require.Error(t, err) + var interrupted *agent.InterruptedError require.ErrorAs(t, err, &interrupted) assert.Len(t, interrupted.PendingApprovals, 1) @@ -605,6 +608,7 @@ func TestAgentTool_Execute_NestedApproval(t *testing.T) { var siblingCalled bool type Params struct{} + siblingTool := agent.FunctionTool[Params]( "list_files", "List files", @@ -740,6 +744,7 @@ func TestAgentTool_Execute_NestedApproval(t *testing.T) { ) require.Error(t, err) + var interrupted *agent.InterruptedError require.ErrorAs(t, err, &interrupted) assert.Equal(t, "agent_c", interrupted.Agent.Name()) diff --git a/pkg/agent/approval.go b/pkg/agent/approval.go index b3d7b32ef..0271da553 100644 --- a/pkg/agent/approval.go +++ b/pkg/agent/approval.go @@ -47,6 +47,7 @@ func buildToolNameSet(names []string) map[string]struct{} { for _, name := range names { set[name] = struct{}{} } + return set } @@ -60,5 +61,6 @@ func (c *ApprovalConfig) requiresApproval(ctx context.Context, tc llm.ToolCall) } _, ok := c.toolNameSet[tc.Function.Name] + return ok } diff --git a/pkg/agent/approval_test.go b/pkg/agent/approval_test.go index 7c3817190..a07aaf967 100644 --- a/pkg/agent/approval_test.go +++ b/pkg/agent/approval_test.go @@ -64,6 +64,7 @@ func TestBuildToolNameSet(t *testing.T) { set := buildToolNameSet([]string{"delete", "update", "create"}) assert.Len(t, set, 3) + for _, name := range []string{"delete", "update", "create"} { _, ok := set[name] assert.True(t, ok, "expected set to contain %q", name) @@ -165,15 +166,19 @@ func TestApprovalConfig_RequiresApproval(t *testing.T) { t.Parallel() type ctxKey struct{} + ctx := context.WithValue(context.Background(), ctxKey{}, "marker") - var capturedCtx context.Context - var capturedTC llm.ToolCall + var ( + capturedCtx context.Context + capturedTC llm.ToolCall + ) c := &ApprovalConfig{ ShouldApprove: func(ctx context.Context, tc llm.ToolCall) bool { capturedCtx = ctx capturedTC = tc + return true }, } diff --git a/pkg/agent/cancel_test.go b/pkg/agent/cancel_test.go index 0c25409a9..263068055 100644 --- a/pkg/agent/cancel_test.go +++ b/pkg/agent/cancel_test.go @@ -52,6 +52,7 @@ func (p *blockingProvider) ChatCompletion(ctx context.Context, _ *llm.ChatComple p.ctxAtEnd = ctx.Err() p.mu.Unlock() } + return p.response, nil } @@ -214,6 +215,7 @@ func TestRun_CtxCancelGracefulSuspend(t *testing.T) { defer cancel() done := make(chan error, 1) + go func() { _, err := ag.Run( ctx, @@ -230,6 +232,7 @@ func TestRun_CtxCancelGracefulSuspend(t *testing.T) { case <-time.After(2 * time.Second): t.Fatal("LLM call never started") } + cancel() close(provider.release) diff --git a/pkg/agent/context.go b/pkg/agent/context.go index 140d60b2a..43f6d77a5 100644 --- a/pkg/agent/context.go +++ b/pkg/agent/context.go @@ -49,5 +49,6 @@ func TryRunContextFrom[C any](ctx context.Context) (C, bool) { } typed, ok := val.(C) + return typed, ok } diff --git a/pkg/agent/guardrail/prompt_injection.go b/pkg/agent/guardrail/prompt_injection.go index 787f1c6d4..530ac675a 100644 --- a/pkg/agent/guardrail/prompt_injection.go +++ b/pkg/agent/guardrail/prompt_injection.go @@ -83,6 +83,7 @@ func (g *PromptInjectionGuardrail) Check(ctx context.Context, messages []llm.Mes "prompt injection classifier failed, allowing message through", log.Error(err), ) + return &agent.GuardrailResult{Tripwire: false}, nil } diff --git a/pkg/agent/guardrail/system_prompt_leak.go b/pkg/agent/guardrail/system_prompt_leak.go index 271234895..703d77f88 100644 --- a/pkg/agent/guardrail/system_prompt_leak.go +++ b/pkg/agent/guardrail/system_prompt_leak.go @@ -32,6 +32,7 @@ func NewSystemPromptLeakGuardrail(fingerprints []string) *SystemPromptLeakGuardr if f == "" { continue } + lowered = append(lowered, strings.ToLower(f)) } diff --git a/pkg/agent/handoff.go b/pkg/agent/handoff.go index a5d348d9d..edc6e21bd 100644 --- a/pkg/agent/handoff.go +++ b/pkg/agent/handoff.go @@ -53,6 +53,7 @@ func HandoffTo(agent *Agent, opts ...HandoffOption) *Handoff { for _, opt := range opts { opt(h) } + return h } @@ -84,6 +85,7 @@ func (h *Handoff) toolName() string { if h.ToolName != "" { return h.ToolName } + return "transfer_to_" + sanitizeToolName(h.Agent.name) } diff --git a/pkg/agent/handoff_test.go b/pkg/agent/handoff_test.go index 9fee403cd..054144097 100644 --- a/pkg/agent/handoff_test.go +++ b/pkg/agent/handoff_test.go @@ -156,11 +156,13 @@ func TestWithHandoffInputFilter(t *testing.T) { target, agent.WithHandoffInputFilter(func(data agent.HandoffInputData) []llm.Message { var filtered []llm.Message + for _, m := range data.NewItems { if m.Role == llm.RoleUser { filtered = append(filtered, m) } } + return filtered }), ) @@ -194,6 +196,7 @@ func TestWithHandoffInputFilter(t *testing.T) { all := make([]llm.Message, 0, len(data.InputHistory)+len(data.NewItems)) all = append(all, data.InputHistory...) all = append(all, data.NewItems...) + return all }), ) diff --git a/pkg/agent/mcp.go b/pkg/agent/mcp.go index 520532150..4f5fc2a4b 100644 --- a/pkg/agent/mcp.go +++ b/pkg/agent/mcp.go @@ -56,12 +56,15 @@ func (s *MCPServer) Name() string { func (s *MCPServer) Tools(ctx context.Context) ([]Tool, error) { s.mu.RLock() + if s.toolsCached { cp := make([]Tool, len(s.cachedTools)) copy(cp, s.cachedTools) s.mu.RUnlock() + return cp, nil } + s.mu.RUnlock() s.mu.Lock() @@ -70,11 +73,14 @@ func (s *MCPServer) Tools(ctx context.Context) ([]Tool, error) { if s.toolsCached { cp := make([]Tool, len(s.cachedTools)) copy(cp, s.cachedTools) + return cp, nil } - var allTools []*mcp.Tool - var cursor string + var ( + allTools []*mcp.Tool + cursor string + ) for { params := &mcp.ListToolsParams{} @@ -92,6 +98,7 @@ func (s *MCPServer) Tools(ctx context.Context) ([]Tool, error) { if result.NextCursor == "" { break } + cursor = result.NextCursor } @@ -172,6 +179,7 @@ func extractMCPContent(result *mcp.CallToolResult) string { } var parts []string + for _, c := range result.Content { if tc, ok := c.(*mcp.TextContent); ok { parts = append(parts, tc.Text) diff --git a/pkg/agent/mcp_test.go b/pkg/agent/mcp_test.go index dd231976e..f7565cec9 100644 --- a/pkg/agent/mcp_test.go +++ b/pkg/agent/mcp_test.go @@ -106,6 +106,7 @@ func TestMCPServer_Tools(t *testing.T) { // Mutating one slice must not affect the other. tools1[0] = nil + assert.NotNil(t, tools2[0]) // Underlying cache must be untouched. @@ -131,6 +132,7 @@ func TestMCPServer_Tools(t *testing.T) { s.toolsCached = true const goroutines = 50 + var wg sync.WaitGroup wg.Add(goroutines) @@ -212,12 +214,14 @@ func TestMCPServer_ResetCache(t *testing.T) { s.toolsCached = true const goroutines = 50 + var wg sync.WaitGroup wg.Add(goroutines) for range goroutines { go func() { defer wg.Done() + s.ResetCache() }() } @@ -321,6 +325,7 @@ func TestExtractMCPContent(t *testing.T) { "empty content returns empty", func(t *testing.T) { t.Parallel() + result := &mcp.CallToolResult{Content: []mcp.Content{}} assert.Equal(t, "", extractMCPContent(result)) }, @@ -330,6 +335,7 @@ func TestExtractMCPContent(t *testing.T) { "single text content", func(t *testing.T) { t.Parallel() + result := &mcp.CallToolResult{ Content: []mcp.Content{ &mcp.TextContent{Text: "hello world"}, @@ -343,6 +349,7 @@ func TestExtractMCPContent(t *testing.T) { "multiple text contents joined by newline", func(t *testing.T) { t.Parallel() + result := &mcp.CallToolResult{ Content: []mcp.Content{ &mcp.TextContent{Text: "line one"}, @@ -357,6 +364,7 @@ func TestExtractMCPContent(t *testing.T) { "non-text content is skipped", func(t *testing.T) { t.Parallel() + result := &mcp.CallToolResult{ Content: []mcp.Content{ &mcp.TextContent{Text: "text part"}, @@ -371,6 +379,7 @@ func TestExtractMCPContent(t *testing.T) { "only non-text content returns empty", func(t *testing.T) { t.Parallel() + result := &mcp.CallToolResult{ Content: []mcp.Content{ &mcp.ImageContent{Data: []byte("base64data"), MIMEType: "image/png"}, @@ -384,6 +393,7 @@ func TestExtractMCPContent(t *testing.T) { "falls back to structured content when no text content", func(t *testing.T) { t.Parallel() + result := &mcp.CallToolResult{ StructuredContent: map[string]any{ "status": "ok", @@ -400,6 +410,7 @@ func TestExtractMCPContent(t *testing.T) { "text content takes precedence over structured content", func(t *testing.T) { t.Parallel() + result := &mcp.CallToolResult{ Content: []mcp.Content{ &mcp.TextContent{Text: "text wins"}, @@ -414,6 +425,7 @@ func TestExtractMCPContent(t *testing.T) { "structured content used when content has only non-text", func(t *testing.T) { t.Parallel() + result := &mcp.CallToolResult{ Content: []mcp.Content{ &mcp.ImageContent{Data: []byte("img"), MIMEType: "image/png"}, diff --git a/pkg/agent/output_type_test.go b/pkg/agent/output_type_test.go index d2846b9ca..8c3d01eef 100644 --- a/pkg/agent/output_type_test.go +++ b/pkg/agent/output_type_test.go @@ -75,6 +75,7 @@ func TestOutputType_responseFormat(t *testing.T) { ot, err := NewOutputType[Verdict]("verdict") require.NoError(t, err) + rf := ot.responseFormat() require.NotNil(t, rf) @@ -96,6 +97,7 @@ func TestOutputType_responseFormat_SchemaMatchesOutputType(t *testing.T) { ot, err := NewOutputType[Analysis]("analysis") require.NoError(t, err) + rf := ot.responseFormat() var schema map[string]any diff --git a/pkg/agent/restore.go b/pkg/agent/restore.go index 2484890a0..e608fb22b 100644 --- a/pkg/agent/restore.go +++ b/pkg/agent/restore.go @@ -37,13 +37,16 @@ func Restore( if err != nil { return nil, fmt.Errorf("cannot load checkpoint: %w", err) } + if cp == nil { return nil, fmt.Errorf("cannot restore: no checkpoint for run %s", runID) } + agent, err := registry.Agent(cp.AgentName) if err != nil { return nil, fmt.Errorf("cannot resolve agent %q: %w", cp.AgentName, err) } + agent = applyCheckpointConfig(agent, cp.Config) return restoreCheckpoint(ctx, agent, cp, store, runID, registry) @@ -58,6 +61,7 @@ func applyCheckpointConfig(agent *Agent, cfg AgentConfig) *Agent { if cfg.MaxTurns <= 0 { return agent } + return agent.Clone(WithMaxTurns(cfg.MaxTurns)) } @@ -154,13 +158,17 @@ func restoreNestedSuspended( } entries := make([]nestedRestoreEntry, len(cp.AllToolCalls)) + var wg sync.WaitGroup + for i, tc := range cp.AllToolCalls { entries[i].toolCall = tc + result, ok := completedByID[tc.ID] if ok { entries[i].result = result entries[i].completed = true + continue } @@ -169,6 +177,7 @@ func restoreNestedSuspended( entries[i].err = fmt.Errorf("cannot restore nested tool call %q: missing inner checkpoint", tc.ID) continue } + entries[i].originalCheckpoint = innerCP innerAgent, err := registry.Agent(innerCP.AgentName) @@ -176,9 +185,11 @@ func restoreNestedSuspended( entries[i].err = fmt.Errorf("cannot resolve inner agent %q: %w", innerCP.AgentName, err) continue } + innerAgent = applyCheckpointConfig(innerAgent, innerCP.Config) wg.Add(1) + go func(i int, tc llm.ToolCall, innerAgent *Agent, innerCP *Checkpoint) { defer wg.Done() @@ -189,10 +200,14 @@ func restoreNestedSuspended( entries[i].err = fmt.Errorf("cannot restore nested tool call %q: missing suspension checkpoint", tc.ID) return } + entries[i].suspendedCheckpoint = se.Checkpoint + return } + entries[i].err = fmt.Errorf("cannot restore nested tool call %q: %w", tc.ID, err) + return } @@ -200,6 +215,7 @@ func restoreNestedSuspended( entries[i].completed = true }(i, tc, innerAgent, innerCP) } + wg.Wait() messages := make([]llm.Message, len(cp.Messages)) @@ -207,16 +223,20 @@ func restoreNestedSuspended( completedCalls := make([]CompletedCall, 0, len(cp.AllToolCalls)) remainingInner := make(map[string]*Checkpoint) + var restoreErr error + for _, entry := range entries { switch { case entry.err != nil: if entry.originalCheckpoint != nil { remainingInner[entry.toolCall.ID] = entry.originalCheckpoint } + if restoreErr == nil { restoreErr = entry.err } + continue case entry.suspendedCheckpoint != nil: @@ -227,6 +247,7 @@ func restoreNestedSuspended( if restoreErr == nil { restoreErr = fmt.Errorf("cannot restore nested tool call %q: no result", entry.toolCall.ID) } + continue } @@ -250,13 +271,16 @@ func restoreNestedSuspended( saveProgress := func() (*Checkpoint, error) { next := *cp next.InnerCheckpoints = remainingInner + next.CompletedCalls = completedCalls if store != nil && runID != "" { if err := store.Save(saveCtx, runID, &next); err != nil { return nil, fmt.Errorf("cannot save nested restore progress: %w", err) } + emitHook(agent, func(h RunHooks) { h.OnRunSnapshot(saveCtx, agent, &next) }) } + return &next, nil } @@ -264,6 +288,7 @@ func restoreNestedSuspended( if _, err := saveProgress(); err != nil { return nil, errors.Join(restoreErr, err) } + return nil, restoreErr } @@ -272,6 +297,7 @@ func restoreNestedSuspended( if err != nil { return nil, err } + return nil, &SuspendedError{RunID: runID, Checkpoint: next} } @@ -301,11 +327,13 @@ func restoreAwaitingApproval( if len(cp.InnerCheckpoints) > 1 { return nil, fmt.Errorf("cannot restore approval checkpoint: expected one inner checkpoint, got %d", len(cp.InnerCheckpoints)) } + for toolCallID, innerCP := range cp.InnerCheckpoints { innerAgent, err := registry.Agent(innerCP.AgentName) if err != nil { return nil, fmt.Errorf("cannot resolve inner agent %q: %w", innerCP.AgentName, err) } + innerAgent = applyCheckpointConfig(innerAgent, innerCP.Config) innerIE := &InterruptedError{ @@ -334,6 +362,7 @@ func restoreAwaitingApproval( completedCalls: cp.CompletedCalls, innerInterrupt: innerIE, } + break } } diff --git a/pkg/agent/restore_test.go b/pkg/agent/restore_test.go index b5f52f963..60d4dc9ad 100644 --- a/pkg/agent/restore_test.go +++ b/pkg/agent/restore_test.go @@ -43,6 +43,7 @@ func (s *memoryCheckpointer) Save(_ context.Context, runID string, cp *agent.Che clone := *cp s.checkpoints[runID] = &clone + return nil } @@ -56,6 +57,7 @@ func (s *memoryCheckpointer) Load(_ context.Context, runID string) (*agent.Check } clone := *cp + return &clone, nil } @@ -68,6 +70,7 @@ func (r *simpleRegistry) Agent(name string) (*agent.Agent, error) { if !ok { return nil, fmt.Errorf("agent %q not found", name) } + return a, nil } @@ -220,6 +223,7 @@ func TestRestore(t *testing.T) { ) require.Error(t, err) + var interrupted *agent.InterruptedError require.ErrorAs(t, err, &interrupted) assert.Len(t, interrupted.PendingApprovals, 1) diff --git a/pkg/agent/result.go b/pkg/agent/result.go index 2052e2684..24dd556e4 100644 --- a/pkg/agent/result.go +++ b/pkg/agent/result.go @@ -29,5 +29,6 @@ func (r *Result) FinalMessage() llm.Message { if len(r.Messages) == 0 { return llm.Message{} } + return r.Messages[len(r.Messages)-1] } diff --git a/pkg/agent/run.go b/pkg/agent/run.go index 5206004b0..a570e23ae 100644 --- a/pkg/agent/run.go +++ b/pkg/agent/run.go @@ -104,14 +104,17 @@ func blockingCallLLM(ctx context.Context, agent *Agent, req *llm.ChatCompletionR if sErr != nil { return nil, err // return the original error } + defer func() { _ = stream.Close() }() acc := llm.NewStreamAccumulator(stream) for acc.Next() { } + if sErr := acc.Err(); sErr != nil { return nil, sErr } + return acc.Response(), nil } @@ -143,6 +146,7 @@ func (s *loopState) resolveAgentTools(ctx context.Context) error { s.toolMap = toolMap s.toolDefs = toolDefs + return nil } @@ -218,6 +222,7 @@ func (s *loopState) finishRun(ctx context.Context, result *Result, err error) (* ) s.opts.onEvent(ctx, StreamEvent{Type: StreamEventComplete, Agent: s.agent, Result: result}) + return result, err } @@ -385,6 +390,7 @@ func coreLoop(ctx context.Context, startAgent *Agent, inputMessages []llm.Messag if s.opts.checkpointer != nil { if saveErr := s.opts.checkpointer.Save(ctx, s.opts.runID, cp); saveErr != nil { s.logger.ErrorCtx(ctx, "cannot save suspension checkpoint", log.Error(saveErr)) + se.Checkpoint = cp } else { emitHook(s.agent, func(h RunHooks) { h.OnRunSnapshot(ctx, s.agent, cp) }) @@ -412,6 +418,7 @@ func coreLoop(ctx context.Context, startAgent *Agent, inputMessages []llm.Messag if s.toolUsedInRun && s.agent.resetToolChoice && toolChoice != nil { toolChoice = nil } + if !exploring && structuredFormat != nil && len(s.toolDefs) > 0 { // On the synthesis turn, forbid further tool calls so the // model is forced to convert what it has into JSON. @@ -478,9 +485,11 @@ func coreLoop(ctx context.Context, startAgent *Agent, inputMessages []llm.Messag // conclusions during synthesis. if exploring && s.turns < s.agent.maxTurns { exploring = false + if resp.Message.Text() == "" { s.messages = s.messages[:len(s.messages)-1] } + s.messages = append( s.messages, llm.Message{ @@ -494,6 +503,7 @@ func coreLoop(ctx context.Context, startAgent *Agent, inputMessages []llm.Messag log.Int("turn", s.turns), log.Int("output_tokens", resp.Usage.OutputTokens), ) + continue } @@ -514,8 +524,10 @@ func coreLoop(ctx context.Context, startAgent *Agent, inputMessages []llm.Messag log.Int("retry", emptyOutputRetries), log.Int("output_tokens", resp.Usage.OutputTokens), ) + continue } + if err := runOutputGuardrails(ctx, s.agent, resp.Message); err != nil { return s.finishRun(ctx, nil, err) } @@ -530,6 +542,7 @@ func coreLoop(ctx context.Context, startAgent *Agent, inputMessages []llm.Messag emitAgentHook(s.agent, func(h AgentHooks) { h.OnEnd(ctx, s.agent, resp.Message.Text()) }) opts.onEvent(ctx, StreamEvent{Type: StreamEventAgentEnd, Agent: s.agent}) + return s.finishRun(ctx, result, nil) case llm.FinishReasonToolCalls: @@ -561,6 +574,7 @@ func coreLoop(ctx context.Context, startAgent *Agent, inputMessages []llm.Messag outerCP.InnerCheckpoints = se.Checkpoint.InnerCheckpoints outerCP.CompletedCalls = se.Checkpoint.CompletedCalls } + if s.opts.checkpointer != nil { if saveErr := s.opts.checkpointer.Save(ctx, s.opts.runID, outerCP); saveErr != nil { s.logger.ErrorCtx(ctx, "cannot save checkpoint", log.Error(saveErr)) @@ -568,6 +582,7 @@ func coreLoop(ctx context.Context, startAgent *Agent, inputMessages []llm.Messag emitHook(s.agent, func(h RunHooks) { h.OnRunSnapshot(ctx, s.agent, outerCP) }) } } + return s.finishRun(ctx, nil, &SuspendedError{RunID: s.opts.runID, Checkpoint: outerCP}) } @@ -584,6 +599,7 @@ func coreLoop(ctx context.Context, startAgent *Agent, inputMessages []llm.Messag if s.opts.checkpointer != nil { cp := s.buildCheckpoint(AgentStatusAwaitingApproval) cp.PendingToolCalls = nae.allToolCalls + cp.PendingApprovals = nae.pendingApprovals if saveErr := s.opts.checkpointer.Save(ctx, s.opts.runID, cp); saveErr != nil { s.logger.ErrorCtx(ctx, "cannot save approval checkpoint", log.Error(saveErr)) @@ -623,6 +639,7 @@ func coreLoop(ctx context.Context, startAgent *Agent, inputMessages []llm.Messag cp.PendingApprovals = nie.inner.PendingApprovals cp.AllToolCalls = nie.allToolCalls cp.CompletedCalls = nie.completedCalls + cp.InnerCheckpoints = map[string]*Checkpoint{ nie.toolCallID: { Status: AgentStatusAwaitingApproval, @@ -692,6 +709,7 @@ func coreLoop(ctx context.Context, startAgent *Agent, inputMessages []llm.Messag emitAgentHook(s.agent, func(h AgentHooks) { h.OnEnd(ctx, s.agent, finalOutput) }) opts.onEvent(ctx, StreamEvent{Type: StreamEventAgentEnd, Agent: s.agent}) + return s.finishRun(ctx, result, nil) } @@ -748,6 +766,7 @@ func callLLMWithHooks( if err != nil { emitHook(agent, func(h RunHooks) { h.OnLLMEnd(ctx, agent, nil, err) }) emitAgentHook(agent, func(h AgentHooks) { h.OnLLMEnd(ctx, agent, nil, err) }) + return nil, err } @@ -790,6 +809,7 @@ func executeToolCalls( if !ok { return nil, nil, nil, fmt.Errorf("cannot dispatch tool call: unknown tool %q", tc.Function.Name) } + descriptors[i] = desc if _, isHandoff := desc.(*handoffToolAdapter); isHandoff && handoffIdx == -1 { handoffIdx = i @@ -806,6 +826,7 @@ func executeToolCalls( } results, msgs, err := executeParallel(ctx, tracer, agent, toolCalls, tools, onEvent, logger) + return nil, results, msgs, err } @@ -844,6 +865,7 @@ func executeWithHandoff( }, ) } + return nil, nil, msgs, &nestedInterruptionError{ inner: ie, toolCallID: toolCalls[i].ID, @@ -851,6 +873,7 @@ func executeWithHandoff( completedCalls: completed, } } + return nil, nil, msgs, err } @@ -915,9 +938,11 @@ func executeParallel( logger *log.Logger, ) ([]ToolCallResult, []llm.Message, error) { entries := make([]parallelToolEntry, len(toolCalls)) + var wg sync.WaitGroup wg.Add(len(toolCalls)) + for i := range toolCalls { go func(idx int, tc llm.ToolCall, tool Tool) { defer wg.Done() @@ -927,6 +952,7 @@ func executeParallel( entries[idx] = parallelToolEntry{err: err} return } + entries[idx] = parallelToolEntry{result: tr} }(i, toolCalls[i], tools[i]) } @@ -940,10 +966,12 @@ func executeParallel( } var completed []CompletedCall + for j, other := range entries { if j == i { continue } + if other.err != nil { completed = append( completed, @@ -955,8 +983,10 @@ func executeParallel( }, }, ) + continue } + completed = append( completed, CompletedCall{ @@ -965,6 +995,7 @@ func executeParallel( }, ) } + return nil, nil, &nestedInterruptionError{ inner: ie, toolCallID: toolCalls[i].ID, @@ -978,15 +1009,18 @@ func executeParallel( if entry.err == nil { continue } + se, ok := errors.AsType[*SuspendedError](entry.err) if ok && se.Checkpoint != nil { innerCheckpoints := make(map[string]*Checkpoint) + var completed []CompletedCall for j, other := range entries { if j == i { continue } + if other.err == nil { completed = append( completed, @@ -995,8 +1029,10 @@ func executeParallel( Result: other.result, }, ) + continue } + otherSE, ok := errors.AsType[*SuspendedError](other.err) if ok && otherSE.Checkpoint != nil { innerCheckpoints[toolCalls[j].ID] = otherSE.Checkpoint @@ -1024,6 +1060,7 @@ func executeParallel( CompletedCalls: completed, }, } + return nil, nil, outerSE } } @@ -1060,6 +1097,7 @@ func executeParallel( }, }, ) + continue } @@ -1169,6 +1207,7 @@ func executeSingleTool( if len(content) > 200 { content = content[:200] + "... (truncated)" } + logger.WarnCtx( ctx, "tool returned error", @@ -1192,6 +1231,7 @@ func checkApproval(ctx context.Context, a *Agent, toolCalls []llm.ToolCall) erro } var pending []llm.ToolCall + for _, tc := range toolCalls { if a.approval.requiresApproval(ctx, tc) { pending = append(pending, tc) @@ -1363,6 +1403,7 @@ func resumeWithOpts(ctx context.Context, interrupted *InterruptedError, input Re ) handoffTarget = ht.handoff + break } @@ -1464,6 +1505,7 @@ func resumeNested(ctx context.Context, interrupted *InterruptedError, input Resu }, } } + return nil, fmt.Errorf("cannot resume nested agent: %w", err) } @@ -1534,8 +1576,10 @@ func resolveStructuredFormat(a *Agent) *llm.ResponseFormat { if a.responseFormat != nil { return a.responseFormat } + if a.outputType != nil { return a.outputType.responseFormat() } + return nil } diff --git a/pkg/agent/schema.go b/pkg/agent/schema.go index 12ad5e930..57c043a07 100644 --- a/pkg/agent/schema.go +++ b/pkg/agent/schema.go @@ -45,6 +45,7 @@ func mustJSONSchemaFor[T any]() json.RawMessage { if err != nil { panic(err) } + return schema } @@ -63,6 +64,7 @@ func stripNullTypes(s *jsonschema.Schema) { filtered = append(filtered, t) } } + if len(filtered) == 1 { s.Type = filtered[0] s.Types = nil diff --git a/pkg/agent/schema_test.go b/pkg/agent/schema_test.go index 6796c865d..c9e7e8d23 100644 --- a/pkg/agent/schema_test.go +++ b/pkg/agent/schema_test.go @@ -129,6 +129,7 @@ func TestGenerateSchema_NestedPointerStruct(t *testing.T) { type Inner struct { Value *string `json:"value"` } + type Params struct { Inner *Inner `json:"inner"` } @@ -251,12 +252,15 @@ func TestGenerateSchema_DeeplyNestedStructure(t *testing.T) { type Level3 struct { Value *int `json:"value"` } + type Level2 struct { Items []Level3 `json:"items"` } + type Level1 struct { Child *Level2 `json:"child"` } + type Params struct { Root Level1 `json:"root"` } @@ -299,6 +303,7 @@ func TestGenerateSchema_SliceOfStructs(t *testing.T) { Name string `json:"name"` Count *int `json:"count,omitempty"` } + type Params struct { Items []Item `json:"items"` } diff --git a/pkg/agent/session_memory.go b/pkg/agent/session_memory.go index ffb08060a..33a543953 100644 --- a/pkg/agent/session_memory.go +++ b/pkg/agent/session_memory.go @@ -61,6 +61,7 @@ func (s *memorySession) Save(_ context.Context, sessionID string, messages []llm } s.sessions[sessionID] = cp + return nil } diff --git a/pkg/agent/system_prompt.go b/pkg/agent/system_prompt.go index d68f92d34..b4d28022c 100644 --- a/pkg/agent/system_prompt.go +++ b/pkg/agent/system_prompt.go @@ -45,6 +45,7 @@ You can transfer the conversation to a more specialized agent when appropriate: func buildSystemPrompt(data systemPromptData) string { var buf bytes.Buffer + _ = systemPromptTmpl.Execute(&buf, data) return buf.String() diff --git a/pkg/agent/tool.go b/pkg/agent/tool.go index c898aaf0e..890723a44 100644 --- a/pkg/agent/tool.go +++ b/pkg/agent/tool.go @@ -50,6 +50,7 @@ func ResultJSON(v any) ToolResult { IsError: true, } } + return ToolResult{Content: string(data)} } @@ -128,6 +129,7 @@ func (t *functionTool[P]) Execute(ctx context.Context, arguments string) (ToolRe } var missing []string + for _, f := range t.requiredFields { if _, ok := fields[f]; !ok { missing = append(missing, f) diff --git a/pkg/agent/tool_test.go b/pkg/agent/tool_test.go index 76cee47eb..d30e7ce64 100644 --- a/pkg/agent/tool_test.go +++ b/pkg/agent/tool_test.go @@ -193,6 +193,7 @@ func TestFunctionTool_Execute(t *testing.T) { } var received string + tool := agent.FunctionTool( "weather", "Get weather", @@ -257,6 +258,7 @@ func TestFunctionTool_Execute(t *testing.T) { t.Parallel() type ctxKey struct{} + type Params struct{} tool := agent.FunctionTool( diff --git a/pkg/agent/tool_use_behavior.go b/pkg/agent/tool_use_behavior.go index 67ee2c929..59b8c3be1 100644 --- a/pkg/agent/tool_use_behavior.go +++ b/pkg/agent/tool_use_behavior.go @@ -42,6 +42,7 @@ func StopOnFirstTool() ToolUseBehavior { if len(results) == 0 { return "", false, nil } + return results[0].Result.Content, true, nil } } @@ -53,12 +54,14 @@ func StopAtTools(names ...string) ToolUseBehavior { for _, n := range names { stopSet[n] = struct{}{} } + return func(_ context.Context, results []ToolCallResult) (string, bool, error) { for _, r := range results { if _, ok := stopSet[r.ToolName]; ok { return r.Result.Content, true, nil } } + return "", false, nil } } diff --git a/pkg/agent/tools/browser/browser.go b/pkg/agent/tools/browser/browser.go index e58ae654b..98896c238 100644 --- a/pkg/agent/tools/browser/browser.go +++ b/pkg/agent/tools/browser/browser.go @@ -123,6 +123,7 @@ func (b *Browser) checkAlive() *agent.ToolResult { IsError: true, } } + return nil } diff --git a/pkg/agent/tools/browser/download_pdf.go b/pkg/agent/tools/browser/download_pdf.go index 01d72b21a..0fb999851 100644 --- a/pkg/agent/tools/browser/download_pdf.go +++ b/pkg/agent/tools/browser/download_pdf.go @@ -72,6 +72,7 @@ func DownloadPDFTool() agent.Tool { ErrorDetail: fmt.Sprintf("cannot download PDF: %s", err), }), nil } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { @@ -95,6 +96,7 @@ func DownloadPDFTool() agent.Tool { ErrorDetail: fmt.Sprintf("cannot create temp dir: %s", err), }), nil } + defer func() { _ = os.RemoveAll(tmpDir) }() tmpFile := filepath.Join(tmpDir, "input.pdf") @@ -106,6 +108,7 @@ func DownloadPDFTool() agent.Tool { // Get page count. conf := model.NewDefaultConfiguration() + pageCount, err := api.PageCountFile(tmpFile) if err != nil { return agent.ResultJSON(downloadPDFResult{ @@ -130,15 +133,18 @@ func DownloadPDFTool() agent.Tool { // Read all extracted content files. var sb strings.Builder + entries, _ := os.ReadDir(outDir) for _, entry := range entries { if entry.IsDir() { continue } + content, err := os.ReadFile(filepath.Join(outDir, entry.Name())) if err != nil { continue } + sb.Write(content) sb.WriteString("\n") } diff --git a/pkg/agent/tools/browser/fetch_robots.go b/pkg/agent/tools/browser/fetch_robots.go index ad200c261..dd0599a0b 100644 --- a/pkg/agent/tools/browser/fetch_robots.go +++ b/pkg/agent/tools/browser/fetch_robots.go @@ -69,6 +69,7 @@ func FetchRobotsTxtTool() agent.Tool { ErrorDetail: fmt.Sprintf("cannot fetch robots.txt: %s", err), }), nil } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { @@ -79,6 +80,7 @@ func FetchRobotsTxtTool() agent.Tool { } var result robotsResult + result.Found = true scanner := bufio.NewScanner(resp.Body) diff --git a/pkg/agent/tools/browser/fetch_sitemap.go b/pkg/agent/tools/browser/fetch_sitemap.go index 8e3c49793..5745479e1 100644 --- a/pkg/agent/tools/browser/fetch_sitemap.go +++ b/pkg/agent/tools/browser/fetch_sitemap.go @@ -73,6 +73,7 @@ func FetchSitemapTool() agent.Tool { ErrorDetail: fmt.Sprintf("cannot fetch sitemap: %s", err), }), nil } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { @@ -92,7 +93,9 @@ func FetchSitemapTool() agent.Tool { ErrorDetail: fmt.Sprintf("cannot decompress gzipped sitemap: %s", err), }), nil } + defer func() { _ = gz.Close() }() + reader = gz } @@ -125,6 +128,7 @@ func FetchSitemapTool() agent.Tool { func parseSitemapXML(r io.Reader) ([]string, error) { var urls []string + decoder := xml.NewDecoder(r) for { @@ -132,6 +136,7 @@ func parseSitemapXML(r io.Reader) ([]string, error) { if err == io.EOF { break } + if err != nil { return urls, err } diff --git a/pkg/agent/tools/internal/netcheck/netcheck.go b/pkg/agent/tools/internal/netcheck/netcheck.go index cf8f59d6f..063e26744 100644 --- a/pkg/agent/tools/internal/netcheck/netcheck.go +++ b/pkg/agent/tools/internal/netcheck/netcheck.go @@ -119,7 +119,9 @@ func NewPinnedTransport() *http.Transport { // Dial the first validated IP directly to prevent DNS rebinding. pinnedAddr := net.JoinHostPort(ips[0].IP.String(), port) + var d net.Dialer + return d.DialContext(ctx, network, pinnedAddr) }, } diff --git a/pkg/agent/tools/search/diff_documents.go b/pkg/agent/tools/search/diff_documents.go index d3afa3d33..c2edb768c 100644 --- a/pkg/agent/tools/search/diff_documents.go +++ b/pkg/agent/tools/search/diff_documents.go @@ -52,6 +52,7 @@ func DiffDocumentsTool() agent.Tool { if labelA == "" { labelA = "document_a" } + labelB := p.LabelB if labelB == "" { labelB = "document_b" @@ -80,6 +81,7 @@ func DiffDocumentsTool() agent.Tool { if len(output) > maxDiffOutput { output = output[:maxDiffOutput] + "\n[... diff truncated]" } + result.UnifiedDiff = output } @@ -114,6 +116,7 @@ func computeDiff(linesA, linesB []string, labelA, labelB string) diffOutput { for i := range dp { dp[i] = make([]int, n+1) } + for i := m - 1; i >= 0; i-- { for j := n - 1; j >= 0; j-- { if linesA[i] == linesB[j] { @@ -131,6 +134,7 @@ func computeDiff(linesA, linesB []string, labelA, labelB string) diffOutput { fmt.Fprintf(&sb, "--- %s\n+++ %s\n", labelA, labelB) var added, removed int + i, j := 0, 0 for i < m || j < n { if i < m && j < n && linesA[i] == linesB[j] { @@ -139,10 +143,12 @@ func computeDiff(linesA, linesB []string, labelA, labelB string) diffOutput { j++ } else if j < n && (i >= m || dp[i][j+1] >= dp[i+1][j]) { sb.WriteString("+ " + linesB[j] + "\n") + added++ j++ } else if i < m { sb.WriteString("- " + linesA[i] + "\n") + removed++ i++ } diff --git a/pkg/agent/tools/search/firecrawl.go b/pkg/agent/tools/search/firecrawl.go index 7324a47f4..6a3c0de67 100644 --- a/pkg/agent/tools/search/firecrawl.go +++ b/pkg/agent/tools/search/firecrawl.go @@ -72,6 +72,7 @@ func FirecrawlSearchTool(apiKey string) agent.Tool { if maxResults <= 0 { maxResults = 5 } + if maxResults > 10 { maxResults = 10 } @@ -111,6 +112,7 @@ func firecrawlSearch( if err != nil { return nil, fmt.Errorf("cannot create request: %w", err) } + req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+apiKey) @@ -118,6 +120,7 @@ func firecrawlSearch( if err != nil { return nil, fmt.Errorf("cannot execute search request: %w", err) } + defer func() { _ = resp.Body.Close() }() respBody, err := io.ReadAll(resp.Body) diff --git a/pkg/agent/tools/search/government_db.go b/pkg/agent/tools/search/government_db.go index ea1123e45..b2abbb702 100644 --- a/pkg/agent/tools/search/government_db.go +++ b/pkg/agent/tools/search/government_db.go @@ -91,6 +91,7 @@ func CheckGovernmentDBTool(apiKey string) agent.Tool { if err != nil { continue } + for _, e := range entries { *s.target = append( *s.target, diff --git a/pkg/agent/tools/search/httpclient.go b/pkg/agent/tools/search/httpclient.go index bcc0a22a9..6c78b1af8 100644 --- a/pkg/agent/tools/search/httpclient.go +++ b/pkg/agent/tools/search/httpclient.go @@ -28,6 +28,7 @@ type userAgentTransport struct { func (t *userAgentTransport) RoundTrip(r *http.Request) (*http.Response, error) { r2 := r.Clone(r.Context()) r2.Header.Set("User-Agent", "Probo-Agent/1.0") + return t.next.RoundTrip(r2) } @@ -35,5 +36,6 @@ func newHTTPClient() *http.Client { client := httpclient.DefaultPooledClient() client.Timeout = 15 * time.Second client.Transport = &userAgentTransport{next: client.Transport} + return client } diff --git a/pkg/agent/tools/search/wayback.go b/pkg/agent/tools/search/wayback.go index 3b26a84b6..1f313b405 100644 --- a/pkg/agent/tools/search/wayback.go +++ b/pkg/agent/tools/search/wayback.go @@ -66,6 +66,7 @@ func CheckWaybackTool() agent.Tool { // Check availability. availURL := "https://archive.org/wayback/available?url=" + url.QueryEscape(p.URL) + body, err := httpGet(ctx, client, availURL) if err != nil { result.ErrorDetail = fmt.Sprintf("cannot check Wayback Machine availability: %s", err) @@ -118,6 +119,7 @@ func httpGet(ctx context.Context, client *http.Client, rawURL string) ([]byte, e if err != nil { return nil, err } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { diff --git a/pkg/agent/tools/security/cors.go b/pkg/agent/tools/security/cors.go index 960d9e4af..43c49d461 100644 --- a/pkg/agent/tools/security/cors.go +++ b/pkg/agent/tools/security/cors.go @@ -101,6 +101,7 @@ func CheckCORSTool() agent.Tool { ErrorDetail: fmt.Sprintf("cannot fetch %s: %s", p.URL, err), }), nil } + defer func() { _ = resp.Body.Close() }() allowOrigin := resp.Header.Get("Access-Control-Allow-Origin") diff --git a/pkg/agent/tools/security/csp.go b/pkg/agent/tools/security/csp.go index ebc823a17..bd807d0bc 100644 --- a/pkg/agent/tools/security/csp.go +++ b/pkg/agent/tools/security/csp.go @@ -92,6 +92,7 @@ func AnalyzeCSPTool() agent.Tool { ErrorDetail: fmt.Sprintf("cannot fetch %s: %s", p.URL, err), }), nil } + defer func() { _ = resp.Body.Close() }() raw := resp.Header.Get("Content-Security-Policy") @@ -111,6 +112,7 @@ func AnalyzeCSPTool() agent.Tool { directives := parseCSPDirectives(raw) var hasUnsafeEval, hasUnsafeInline, hasWildcard bool + for _, d := range directives { for _, v := range d.Values { switch v { diff --git a/pkg/agent/tools/security/dmarc.go b/pkg/agent/tools/security/dmarc.go index 399893de8..3f621ebfc 100644 --- a/pkg/agent/tools/security/dmarc.go +++ b/pkg/agent/tools/security/dmarc.go @@ -46,6 +46,7 @@ func parseDMARCTag(record, tag string) string { return after } } + return "" } @@ -60,6 +61,7 @@ func CheckDMARCTool() agent.Tool { } client := dns.NewClient() + answers, err := queryDNS( ctx, client, diff --git a/pkg/agent/tools/security/dns_records.go b/pkg/agent/tools/security/dns_records.go index 31daef5c0..051215f4f 100644 --- a/pkg/agent/tools/security/dns_records.go +++ b/pkg/agent/tools/security/dns_records.go @@ -53,10 +53,14 @@ func CheckDNSRecordsTool() agent.Tool { hdr := dns.Header{Name: fqdn, Class: dns.ClassINET} client := dns.NewClient() - var result dnsRecordsResult - var errs []string + + var ( + result dnsRecordsResult + errs []string + ) // A records. + if answers, err := queryDNS(ctx, client, &dns.A{Hdr: hdr}); err != nil { errs = append(errs, fmt.Sprintf("A query failed: %s", err)) } else { @@ -148,12 +152,14 @@ func queryDNS(ctx context.Context, client *dns.Client, question dns.RR, opts ... for _, opt := range opts { opt(&msg.MsgHeader) } + msg.Question = []dns.RR{question} resp, _, err := client.Exchange(ctx, msg, "udp", defaultResolverAddr) if err == nil && resp.Truncated { resp, _, err = client.Exchange(ctx, msg, "tcp", defaultResolverAddr) } + if err != nil { return nil, err } diff --git a/pkg/agent/tools/security/dnssec.go b/pkg/agent/tools/security/dnssec.go index 24a488b71..cb5f32b84 100644 --- a/pkg/agent/tools/security/dnssec.go +++ b/pkg/agent/tools/security/dnssec.go @@ -48,6 +48,7 @@ func CheckDNSSECTool() agent.Tool { } client := dns.NewClient() + answers, err := queryDNS( ctx, client, @@ -66,8 +67,11 @@ func CheckDNSSECTool() agent.Tool { }), nil } - var keyCount int - var keyDetails []string + var ( + keyCount int + keyDetails []string + ) + for _, answer := range answers { if key, ok := answer.(*dns.DNSKEY); ok { keyCount++ @@ -76,6 +80,7 @@ func CheckDNSSECTool() agent.Tool { if key.Flags&0x0001 != 0 { flags = "KSK" } + keyDetails = append( keyDetails, fmt.Sprintf("%s (algorithm=%d, flags=%d)", flags, key.Algorithm, key.Flags), diff --git a/pkg/agent/tools/security/headers.go b/pkg/agent/tools/security/headers.go index f56ff6f6f..cd53ede40 100644 --- a/pkg/agent/tools/security/headers.go +++ b/pkg/agent/tools/security/headers.go @@ -52,6 +52,7 @@ type ( func checkHeader(h http.Header, name string) headerCheck { v := h.Get(name) + return headerCheck{ Present: v != "", Value: v, @@ -92,6 +93,7 @@ func CheckSecurityHeadersTool() agent.Tool { // First check the HTTP version to detect HTTP→HTTPS redirect. redirectsToHTTPS := false + httpURL := p.URL if after, ok := strings.CutPrefix(httpURL, "https://"); ok { httpURL = "http://" + after @@ -118,18 +120,21 @@ func CheckSecurityHeadersTool() agent.Tool { } followClient := &http.Client{Timeout: 10 * time.Second} + httpsReq, err := http.NewRequestWithContext(ctx, http.MethodGet, httpsURL, nil) if err != nil { return agent.ResultJSON(headersResult{ ErrorDetail: fmt.Sprintf("cannot create request for %s: %s", httpsURL, err), }), nil } + resp, err := followClient.Do(httpsReq) if err != nil { return agent.ResultJSON(headersResult{ ErrorDetail: fmt.Sprintf("cannot fetch %s: %s", httpsURL, err), }), nil } + defer func() { _ = resp.Body.Close() }() result := headersFromResponse(resp) diff --git a/pkg/agent/tools/security/hibp.go b/pkg/agent/tools/security/hibp.go index cab0468ab..f2bb0c4bf 100644 --- a/pkg/agent/tools/security/hibp.go +++ b/pkg/agent/tools/security/hibp.go @@ -81,6 +81,7 @@ func CheckBreachesTool() agent.Tool { ErrorDetail: fmt.Sprintf("cannot fetch breaches: %s", err), }), nil } + defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) diff --git a/pkg/agent/tools/security/security.go b/pkg/agent/tools/security/security.go index f130b656f..c28d94db8 100644 --- a/pkg/agent/tools/security/security.go +++ b/pkg/agent/tools/security/security.go @@ -26,6 +26,7 @@ func resolverAddr() string { if addr := os.Getenv("DNS_RESOLVER_ADDR"); addr != "" { return addr } + return "8.8.8.8:53" } diff --git a/pkg/agent/tools/security/spf.go b/pkg/agent/tools/security/spf.go index e5aeca78e..f37259ad5 100644 --- a/pkg/agent/tools/security/spf.go +++ b/pkg/agent/tools/security/spf.go @@ -65,6 +65,7 @@ func CheckSPFTool() agent.Tool { } client := dns.NewClient() + answers, err := queryDNS( ctx, client, @@ -83,6 +84,7 @@ func CheckSPFTool() agent.Tool { } var spfRecords []string + for _, answer := range answers { txt, ok := answer.(*dns.TXT) if !ok { @@ -106,6 +108,7 @@ func CheckSPFTool() agent.Tool { if len(spfRecords) == 1 { record := spfRecords[0] + return agent.ResultJSON(spfResult{ Found: true, RawRecord: record, diff --git a/pkg/agent/tools/security/ssl.go b/pkg/agent/tools/security/ssl.go index a35c604bf..130a9dfe2 100644 --- a/pkg/agent/tools/security/ssl.go +++ b/pkg/agent/tools/security/ssl.go @@ -89,16 +89,19 @@ func CheckSSLCertificateTool() agent.Tool { }, } netConn, err := dialer.DialContext(ctx, "tcp", p.Domain+":443") + var conn *tls.Conn if netConn != nil { conn = netConn.(*tls.Conn) } + if err != nil { return agent.ResultJSON(sslResult{ Valid: false, ErrorDetail: err.Error(), }), nil } + defer func() { _ = conn.Close() }() state := conn.ConnectionState() @@ -124,6 +127,7 @@ func CheckSSLCertificateTool() agent.Tool { for _, ic := range state.PeerCertificates[1:] { opts.Intermediates.AddCert(ic) } + if _, err := cert.Verify(opts); err != nil { valid = false } diff --git a/pkg/agent/tools/security/whois.go b/pkg/agent/tools/security/whois.go index 4c5882f85..57b7327cd 100644 --- a/pkg/agent/tools/security/whois.go +++ b/pkg/agent/tools/security/whois.go @@ -67,6 +67,7 @@ func CheckWhoisTool() agent.Tool { if whoisServer == "" { whoisServer = parseWhoisField(referral, "whois") } + if whoisServer == "" { // Try common TLD WHOIS servers as fallback. parts := strings.Split(p.Domain, ".") @@ -84,6 +85,7 @@ func CheckWhoisTool() agent.Tool { if whoisHost == "" { whoisHost = whoisServer } + if err := netcheck.ValidatePublicDomain(whoisHost); err != nil { return agent.ResultJSON(whoisResult{ ErrorDetail: fmt.Sprintf("WHOIS referral server not allowed: %s", err), @@ -114,6 +116,7 @@ func CheckWhoisTool() agent.Tool { years := int(age.Hours() / 24 / 365) months := int(age.Hours()/24/30) % 12 result.DomainAge = fmt.Sprintf("%d years, %d months", years, months) + break } } @@ -126,10 +129,12 @@ func CheckWhoisTool() agent.Tool { func queryWhois(ctx context.Context, server, domain string) (string, error) { dialer := net.Dialer{Timeout: 10 * time.Second} + conn, err := dialer.DialContext(ctx, "tcp", server) if err != nil { return "", fmt.Errorf("cannot connect to %s: %w", server, err) } + defer func() { _ = conn.Close() }() _ = conn.SetDeadline(time.Now().Add(10 * time.Second)) @@ -140,11 +145,13 @@ func queryWhois(ctx context.Context, server, domain string) (string, error) { } var sb strings.Builder + scanner := bufio.NewScanner(conn) for scanner.Scan() { sb.WriteString(scanner.Text()) sb.WriteString("\n") } + if err := scanner.Err(); err != nil { return "", fmt.Errorf("cannot read from %s: %w", server, err) } @@ -154,19 +161,23 @@ func queryWhois(ctx context.Context, server, domain string) (string, error) { func parseWhoisField(raw, field string) string { field = strings.ToLower(field) + for line := range strings.SplitSeq(raw, "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "%") || strings.HasPrefix(line, "#") { continue } + k, v, ok := strings.Cut(line, ":") if !ok { continue } + if strings.ToLower(strings.TrimSpace(k)) == field { return strings.TrimSpace(v) } } + return "" } @@ -199,16 +210,20 @@ var ( func parseWhoisResponse(raw string) whoisResult { var result whoisResult + for line := range strings.SplitSeq(raw, "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "%") || strings.HasPrefix(line, "#") { continue } + k, v, ok := strings.Cut(line, ":") if !ok { continue } + key := strings.ToLower(strings.TrimSpace(k)) + val := strings.TrimSpace(v) if val == "" { continue diff --git a/pkg/agent/typed_test.go b/pkg/agent/typed_test.go index 7bfd425e0..528ca25d3 100644 --- a/pkg/agent/typed_test.go +++ b/pkg/agent/typed_test.go @@ -33,8 +33,10 @@ func (m *typedMockProvider) ChatCompletion(_ context.Context, _ *llm.ChatComplet if m.calls >= len(m.responses) { return nil, errors.New("no more mock responses") } + resp := m.responses[m.calls] m.calls++ + return resp, nil } diff --git a/pkg/agentruntest/agent_run_supervisor_test.go b/pkg/agentruntest/agent_run_supervisor_test.go index 4dcd109ab..edd5d0da5 100644 --- a/pkg/agentruntest/agent_run_supervisor_test.go +++ b/pkg/agentruntest/agent_run_supervisor_test.go @@ -64,8 +64,10 @@ func (m *mockProvider) ChatCompletion(_ context.Context, _ *llm.ChatCompletionRe if m.calls >= len(m.responses) { return nil, errors.New("no more mock responses") } + resp := m.responses[m.calls] m.calls++ + return resp, nil } @@ -114,6 +116,7 @@ func (r *simpleRegistry) Agent(name string) (*agent.Agent, error) { if !ok { return nil, fmt.Errorf("agent %q not found", name) } + return a, nil } @@ -206,6 +209,7 @@ func TestAgentRunSupervisor_StopAndResume(t *testing.T) { func(_ context.Context, _ struct{}) (agent.ToolResult, error) { close(toolReady) <-toolRelease + return agent.ToolResult{Content: "work done"}, nil }, ) @@ -332,6 +336,7 @@ func TestAgentRunSupervisor_StopAndResume(t *testing.T) { WHERE id = $1`, run.ID.String(), ) + return err }, ) @@ -524,11 +529,13 @@ func makeBattleTools(progressFile string) []agent.Tool { // Record completion — written AFTER the sleep so the parent's // step count reflects truly-finished work. mu.Lock() + f, err := os.OpenFile(progressFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { mu.Unlock() return agent.ToolResult{}, err } + _, _ = fmt.Fprintln(f, input.Task) _ = f.Close() mu.Unlock() @@ -584,12 +591,15 @@ func TestAgentRunSupervisor_SIGTERM(t *testing.T) { if err != nil { return 0 } + n := 0 + for _, b := range data { if b == '\n' { n++ } } + return n } @@ -599,6 +609,7 @@ func TestAgentRunSupervisor_SIGTERM(t *testing.T) { "-test.run=^TestAgentRunSupervisor_SIGTERM$", "-test.v", ) + cmd.Env = append(os.Environ(), "TEST_SIGTERM_SUBPROCESS=1", "TEST_SIGTERM_PROGRESS_FILE="+progressFile, @@ -607,27 +618,33 @@ func TestAgentRunSupervisor_SIGTERM(t *testing.T) { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr require.NoError(t, cmd.Start()) + return cmd } killAndWait := func(cmd *exec.Cmd) { require.NoError(t, cmd.Process.Signal(syscall.SIGTERM)) + err := cmd.Wait() if err == nil { return } + exitErr, ok := errors.AsType[*exec.ExitError](err) if !ok { t.Fatalf("subprocess error: %v", err) } + ws, ok := exitErr.Sys().(syscall.WaitStatus) if !ok { t.Fatalf("subprocess exited with unexpected wait status: %v", exitErr) } + if ws.Signaled() && ws.Signal() == syscall.SIGTERM { t.Logf("subprocess terminated by SIGTERM") return } + t.Fatalf("subprocess exited unexpectedly (signaled=%v signal=%v exit=%d): %v", ws.Signaled(), ws.Signal(), ws.ExitStatus(), exitErr) } @@ -646,6 +663,7 @@ func TestAgentRunSupervisor_SIGTERM(t *testing.T) { WHERE id = $1`, run.ID.String(), ) + return err }, ) @@ -674,6 +692,7 @@ func TestAgentRunSupervisor_SIGTERM(t *testing.T) { " checkpoint: %d messages, %d turns, usage=%+v", len(cp.Messages), cp.Turns, cp.Usage, ) + return cp } @@ -682,7 +701,9 @@ func TestAgentRunSupervisor_SIGTERM(t *testing.T) { // Steps so far: turn0=1(scan) + turn1=3(fetch×3) = 4 // ============================================================ t.Log("=== Phase 1: SIGTERM after scan + parallel fetch (4 steps) ===") + cmd1 := startSubprocess(0) + waitForSteps(4) killAndWait(cmd1) @@ -700,7 +721,9 @@ func TestAgentRunSupervisor_SIGTERM(t *testing.T) { // New steps: turn2=1(analyze) + turn3=3(check×3) = 4 // ============================================================ t.Log("=== Phase 2: SIGTERM after analyze + parallel checks (4 more steps) ===") + cmd2 := startSubprocess(cp1.Turns) + waitForSteps(steps1 + 4) killAndWait(cmd2) @@ -720,7 +743,9 @@ func TestAgentRunSupervisor_SIGTERM(t *testing.T) { // New steps: turn4=1(deep) + turn5=2(generate×2) = 3 // ============================================================ t.Log("=== Phase 3: SIGTERM during long-running deep analysis (3 more steps) ===") + cmd3 := startSubprocess(cp2.Turns) + waitForSteps(steps2 + 3) killAndWait(cmd3) @@ -811,14 +836,17 @@ func runSIGTERMSubprocess() { if addr == "" { addr = "localhost:5432" } + user := os.Getenv("PROBO_TEST_PG_USER") if user == "" { user = "probod" } + password := os.Getenv("PROBO_TEST_PG_PASSWORD") if password == "" { password = "probod" } + database := os.Getenv("PROBO_TEST_PG_DATABASE") if database == "" { database = "probod_test" diff --git a/pkg/agentruntest/agentruntest.go b/pkg/agentruntest/agentruntest.go index 0e0e2256a..e8bca26e3 100644 --- a/pkg/agentruntest/agentruntest.go +++ b/pkg/agentruntest/agentruntest.go @@ -115,6 +115,7 @@ func EnsureAgentRunsTable(t *testing.T, client *pg.Client) { ).Scan(&exists); err != nil { return fmt.Errorf("cannot check agent_runs existence: %w", err) } + if exists { return nil } @@ -127,6 +128,7 @@ func EnsureAgentRunsTable(t *testing.T, client *pg.Client) { if _, err := conn.Exec(ctx, string(ddl)); err != nil { return fmt.Errorf("cannot apply agent_runs migration: %w", err) } + return nil }) }) @@ -185,6 +187,7 @@ func InsertPendingRun( ); err != nil { return fmt.Errorf("cannot insert placeholder organization: %w", err) } + return run.Insert(ctx, tx, coredata.NewScope(tenantID)) }, ) @@ -214,6 +217,7 @@ func LoadAgentRun(t *testing.T, client *pg.Client, id gid.GID) coredata.AgentRun t.Helper() var run coredata.AgentRun + err := client.WithConn( context.Background(), func(ctx context.Context, conn pg.Querier) error { @@ -231,11 +235,13 @@ func LoadAgentRun(t *testing.T, client *pg.Client, id gid.GID) coredata.AgentRun // require.Eventually callbacks (which recover panics). func TryLoadAgentRun(client *pg.Client, id gid.GID) (coredata.AgentRun, error) { var run coredata.AgentRun + err := client.WithConn( context.Background(), func(ctx context.Context, conn pg.Querier) error { return run.LoadByID(ctx, conn, coredata.NewNoScope(), id) }, ) + return run, err } diff --git a/pkg/baseurl/baseurl.go b/pkg/baseurl/baseurl.go index ebe561a92..c1f1faf18 100644 --- a/pkg/baseurl/baseurl.go +++ b/pkg/baseurl/baseurl.go @@ -64,6 +64,7 @@ func MustParse(rawURL string) *BaseURL { if err != nil { panic(err) } + return b } @@ -72,6 +73,7 @@ func (b *BaseURL) String() string { if b == nil { return "" } + return b.raw } @@ -80,6 +82,7 @@ func (b *BaseURL) Scheme() string { if b == nil || b.parsed == nil { return "" } + return b.parsed.Scheme } @@ -88,6 +91,7 @@ func (b *BaseURL) Host() string { if b == nil || b.parsed == nil { return "" } + return b.parsed.Host } @@ -96,6 +100,7 @@ func (b *BaseURL) Hostname() string { if b == nil || b.parsed == nil { return "" } + return b.parsed.Hostname() } @@ -104,6 +109,7 @@ func (b *BaseURL) Port() string { if b == nil || b.parsed == nil { return "" } + return b.parsed.Port() } @@ -168,7 +174,9 @@ func (ub *URLBuilder) WithQuery(key, value string) *URLBuilder { if ub.err != nil { return ub } + ub.query.Add(key, value) + return ub } @@ -177,11 +185,13 @@ func (ub *URLBuilder) WithQueryValues(values url.Values) *URLBuilder { if ub.err != nil { return ub } + for key, vals := range values { for _, val := range vals { ub.query.Add(key, val) } } + return ub } @@ -208,6 +218,7 @@ func (ub *URLBuilder) MustString() string { if err != nil { panic(err) } + return s } @@ -224,6 +235,7 @@ func (b *BaseURL) UnmarshalJSON(data []byte) error { } *b = *parsed + return nil } @@ -232,6 +244,7 @@ func (b *BaseURL) MarshalJSON() ([]byte, error) { if b == nil { return json.Marshal("") } + return json.Marshal(b.raw) } @@ -243,6 +256,7 @@ func (b *BaseURL) UnmarshalText(text []byte) error { } *b = *parsed + return nil } @@ -251,6 +265,7 @@ func (b *BaseURL) MarshalText() ([]byte, error) { if b == nil { return []byte(""), nil } + return []byte(b.raw), nil } diff --git a/pkg/baseurl/baseurl_test.go b/pkg/baseurl/baseurl_test.go index 72b8f267f..6e3480b8d 100644 --- a/pkg/baseurl/baseurl_test.go +++ b/pkg/baseurl/baseurl_test.go @@ -75,6 +75,7 @@ func TestParse(t *testing.T) { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) return } + if !tt.wantErr && got == nil { t.Error("Parse() returned nil without error") } @@ -133,6 +134,7 @@ func TestBaseURL_WithPath(t *testing.T) { t.Errorf("WithPath().String() error = %v", err) return } + if got != tt.want { t.Errorf("WithPath().String() = %v, want %v", got, tt.want) } @@ -147,7 +149,6 @@ func TestBaseURL_WithQuery(t *testing.T) { WithQuery("q", "test"). WithQuery("limit", "10"). String() - if err != nil { t.Fatalf("WithPath().WithQuery().String() error = %v", err) } diff --git a/pkg/bootstrap/builder.go b/pkg/bootstrap/builder.go index b4624c3f5..868afa987 100644 --- a/pkg/bootstrap/builder.go +++ b/pkg/bootstrap/builder.go @@ -36,6 +36,7 @@ func NewBuilder(getEnv EnvGetter) *Builder { if getEnv == nil { getEnv = os.Getenv } + return &Builder{getEnv: getEnv} } @@ -372,6 +373,7 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) { if clientID == "" { continue } + cfg.Probod.Connectors = append(cfg.Probod.Connectors, probodconfig.ConnectorConfig{ Provider: provider, Protocol: "oauth2", @@ -480,6 +482,7 @@ func (b *Builder) getSAMLCredentials() (cert, key string, err error) { if cert == "" { cert = b.getEnv("SAML_CERTIFICATE") } + if key == "" { key = b.getEnv("SAML_PRIVATE_KEY") } @@ -498,6 +501,7 @@ func (b *Builder) getOAuth2SigningKey() string { if b.oauth2SigningKey != "" { return b.oauth2SigningKey } + return b.getEnv("OAUTH2_SERVER_SIGNING_KEY") } @@ -516,6 +520,7 @@ func (b *Builder) getEnvOrDefault(key, defaultValue string) string { if value := b.getEnv(key); value != "" { return value } + return defaultValue } @@ -525,6 +530,7 @@ func (b *Builder) getEnvIntOrDefault(key string, defaultValue int) int { return int(intValue) } } + return defaultValue } @@ -534,6 +540,7 @@ func (b *Builder) getEnvFloatOrDefault(key string, defaultValue float64) float64 return floatValue } } + return defaultValue } @@ -543,6 +550,7 @@ func (b *Builder) getEnvFloatPtr(key string) *float64 { return &floatValue } } + return nil } @@ -553,6 +561,7 @@ func (b *Builder) getEnvIntPtr(key string) *int { return &v } } + return nil } @@ -562,6 +571,7 @@ func (b *Builder) getEnvBoolOrDefault(key string, defaultValue bool) bool { return boolValue } } + return defaultValue } @@ -572,12 +582,15 @@ func (b *Builder) parseOriginsList(s string) []string { } var result []string + for part := range strings.SplitSeq(s, ",") { part = strings.TrimSpace(part) + part = strings.Trim(part, "\"") if part != "" { result = append(result, part) } } + return result } diff --git a/pkg/bootstrap/builder_test.go b/pkg/bootstrap/builder_test.go index cfb62f615..9df51f351 100644 --- a/pkg/bootstrap/builder_test.go +++ b/pkg/bootstrap/builder_test.go @@ -114,6 +114,7 @@ func TestBuilder_Build_MissingRequiredEnvVars(t *testing.T) { _, err := b.Build() require.Error(t, err) + for _, missing := range tt.wantMissing { assert.Contains(t, err.Error(), missing) } @@ -480,6 +481,7 @@ func TestBuilder_Build_AccessReviewConnectors(t *testing.T) { require.NoError(t, err) require.Len(t, cfg.Probod.Connectors, len(providers)) + byProvider := make(map[string]probodconfig.ConnectorConfig, len(cfg.Probod.Connectors)) for _, c := range cfg.Probod.Connectors { byProvider[c.Provider] = c @@ -539,6 +541,7 @@ func TestBuilder_Build_SlackConnector(t *testing.T) { rawConfig := connector.RawConfig.(probodconfig.ConnectorConfigOAuth2) assert.Equal(t, "slack-client-id", rawConfig.ClientID) assert.Equal(t, "slack-client-secret", rawConfig.ClientSecret) + rawSettings := connector.RawSettings.(map[string]any) assert.Equal(t, "slack-signing-secret", rawSettings["signing-secret"]) } diff --git a/pkg/bootstrap/write_test.go b/pkg/bootstrap/write_test.go index 3774ab1fb..b17888dea 100644 --- a/pkg/bootstrap/write_test.go +++ b/pkg/bootstrap/write_test.go @@ -46,6 +46,7 @@ func TestWriteConfig(t *testing.T) { require.NoError(t, err) var loaded probodconfig.FullConfig + err = yaml.Unmarshal(data, &loaded) require.NoError(t, err) @@ -143,6 +144,7 @@ func TestWriteConfig_CompleteConfig(t *testing.T) { require.NoError(t, err) var loaded probodconfig.FullConfig + err = yaml.Unmarshal(data, &loaded) require.NoError(t, err) diff --git a/pkg/certmanager/acme.go b/pkg/certmanager/acme.go index c63deb7f5..21f9bbb8e 100644 --- a/pkg/certmanager/acme.go +++ b/pkg/certmanager/acme.go @@ -66,6 +66,7 @@ func NewACMEService( ) (*ACMEService, error) { if accountKey == nil { var err error + accountKey, err = keys.Generate(keyType) if err != nil { return nil, fmt.Errorf("cannot generate account key: %w", err) @@ -130,6 +131,7 @@ func (s *ACMEService) GetHTTPChallenge(ctx context.Context, domain string) (*HTT } var challenge *acme.Challenge + for _, auth := range order.AuthzURLs { authz, err := s.client.GetAuthorization(ctx, auth) if err != nil { @@ -170,7 +172,6 @@ func (s *ACMEService) CompleteHTTPChallenge( ctx context.Context, challenge0 *HTTPChallenge, ) (*Certificate, error) { - challenge1 := &acme.Challenge{ URI: challenge0.URL, Token: challenge0.Token, @@ -206,6 +207,7 @@ func (s *ACMEService) CompleteHTTPChallenge( } certPEM := pem.EncodeCertificate(der[0]) + keyPEM, err := pem.EncodePrivateKey(certKey) if err != nil { return nil, fmt.Errorf("cannot encode key: %w", err) @@ -215,6 +217,7 @@ func (s *ACMEService) CompleteHTTPChallenge( if len(der) > 1 { chainDER = der[1:] } + chainPEM := pem.EncodeCertificateChain(chainDER) return &Certificate{ diff --git a/pkg/certmanager/acme_challenge_handler.go b/pkg/certmanager/acme_challenge_handler.go index 0602fc7be..209f57ff6 100644 --- a/pkg/certmanager/acme_challenge_handler.go +++ b/pkg/certmanager/acme_challenge_handler.go @@ -62,6 +62,7 @@ func (h *ACMEChallengeHandler) Handle(next http.Handler) http.Handler { ) http.NotFound(w, r) + return } diff --git a/pkg/certmanager/cache_store.go b/pkg/certmanager/cache_store.go index 1419575d5..c4fbf5353 100644 --- a/pkg/certmanager/cache_store.go +++ b/pkg/certmanager/cache_store.go @@ -47,6 +47,7 @@ func NewCacheStore( func (w *CacheStore) WarmCache(ctx context.Context) error { w.logger.InfoCtx(ctx, "warming certificate cache") + startTime := time.Now() err := w.pg.WithConn( @@ -65,6 +66,7 @@ func (w *CacheStore) WarmCache(ctx context.Context) error { w.logger.InfoCtx(ctx, "found active certificates to cache", log.Int("count", len(domains))) successCount := 0 + for _, domain := range domains { select { case <-ctx.Done(): @@ -80,10 +82,10 @@ func (w *CacheStore) WarmCache(ctx context.Context) error { } w.logger.InfoCtx(ctx, "successfully warmed cache", log.Int("success_count", successCount), log.Int("total_count", len(domains))) + return nil }, ) - if err != nil { return fmt.Errorf("cannot warm certificate cache: %w", err) } diff --git a/pkg/certmanager/provisioner.go b/pkg/certmanager/provisioner.go index 1900db185..55bdc5cd6 100644 --- a/pkg/certmanager/provisioner.go +++ b/pkg/certmanager/provisioner.go @@ -160,6 +160,7 @@ func (p *Provisioner) checkCAARecords(domain string) error { } var caaRecords []*dns.CAA + for _, rr := range resp.Answer { if caa, ok := rr.(*dns.CAA); ok { caaRecords = append(caaRecords, caa) @@ -235,7 +236,6 @@ func (p *Provisioner) checkPendingDomains(ctx context.Context) error { return nil }, ) - if err != nil { return fmt.Errorf("cannot provision domains: %w", err) } @@ -343,6 +343,7 @@ func (p *Provisioner) provisionDomainCertificate( ) errMsg := err.Error() + domain.ProvisioningError = &errMsg if err := domain.Update(ctx, tx, coredata.NewNoScope()); err != nil { return fmt.Errorf("cannot update domain with provisioning error: %w", err) @@ -360,6 +361,7 @@ func (p *Provisioner) provisionDomainCertificate( ) errMsg := err.Error() + domain.ProvisioningError = &errMsg if err := domain.Update(ctx, tx, coredata.NewNoScope()); err != nil { return fmt.Errorf("cannot update domain with provisioning error: %w", err) @@ -383,6 +385,7 @@ func (p *Provisioner) provisionDomainCertificate( log.String("domain", domain.Domain), log.Error(err), ) + return err } @@ -466,10 +469,12 @@ func (p *Provisioner) provisionDomainCertificate( ) domain.ProvisioningError = nil + domain.SSLCertificatePEM = cert.CertPEM if err := domain.EncryptPrivateKey(cert.KeyPEM, p.encryptionKey); err != nil { return fmt.Errorf("cannot encrypt private key: %w", err) } + chainStr := string(cert.ChainPEM) domain.SSLCertificateChain = &chainStr domain.SSLExpiresAt = &cert.ExpiresAt diff --git a/pkg/certmanager/renewer.go b/pkg/certmanager/renewer.go index 81278dcd4..6ba51886c 100644 --- a/pkg/certmanager/renewer.go +++ b/pkg/certmanager/renewer.go @@ -78,6 +78,7 @@ func (r *Renewer) checkAndRenew(ctx context.Context) error { ctx, func(ctx context.Context, tx pg.Tx) error { var caches coredata.CachedCertificates + cacheCount, err := caches.CountAll(ctx, tx) if err != nil { r.logger.ErrorCtx(ctx, "cannot count certificate cache", log.Error(err)) @@ -97,6 +98,7 @@ func (r *Renewer) checkAndRenew(ctx context.Context) error { } domains := coredata.CustomDomains{} + scope := coredata.NewNoScope() if err := domains.ListDomainsForRenewal(ctx, tx, scope); err != nil { return fmt.Errorf("cannot list domains for renewal: %w", err) @@ -116,6 +118,7 @@ func (r *Renewer) checkAndRenew(ctx context.Context) error { } r.logger.InfoCtx(ctx, "renewing certificate for domain", log.String("domain", domain.Domain)) + if err := r.renewDomain(ctx, tx, domain.ID); err != nil { r.logger.ErrorCtx(ctx, "cannot renew certificate", log.String("domain", domain.Domain), log.Error(err)) } else { diff --git a/pkg/certmanager/selector.go b/pkg/certmanager/selector.go index da8924454..3e7a94a45 100644 --- a/pkg/certmanager/selector.go +++ b/pkg/certmanager/selector.go @@ -71,6 +71,7 @@ func (s *Selector) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, } s.cache.Store(domain, cert) + return cert, nil } @@ -78,6 +79,7 @@ func (s *Selector) loadFromDatabase(domain string) (*tls.Certificate, error) { ctx := context.Background() var cert *tls.Certificate + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { @@ -103,10 +105,10 @@ func (s *Selector) loadFromDatabase(domain string) (*tls.Certificate, error) { } cert = &tlsCert + return nil }, ) - if err != nil { return nil, err } diff --git a/pkg/cli/api/client.go b/pkg/cli/api/client.go index 5c759e799..aa358a991 100644 --- a/pkg/cli/api/client.go +++ b/pkg/cli/api/client.go @@ -86,6 +86,7 @@ func NewClient(host string, token string, endpoint string, timeout time.Duration for _, opt := range opts { opt(c) } + return c } @@ -106,9 +107,11 @@ func (c *Client) Do( if len(resp.Errors) > 0 { var msg strings.Builder msg.WriteString(resp.Errors[0].Message) + for _, e := range resp.Errors[1:] { msg.WriteString("; " + e.Message) } + return nil, fmt.Errorf("GraphQL error: %s", msg.String()) } @@ -171,6 +174,7 @@ func (c *Client) doRequest( } reqURL := host + c.endpoint + req, err := http.NewRequest(http.MethodPost, reqURL, bytes.NewReader(body)) if err != nil { return nil, 0, fmt.Errorf("cannot create HTTP request: %w", err) @@ -184,6 +188,7 @@ func (c *Client) doRequest( if err != nil { return nil, 0, fmt.Errorf("cannot send HTTP request: %w", err) } + defer func() { _ = resp.Body.Close() }() respBody, err := io.ReadAll(resp.Body) @@ -217,9 +222,11 @@ func (c *Client) DoUpload( if len(resp.Errors) > 0 { var msg strings.Builder msg.WriteString(resp.Errors[0].Message) + for _, e := range resp.Errors[1:] { msg.WriteString("; " + e.Message) } + return nil, fmt.Errorf("GraphQL error: %s", msg.String()) } @@ -234,6 +241,7 @@ func (c *Client) doUploadRequest( file io.Reader, ) ([]byte, error) { var buf bytes.Buffer + writer := multipart.NewWriter(&buf) // Part 1: operations @@ -281,6 +289,7 @@ func (c *Client) doUploadRequest( } reqURL := host + c.endpoint + req, err := http.NewRequest(http.MethodPost, reqURL, &buf) if err != nil { return nil, fmt.Errorf("cannot create HTTP request: %w", err) @@ -294,6 +303,7 @@ func (c *Client) doUploadRequest( if err != nil { return nil, fmt.Errorf("cannot send HTTP request: %w", err) } + defer func() { _ = resp.Body.Close() }() respBody, err := io.ReadAll(resp.Body) @@ -347,6 +357,7 @@ func (c *Client) tryRefreshToken() error { if err != nil { return fmt.Errorf("cannot send refresh request: %w", err) } + defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) diff --git a/pkg/cli/api/pagination.go b/pkg/cli/api/pagination.go index 4c6c95d6a..caea08e1b 100644 --- a/pkg/cli/api/pagination.go +++ b/pkg/cli/api/pagination.go @@ -58,6 +58,7 @@ func Paginate[T any]( if remaining <= 0 { break } + vars["first"] = remaining data, err := client.Do(query, vars) diff --git a/pkg/cli/config/config.go b/pkg/cli/config/config.go index e36bb160d..5affca445 100644 --- a/pkg/cli/config/config.go +++ b/pkg/cli/config/config.go @@ -93,15 +93,18 @@ func (c *Config) Set(key, value string) error { if value != "enabled" && value != "disabled" { return fmt.Errorf("valid values for prompt are 'enabled' or 'disabled'") } + c.Prompt = value case "http_timeout": if _, err := time.ParseDuration(value); err != nil { return fmt.Errorf("invalid duration for http_timeout: %w", err) } + c.HTTPTimeout = value default: return fmt.Errorf("unknown configuration key: %s", key) } + return nil } @@ -123,6 +126,7 @@ func configDir() (string, error) { if err != nil { return "", fmt.Errorf("cannot determine config directory: %w", err) } + return filepath.Join(dir, "prb"), nil } @@ -131,6 +135,7 @@ func configPath() (string, error) { if err != nil { return "", err } + return filepath.Join(dir, "config.yaml"), nil } @@ -145,6 +150,7 @@ func Load() (*Config, error) { if os.IsNotExist(err) { return &Config{Hosts: make(map[string]*HostConfig)}, nil } + return nil, fmt.Errorf("cannot read config file: %w", err) } @@ -161,6 +167,7 @@ func Load() (*Config, error) { for host, hc := range cfg.Hosts { normalized[normalizeHost(host)] = hc } + cfg.Hosts = normalized if cfg.ActiveHost != "" { @@ -206,13 +213,16 @@ func normalizeHost(host string) string { func (c *Config) DefaultHost() (string, *HostConfig, error) { if host := os.Getenv("PROBO_HOST"); host != "" { host = normalizeHost(host) + hc := &HostConfig{} if saved, ok := c.Hosts[host]; ok { *hc = *saved } + if token := os.Getenv("PROBO_TOKEN"); token != "" { hc.Token = token } + return host, hc, nil } @@ -224,6 +234,7 @@ func (c *Config) DefaultHost() (string, *HostConfig, error) { } host := hosts[0] + if c.ActiveHost != "" { if _, ok := c.Hosts[c.ActiveHost]; ok { host = c.ActiveHost diff --git a/pkg/cmd/access-review/campaign/cancel/cancel.go b/pkg/cmd/access-review/campaign/cancel/cancel.go index b4ac9a534..897e17ea2 100644 --- a/pkg/cmd/access-review/campaign/cancel/cancel.go +++ b/pkg/cmd/access-review/campaign/cancel/cancel.go @@ -60,6 +60,7 @@ func NewCmdCancel(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Cancel access review campaign %s?", args[0])). Value(&confirmed). @@ -67,6 +68,7 @@ func NewCmdCancel(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/access-review/campaign/close/close.go b/pkg/cmd/access-review/campaign/close/close.go index e25690797..b776a7ab6 100644 --- a/pkg/cmd/access-review/campaign/close/close.go +++ b/pkg/cmd/access-review/campaign/close/close.go @@ -60,6 +60,7 @@ func NewCmdClose(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Close access review campaign %s?", args[0])). Value(&confirmed). @@ -67,6 +68,7 @@ func NewCmdClose(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/access-review/campaign/delete/delete.go b/pkg/cmd/access-review/campaign/delete/delete.go index 133f77a6a..f686e71bd 100644 --- a/pkg/cmd/access-review/campaign/delete/delete.go +++ b/pkg/cmd/access-review/campaign/delete/delete.go @@ -45,6 +45,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete access review campaign %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/access-review/campaign/list/list.go b/pkg/cmd/access-review/campaign/list/list.go index fe854c670..9d1081c3f 100644 --- a/pkg/cmd/access-review/campaign/list/list.go +++ b/pkg/cmd/access-review/campaign/list/list.go @@ -116,6 +116,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -137,12 +138,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrg) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + return &resp.Node.AccessReviewCampaigns, nil }, ) @@ -154,6 +158,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if campaigns == nil { campaigns = []campaignNode{} } + return cmdutil.PrintJSON(f.IOStreams.Out, campaigns) } diff --git a/pkg/cmd/access-review/campaign/start/start.go b/pkg/cmd/access-review/campaign/start/start.go index af318aefd..9cc0cbcd7 100644 --- a/pkg/cmd/access-review/campaign/start/start.go +++ b/pkg/cmd/access-review/campaign/start/start.go @@ -60,6 +60,7 @@ func NewCmdStart(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Start access review campaign %s?", args[0])). Value(&confirmed). @@ -67,6 +68,7 @@ func NewCmdStart(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/access-review/campaign/view/view.go b/pkg/cmd/access-review/campaign/view/view.go index 17dc2f920..a5619b0fe 100644 --- a/pkg/cmd/access-review/campaign/view/view.go +++ b/pkg/cmd/access-review/campaign/view/view.go @@ -131,6 +131,7 @@ func NewCmdView(f *cmdutil.Factory) *cobra.Command { if c.StartedAt != nil { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Started:"), cmdutil.FormatTime(*c.StartedAt)) } + if c.CompletedAt != nil { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Completed:"), cmdutil.FormatTime(*c.CompletedAt)) } diff --git a/pkg/cmd/access-review/entry/decideall/decideall.go b/pkg/cmd/access-review/entry/decideall/decideall.go index 8709ebf58..774ad78a0 100644 --- a/pkg/cmd/access-review/entry/decideall/decideall.go +++ b/pkg/cmd/access-review/entry/decideall/decideall.go @@ -102,6 +102,7 @@ func NewCmdDecideAll(f *cmdutil.Factory) *cobra.Command { if flagNote != "" { d["decisionNote"] = flagNote } + decisions[i] = d } diff --git a/pkg/cmd/access-review/entry/list/list.go b/pkg/cmd/access-review/entry/list/list.go index 047dde27f..1f2c6f064 100644 --- a/pkg/cmd/access-review/entry/list/list.go +++ b/pkg/cmd/access-review/entry/list/list.go @@ -168,6 +168,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -179,6 +180,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { } filter := map[string]any{} + if flagDecision != "" { if err := cmdutil.ValidateEnum( "decision", @@ -187,8 +189,10 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { ); err != nil { return err } + filter["decision"] = flagDecision } + if flagFlag != "" { if err := cmdutil.ValidateEnum( "flag", @@ -202,8 +206,10 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { ); err != nil { return err } + filter["flag"] = flagFlag } + if flagIncTag != "" { if err := cmdutil.ValidateEnum( "incremental-tag", @@ -212,11 +218,14 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { ); err != nil { return err } + filter["incrementalTag"] = flagIncTag } + if cmd.Flags().Changed("is-admin") { filter["isAdmin"] = *flagIsAdmin } + if flagAuthMethod != "" { if err := cmdutil.ValidateEnum( "auth-method", @@ -225,8 +234,10 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { ); err != nil { return err } + filter["authMethod"] = flagAuthMethod } + if flagAccountType != "" { if err := cmdutil.ValidateEnum( "account-type", @@ -235,8 +246,10 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { ); err != nil { return err } + filter["accountType"] = flagAccountType } + if len(filter) > 0 { variables["filter"] = filter } @@ -256,12 +269,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("campaign %s not found", args[0]) } + if resp.Node.Typename != "AccessReviewCampaign" { return nil, fmt.Errorf("expected AccessReviewCampaign node, got %s", resp.Node.Typename) } + return &resp.Node.Entries, nil }, ) @@ -273,6 +289,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if entries == nil { entries = []entryNode{} } + return cmdutil.PrintJSON(f.IOStreams.Out, entries) } @@ -287,6 +304,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if e.IsAdmin { admin = "yes" } + rows = append(rows, []string{ e.ID, e.Email, diff --git a/pkg/cmd/access-review/source/create/create.go b/pkg/cmd/access-review/source/create/create.go index c21ffaaf0..6ab26e89f 100644 --- a/pkg/cmd/access-review/source/create/create.go +++ b/pkg/cmd/access-review/source/create/create.go @@ -106,6 +106,7 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if err != nil { return fmt.Errorf("cannot read CSV file: %w", err) } + input["csvData"] = string(csvData) } diff --git a/pkg/cmd/access-review/source/delete/delete.go b/pkg/cmd/access-review/source/delete/delete.go index efcfe134c..7ec1acb54 100644 --- a/pkg/cmd/access-review/source/delete/delete.go +++ b/pkg/cmd/access-review/source/delete/delete.go @@ -45,6 +45,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete access source %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/access-review/source/list/list.go b/pkg/cmd/access-review/source/list/list.go index 8908cf376..1a319cbeb 100644 --- a/pkg/cmd/access-review/source/list/list.go +++ b/pkg/cmd/access-review/source/list/list.go @@ -110,6 +110,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -131,12 +132,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrg) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + return &resp.Node.AccessSources, nil }, ) @@ -148,6 +152,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if sources == nil { sources = []sourceNode{} } + return cmdutil.PrintJSON(f.IOStreams.Out, sources) } diff --git a/pkg/cmd/access-review/source/update/update.go b/pkg/cmd/access-review/source/update/update.go index 3bd0b99ea..4113465c5 100644 --- a/pkg/cmd/access-review/source/update/update.go +++ b/pkg/cmd/access-review/source/update/update.go @@ -92,6 +92,7 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if err != nil { return fmt.Errorf("cannot read CSV file: %w", err) } + input["csvData"] = string(csvData) } diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index d11cd03d0..24c139526 100644 --- a/pkg/cmd/api/api.go +++ b/pkg/cmd/api/api.go @@ -87,10 +87,12 @@ func NewCmdAPI(f *cmdutil.Factory) *cobra.Command { if len(args) == 0 && f.IOStreams.IsStdinTTY() { return fmt.Errorf("query argument is required when not reading from stdin") } + data, err := io.ReadAll(f.IOStreams.In) if err != nil { return fmt.Errorf("cannot read query from stdin: %w", err) } + query = string(data) } @@ -154,6 +156,7 @@ func parseFields(fields []string) (map[string]any, error) { if err := json.Unmarshal([]byte(value), &parsed); err != nil { parsed = value } + vars[key] = parsed } diff --git a/pkg/cmd/asset/create/create.go b/pkg/cmd/asset/create/create.go index 407b86f0e..4724daed2 100644 --- a/pkg/cmd/asset/create/create.go +++ b/pkg/cmd/asset/create/create.go @@ -127,6 +127,7 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagName == "" { return fmt.Errorf("name is required; pass --name or run interactively") } + if flagAssetType == "" { return fmt.Errorf("asset type is required; pass --asset-type or run interactively") } @@ -140,12 +141,15 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("amount") { input["amount"] = flagAmount } + if flagOwner != "" { input["ownerId"] = flagOwner } + if flagDataTypesStored != "" { input["dataTypesStored"] = flagDataTypesStored } + if len(flagThirdPartyIDs) > 0 { input["thirdPartyIds"] = flagThirdPartyIDs } diff --git a/pkg/cmd/asset/delete/delete.go b/pkg/cmd/asset/delete/delete.go index 1826b680c..5a9b614bc 100644 --- a/pkg/cmd/asset/delete/delete.go +++ b/pkg/cmd/asset/delete/delete.go @@ -45,6 +45,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete asset %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/asset/list/list.go b/pkg/cmd/asset/list/list.go index 5d9c0eabf..1cf7e45c9 100644 --- a/pkg/cmd/asset/list/list.go +++ b/pkg/cmd/asset/list/list.go @@ -113,6 +113,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "AMOUNT"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -134,12 +135,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrg) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + return &resp.Node.Assets, nil }, ) diff --git a/pkg/cmd/asset/publish/publish.go b/pkg/cmd/asset/publish/publish.go index 7daa53832..c02bc8387 100644 --- a/pkg/cmd/asset/publish/publish.go +++ b/pkg/cmd/asset/publish/publish.go @@ -96,6 +96,7 @@ func NewCmdPublish(f *cmdutil.Factory) *cobra.Command { if flagOrg == "" { flagOrg = hc.Organization } + if flagOrg == "" { return fmt.Errorf("organization is required: pass --org or run `prb auth login`") } diff --git a/pkg/cmd/asset/update/update.go b/pkg/cmd/asset/update/update.go index 249961618..e45d92f44 100644 --- a/pkg/cmd/asset/update/update.go +++ b/pkg/cmd/asset/update/update.go @@ -87,12 +87,15 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("name") { input["name"] = flagName } + if cmd.Flags().Changed("asset-type") { input["assetType"] = flagAssetType } + if cmd.Flags().Changed("amount") { input["amount"] = flagAmount } + if cmd.Flags().Changed("owner") { if flagOwner == "" { input["ownerId"] = nil @@ -100,9 +103,11 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { input["ownerId"] = flagOwner } } + if cmd.Flags().Changed("data-types-stored") { input["dataTypesStored"] = flagDataTypesStored } + if cmd.Flags().Changed("thirdParty-ids") { input["thirdPartyIds"] = flagThirdPartyIDs } diff --git a/pkg/cmd/audit/create/create.go b/pkg/cmd/audit/create/create.go index e073ad498..9cb309040 100644 --- a/pkg/cmd/audit/create/create.go +++ b/pkg/cmd/audit/create/create.go @@ -141,15 +141,19 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagFramework != "" { input["frameworkId"] = flagFramework } + if flagState != "" { input["state"] = flagState } + if flagValidFrom != "" { input["validFrom"] = flagValidFrom } + if flagValidUntil != "" { input["validUntil"] = flagValidUntil } + if flagTrustCenterVisibility != "" { input["trustCenterVisibility"] = flagTrustCenterVisibility } diff --git a/pkg/cmd/audit/delete/delete.go b/pkg/cmd/audit/delete/delete.go index b0492440b..8ce605bf1 100644 --- a/pkg/cmd/audit/delete/delete.go +++ b/pkg/cmd/audit/delete/delete.go @@ -45,6 +45,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete audit %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/audit/list/list.go b/pkg/cmd/audit/list/list.go index ffaf8db7c..16ad2d4c8 100644 --- a/pkg/cmd/audit/list/list.go +++ b/pkg/cmd/audit/list/list.go @@ -115,6 +115,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "VALID_FROM", "VALID_UNTIL", "STATE"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -136,12 +137,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrg) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + return &resp.Node.Audits, nil }, ) @@ -164,10 +168,12 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if a.ValidFrom != nil { validFrom = *a.ValidFrom } + validUntil := "" if a.ValidUntil != nil { validUntil = *a.ValidUntil } + rows = append(rows, []string{ a.ID, a.Name, diff --git a/pkg/cmd/audit/update/update.go b/pkg/cmd/audit/update/update.go index e24efc271..5c76165e7 100644 --- a/pkg/cmd/audit/update/update.go +++ b/pkg/cmd/audit/update/update.go @@ -88,15 +88,19 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("name") { input["name"] = flagName } + if cmd.Flags().Changed("state") { input["state"] = flagState } + if cmd.Flags().Changed("valid-from") { input["validFrom"] = flagValidFrom } + if cmd.Flags().Changed("valid-until") { input["validUntil"] = flagValidUntil } + if cmd.Flags().Changed("trust-center-visibility") { input["trustCenterVisibility"] = flagTrustCenterVisibility } diff --git a/pkg/cmd/auditlog/list/list.go b/pkg/cmd/auditlog/list/list.go index ff594c1a3..f8f898f0c 100644 --- a/pkg/cmd/auditlog/list/list.go +++ b/pkg/cmd/auditlog/list/list.go @@ -121,6 +121,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -131,15 +132,19 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if flagAction != "" { filter["action"] = flagAction } + if flagActorID != "" { filter["actorId"] = flagActorID } + if flagResourceType != "" { filter["resourceType"] = flagResourceType } + if flagResourceID != "" { filter["resourceId"] = flagResourceID } + if len(filter) > 0 { variables["filter"] = filter } @@ -159,12 +164,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrg) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + return &resp.Node.AuditLogEntries, nil }, ) @@ -176,6 +184,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if entries == nil { entries = []auditLogEntry{} } + return cmdutil.PrintJSON(f.IOStreams.Out, entries) } diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 5e9953238..12a019e6e 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -189,6 +189,7 @@ func NewCmdLogin(f *cmdutil.Factory) *cobra.Command { if orgsErr == nil && len(orgs) > 0 { selected := orgs[0].ID + options := make([]huh.Option[string], 0, len(orgs)+1) for _, org := range orgs { options = append( @@ -199,6 +200,7 @@ func NewCmdLogin(f *cmdutil.Factory) *cobra.Command { ), ) } + options = append( options, huh.NewOption("Skip (no default)", ""), @@ -260,6 +262,7 @@ func normalizeHostToURL(host string) string { if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") { return strings.TrimRight(host, "/") } + return "https://" + strings.TrimRight(host, "/") } @@ -279,6 +282,7 @@ func fetchDiscovery(client *http.Client, baseURL string) (*oidcDiscovery, error) if err != nil { return nil, fmt.Errorf("cannot fetch discovery document: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { @@ -327,6 +331,7 @@ func requestDeviceCode( if err != nil { return nil, fmt.Errorf("cannot request device code: %w", err) } + defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) @@ -391,6 +396,7 @@ func pollForToken( body, err := io.ReadAll(resp.Body) _ = resp.Body.Close() + if err != nil { return nil, fmt.Errorf("cannot read token response: %w", err) } @@ -400,6 +406,7 @@ func pollForToken( if err := json.Unmarshal(body, &token); err != nil { return nil, fmt.Errorf("cannot decode token response: %w", err) } + return &token, nil } diff --git a/pkg/cmd/auth/logout/logout.go b/pkg/cmd/auth/logout/logout.go index 5f5e03c28..533b84e0f 100644 --- a/pkg/cmd/auth/logout/logout.go +++ b/pkg/cmd/auth/logout/logout.go @@ -64,6 +64,7 @@ func NewCmdLogout(f *cmdutil.Factory) *cobra.Command { for i, h := range hosts { options[i] = huh.NewOption(h, h) } + err := huh.NewSelect[string](). Title("Select a host to log out of"). Options(options...). @@ -85,6 +86,7 @@ func NewCmdLogout(f *cmdutil.Factory) *cobra.Command { revokeTokens(flagHost, hc, f) delete(cfg.Hosts, flagHost) + if cfg.ActiveHost == flagHost { cfg.ActiveHost = "" } @@ -152,6 +154,7 @@ func fetchRevocationEndpoint(client *http.Client, baseURL string) (*oidcDiscover if err != nil { return nil, fmt.Errorf("cannot fetch discovery document: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { @@ -194,6 +197,7 @@ func revokeToken( if err != nil { return fmt.Errorf("cannot send revocation request: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { @@ -208,5 +212,6 @@ func normalizeHostToURL(host string) string { if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") { return strings.TrimRight(host, "/") } + return "https://" + strings.TrimRight(host, "/") } diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index bd22aebbf..05cf9fbb4 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -46,6 +46,7 @@ func NewCmdStatus(f *cmdutil.Factory) *cobra.Command { if host == cfg.ActiveHost { label += " (active)" } + _, _ = fmt.Fprintf( f.IOStreams.Out, "%s\n", @@ -56,6 +57,7 @@ func NewCmdStatus(f *cmdutil.Factory) *cobra.Command { if hc.Token != "" { tokenStatus = "set" } + _, _ = fmt.Fprintf( f.IOStreams.Out, " Token: %s\n", diff --git a/pkg/cmd/cmdutil/factory.go b/pkg/cmd/cmdutil/factory.go index 3dd7e17c7..8af69e3b6 100644 --- a/pkg/cmd/cmdutil/factory.go +++ b/pkg/cmd/cmdutil/factory.go @@ -46,6 +46,7 @@ func TokenRefreshOption( hc.Token = accessToken hc.RefreshToken = refreshToken cfg.Hosts[host] = hc + return cfg.Save() }, }) diff --git a/pkg/cmd/cmdutil/flags.go b/pkg/cmd/cmdutil/flags.go index b484247e6..d9e0a2dfb 100644 --- a/pkg/cmd/cmdutil/flags.go +++ b/pkg/cmd/cmdutil/flags.go @@ -39,6 +39,7 @@ func AddOutputFlag(cmd *cobra.Command) *string { "", "Output format: json, table (default)", ) + return &output } @@ -65,6 +66,7 @@ func ValidateEnum(flag string, value string, allowed []string) error { if slices.Contains(allowed, value) { return nil } + return fmt.Errorf( "invalid --%s value %q: valid values are %s", flag, diff --git a/pkg/cmd/cmdutil/json.go b/pkg/cmd/cmdutil/json.go index e051d1269..0aeee3a2d 100644 --- a/pkg/cmd/cmdutil/json.go +++ b/pkg/cmd/cmdutil/json.go @@ -26,6 +26,8 @@ func PrintJSON(out io.Writer, v any) error { if err != nil { return fmt.Errorf("cannot marshal JSON: %w", err) } + _, err = fmt.Fprintln(out, string(data)) + return err } diff --git a/pkg/cmd/cmdutil/table.go b/pkg/cmd/cmdutil/table.go index ef1409c05..210c59a85 100644 --- a/pkg/cmd/cmdutil/table.go +++ b/pkg/cmd/cmdutil/table.go @@ -32,6 +32,7 @@ func NewTable(headers ...string) *table.Table { if row == table.HeaderRow { return headerStyle } + return cellStyle }) } diff --git a/pkg/cmd/cmdutil/time.go b/pkg/cmd/cmdutil/time.go index 75f1b8699..d2a56ee55 100644 --- a/pkg/cmd/cmdutil/time.go +++ b/pkg/cmd/cmdutil/time.go @@ -23,5 +23,6 @@ func FormatTime(raw string) string { if err != nil { return raw } + return t.Local().Format("Jan 02, 2006 15:04 MST") } diff --git a/pkg/cmd/completion/completion.go b/pkg/cmd/completion/completion.go index 87df4ae4b..9484614f8 100644 --- a/pkg/cmd/completion/completion.go +++ b/pkg/cmd/completion/completion.go @@ -56,6 +56,7 @@ PowerShell: DisableFlagsInUseLine: true, RunE: func(cmd *cobra.Command, args []string) error { out := f.IOStreams.Out + switch args[0] { case "bash": return cmd.Root().GenBashCompletionV2(out, true) diff --git a/pkg/cmd/config/list/list.go b/pkg/cmd/config/list/list.go index e660fc2ff..54e653ce4 100644 --- a/pkg/cmd/config/list/list.go +++ b/pkg/cmd/config/list/list.go @@ -39,6 +39,7 @@ func NewCmdConfigList(f *cmdutil.Factory) *cobra.Command { if val == "" { val = "" } + _, _ = fmt.Fprintf(f.IOStreams.Out, "%s=%s\n", key, val) } diff --git a/pkg/cmd/consent-record/list/list.go b/pkg/cmd/consent-record/list/list.go index b68e4770c..04d48a31b 100644 --- a/pkg/cmd/consent-record/list/list.go +++ b/pkg/cmd/consent-record/list/list.go @@ -105,12 +105,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("action") { filter["action"] = flagAction } + if cmd.Flags().Changed("visitor-id") { filter["visitorId"] = flagVisitorID } + if cmd.Flags().Changed("version") { filter["version"] = flagVersion } + if len(filter) > 0 { variables["filter"] = filter } @@ -130,12 +133,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("cookie banner %s not found", flagBannerID) } + if resp.Node.Typename != "CookieBanner" { return nil, fmt.Errorf("expected CookieBanner node, got %s", resp.Node.Typename) } + return &resp.Node.ConsentRecords, nil }, ) @@ -158,10 +164,12 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if r.Regulation != nil { regulation = *r.Regulation } + countryCode := "-" if r.CountryCode != nil { countryCode = *r.CountryCode } + rows = append(rows, []string{r.ID, r.VisitorID, r.Action, r.SDKVersion, regulation, countryCode, r.CreatedAt}) } diff --git a/pkg/cmd/consent-record/view/view.go b/pkg/cmd/consent-record/view/view.go index 79bba9704..067d4d82c 100644 --- a/pkg/cmd/consent-record/view/view.go +++ b/pkg/cmd/consent-record/view/view.go @@ -118,19 +118,24 @@ func NewCmdView(f *cmdutil.Factory) *cobra.Command { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), v.ID) _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Visitor ID:"), v.VisitorID) _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Action:"), v.Action) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("SDK Version:"), v.SdkVersion) if v.Regulation != nil { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Regulation:"), *v.Regulation) } + if v.CountryCode != nil { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Country Code:"), *v.CountryCode) } + if v.IPAddress != nil && *v.IPAddress != "" { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("IP Address:"), *v.IPAddress) } + if v.UserAgent != nil && *v.UserAgent != "" { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("User Agent:"), *v.UserAgent) } + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Consent Data:"), v.ConsentData) _, _ = fmt.Fprintln(out) _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(v.CreatedAt)) diff --git a/pkg/cmd/context/get/get.go b/pkg/cmd/context/get/get.go index eb2cc0f95..db352dd07 100644 --- a/pkg/cmd/context/get/get.go +++ b/pkg/cmd/context/get/get.go @@ -86,6 +86,7 @@ func NewCmdGet(f *cmdutil.Factory) *cobra.Command { if orgID == "" { orgID = hc.Organization } + if orgID == "" { return fmt.Errorf("organization ID is required: pass --org or run `prb auth login`") } diff --git a/pkg/cmd/context/update/update.go b/pkg/cmd/context/update/update.go index adc606a68..c5ddd1aa8 100644 --- a/pkg/cmd/context/update/update.go +++ b/pkg/cmd/context/update/update.go @@ -82,6 +82,7 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if orgID == "" { orgID = hc.Organization } + if orgID == "" { return fmt.Errorf("organization ID is required: pass --org or run `prb auth login`") } @@ -93,15 +94,19 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("product") { input["product"] = flagProduct } + if cmd.Flags().Changed("architecture") { input["architecture"] = flagArchitecture } + if cmd.Flags().Changed("team") { input["team"] = flagTeam } + if cmd.Flags().Changed("processes") { input["processes"] = flagProcesses } + if cmd.Flags().Changed("customers") { input["customers"] = flagCustomers } diff --git a/pkg/cmd/control/delete/delete.go b/pkg/cmd/control/delete/delete.go index a29357a66..a8a2105ac 100644 --- a/pkg/cmd/control/delete/delete.go +++ b/pkg/cmd/control/delete/delete.go @@ -45,6 +45,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete control %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/control/list/list.go b/pkg/cmd/control/list/list.go index e3fd10523..a987a1a8a 100644 --- a/pkg/cmd/control/list/list.go +++ b/pkg/cmd/control/list/list.go @@ -112,6 +112,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "SECTION_TITLE"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -139,12 +140,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("framework %s not found", flagFramework) } + if resp.Node.Typename != "Framework" { return nil, fmt.Errorf("expected Framework node, got %s", resp.Node.Typename) } + return &resp.Node.Controls, nil }, ) @@ -167,6 +171,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if c.BestPractice { bp = "Yes" } + rows = append(rows, []string{ c.ID, c.SectionTitle, diff --git a/pkg/cmd/control/update/update.go b/pkg/cmd/control/update/update.go index 983ad6d4b..aedd783a4 100644 --- a/pkg/cmd/control/update/update.go +++ b/pkg/cmd/control/update/update.go @@ -102,9 +102,11 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("section-title") { input["sectionTitle"] = flagSectionTitle } + if cmd.Flags().Changed("name") { input["name"] = flagName } + if cmd.Flags().Changed("description") { if flagDescription == "" { input["description"] = nil @@ -112,15 +114,19 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { input["description"] = flagDescription } } + if cmd.Flags().Changed("best-practice") { input["bestPractice"] = flagBestPractice } + if cmd.Flags().Changed("maturity-level") { if err := cmdutil.ValidateEnum("maturity-level", flagMaturityLevel, maturityLevelValues); err != nil { return err } + input["maturityLevel"] = flagMaturityLevel } + if cmd.Flags().Changed("not-implemented-justification") { if flagNotImplementedJustification == "" { input["notImplementedJustification"] = nil diff --git a/pkg/cmd/control/view/view.go b/pkg/cmd/control/view/view.go index 2b977cdbf..12559e0f8 100644 --- a/pkg/cmd/control/view/view.go +++ b/pkg/cmd/control/view/view.go @@ -143,7 +143,9 @@ func NewCmdView(f *cmdutil.Factory) *cobra.Command { if c.BestPractice { bp = "Yes" } + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Best Practice:"), bp) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Maturity:"), docgen.MaturityLabel(coredata.ControlMaturityLevel(c.MaturityLevel))) if c.MaturityLevel == "NONE" && c.NotImplementedJustification != nil && *c.NotImplementedJustification != "" { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Justification:"), *c.NotImplementedJustification) diff --git a/pkg/cmd/cookie-banner/create/create.go b/pkg/cmd/cookie-banner/create/create.go index 2ce77cba4..e0e2126f1 100644 --- a/pkg/cmd/cookie-banner/create/create.go +++ b/pkg/cmd/cookie-banner/create/create.go @@ -85,6 +85,7 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagOrg == "" { flagOrg = hc.Organization } + if flagOrg == "" { return fmt.Errorf("organization is required; pass --org or set a default with 'prb auth login'") } @@ -95,11 +96,13 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { return err } } + if flagOrigin == "" { if err := huh.NewInput().Title("Website origin (e.g. https://example.com)").Value(&flagOrigin).Run(); err != nil { return err } } + if flagCookiePolicyUrl == "" { if err := huh.NewInput().Title("Cookie policy URL").Value(&flagCookiePolicyUrl).Run(); err != nil { return err @@ -110,9 +113,11 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagName == "" { return fmt.Errorf("name is required; pass --name or run interactively") } + if flagOrigin == "" { return fmt.Errorf("origin is required; pass --origin or run interactively") } + if flagCookiePolicyUrl == "" { return fmt.Errorf("cookie-policy-url is required; pass --cookie-policy-url or run interactively") } diff --git a/pkg/cmd/cookie-banner/delete/delete.go b/pkg/cmd/cookie-banner/delete/delete.go index 0218d8cfd..cafa9f475 100644 --- a/pkg/cmd/cookie-banner/delete/delete.go +++ b/pkg/cmd/cookie-banner/delete/delete.go @@ -43,10 +43,12 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if !f.IOStreams.IsInteractive() { return fmt.Errorf("cannot delete cookie banner: confirmation required, use --yes to confirm") } + var confirmed bool if err := huh.NewConfirm().Title(fmt.Sprintf("Delete cookie banner %s?", args[0])).Value(&confirmed).Run(); err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/cookie-banner/list/list.go b/pkg/cmd/cookie-banner/list/list.go index d81f2f05b..8b27e446a 100644 --- a/pkg/cmd/cookie-banner/list/list.go +++ b/pkg/cmd/cookie-banner/list/list.go @@ -93,6 +93,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if flagOrg == "" { flagOrg = hc.Organization } + if flagOrg == "" { return fmt.Errorf("organization is required; pass --org or set a default with 'prb auth login'") } @@ -114,12 +115,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrg) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + return &resp.Node.CookieBanners, nil }, ) diff --git a/pkg/cmd/cookie-banner/translate/translate.go b/pkg/cmd/cookie-banner/translate/translate.go index 00cbcc3f5..9df025801 100644 --- a/pkg/cmd/cookie-banner/translate/translate.go +++ b/pkg/cmd/cookie-banner/translate/translate.go @@ -48,6 +48,7 @@ func NewCmdTranslate(f *cmdutil.Factory) *cobra.Command { if flagLanguage == "" { return fmt.Errorf("--language is required") } + if flagTranslations == "" { return fmt.Errorf("--translations is required") } diff --git a/pkg/cmd/cookie-banner/update/update.go b/pkg/cmd/cookie-banner/update/update.go index 10e9bac35..1af6b0b57 100644 --- a/pkg/cmd/cookie-banner/update/update.go +++ b/pkg/cmd/cookie-banner/update/update.go @@ -80,15 +80,19 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("name") { input["name"] = flagName } + if cmd.Flags().Changed("cookie-policy-url") { input["cookiePolicyUrl"] = flagCookiePolicyUrl } + if cmd.Flags().Changed("privacy-policy-url") { input["privacyPolicyUrl"] = flagPrivacyPolicyUrl } + if cmd.Flags().Changed("consent-expiry-days") { input["consentExpiryDays"] = flagConsentExpiry } + if cmd.Flags().Changed("default-language") { input["defaultLanguage"] = flagDefaultLanguage } diff --git a/pkg/cmd/cookie-banner/view/view.go b/pkg/cmd/cookie-banner/view/view.go index 26baeda9c..9569de017 100644 --- a/pkg/cmd/cookie-banner/view/view.go +++ b/pkg/cmd/cookie-banner/view/view.go @@ -122,10 +122,12 @@ func NewCmdView(f *cmdutil.Factory) *cobra.Command { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("State:"), v.State) _, _ = fmt.Fprintf(out, "%s%d days\n", label.Render("Consent Expiry:"), v.ConsentExpiryDays) _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Default Language:"), v.DefaultLanguage) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Cookie Policy:"), v.CookiePolicyUrl) if v.PrivacyPolicyUrl != nil && *v.PrivacyPolicyUrl != "" { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Privacy Policy:"), *v.PrivacyPolicyUrl) } + _, _ = fmt.Fprintln(out) _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(v.CreatedAt)) _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(v.UpdatedAt)) diff --git a/pkg/cmd/cookie-category/create/create.go b/pkg/cmd/cookie-category/create/create.go index 7e62edc83..92a2bf699 100644 --- a/pkg/cmd/cookie-category/create/create.go +++ b/pkg/cmd/cookie-category/create/create.go @@ -94,11 +94,13 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { return err } } + if flagSlug == "" { if err := huh.NewInput().Title("Category slug").Value(&flagSlug).Run(); err != nil { return err } } + if flagDescription == "" { if err := huh.NewText().Title("Description").Value(&flagDescription).Run(); err != nil { return err @@ -109,6 +111,7 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagName == "" { return fmt.Errorf("name is required; pass --name or run interactively") } + if flagSlug == "" { return fmt.Errorf("slug is required; pass --slug or run interactively") } diff --git a/pkg/cmd/cookie-category/delete/delete.go b/pkg/cmd/cookie-category/delete/delete.go index a6a41eab9..5890fd3bf 100644 --- a/pkg/cmd/cookie-category/delete/delete.go +++ b/pkg/cmd/cookie-category/delete/delete.go @@ -46,10 +46,12 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if !f.IOStreams.IsInteractive() { return fmt.Errorf("cannot delete cookie category: confirmation required, use --yes to confirm") } + var confirmed bool if err := huh.NewConfirm().Title(fmt.Sprintf("Delete cookie category %s?", args[0])).Value(&confirmed).Run(); err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/cookie-category/list/list.go b/pkg/cmd/cookie-category/list/list.go index dd0892231..db5b5c0b2 100644 --- a/pkg/cmd/cookie-category/list/list.go +++ b/pkg/cmd/cookie-category/list/list.go @@ -113,12 +113,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("cookie banner %s not found", flagBannerID) } + if resp.Node.Typename != "CookieBanner" { return nil, fmt.Errorf("expected CookieBanner node, got %s", resp.Node.Typename) } + return &resp.Node.Categories, nil }, ) diff --git a/pkg/cmd/cookie-category/update/update.go b/pkg/cmd/cookie-category/update/update.go index 475d2dbdd..e99e6cf1b 100644 --- a/pkg/cmd/cookie-category/update/update.go +++ b/pkg/cmd/cookie-category/update/update.go @@ -84,9 +84,11 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("name") { input["name"] = flagName } + if cmd.Flags().Changed("slug") { input["slug"] = flagSlug } + if cmd.Flags().Changed("description") { input["description"] = flagDescription } diff --git a/pkg/cmd/cookie-category/view/view.go b/pkg/cmd/cookie-category/view/view.go index 2bc64bcfe..73c481939 100644 --- a/pkg/cmd/cookie-category/view/view.go +++ b/pkg/cmd/cookie-category/view/view.go @@ -119,13 +119,16 @@ func NewCmdView(f *cmdutil.Factory) *cobra.Command { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Slug:"), v.Slug) _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Description:"), v.Description) _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Kind:"), v.Kind) + _, _ = fmt.Fprintf(out, "%s%d\n", label.Render("Rank:"), v.Rank) if len(v.GcmConsentTypes) > 0 { _, _ = fmt.Fprintf(out, "%s%v\n", label.Render("GCM Consent Types:"), v.GcmConsentTypes) } + if v.PosthogConsent != "" { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("PostHog Consent:"), v.PosthogConsent) } + _, _ = fmt.Fprintln(out) _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(v.CreatedAt)) _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(v.UpdatedAt)) diff --git a/pkg/cmd/datum/create/create.go b/pkg/cmd/datum/create/create.go index 58375b3bf..ae48aba52 100644 --- a/pkg/cmd/datum/create/create.go +++ b/pkg/cmd/datum/create/create.go @@ -125,6 +125,7 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagName == "" { return fmt.Errorf("name is required; pass --name or run interactively") } + if flagClassification == "" { return fmt.Errorf("data classification is required; pass --data-classification or run interactively") } @@ -138,6 +139,7 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagOwner != "" { input["ownerId"] = flagOwner } + if len(flagThirdPartyIDs) > 0 { input["thirdPartyIds"] = flagThirdPartyIDs } diff --git a/pkg/cmd/datum/delete/delete.go b/pkg/cmd/datum/delete/delete.go index 47d194dbf..405d97f8f 100644 --- a/pkg/cmd/datum/delete/delete.go +++ b/pkg/cmd/datum/delete/delete.go @@ -45,6 +45,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete datum %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/datum/list/list.go b/pkg/cmd/datum/list/list.go index 852a35b8c..a5814ad82 100644 --- a/pkg/cmd/datum/list/list.go +++ b/pkg/cmd/datum/list/list.go @@ -111,6 +111,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "NAME", "DATA_CLASSIFICATION"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -132,12 +133,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(raw, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrg) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + return &resp.Node.Data, nil }, ) diff --git a/pkg/cmd/datum/publish/publish.go b/pkg/cmd/datum/publish/publish.go index f620f7b33..98ff19cc8 100644 --- a/pkg/cmd/datum/publish/publish.go +++ b/pkg/cmd/datum/publish/publish.go @@ -96,6 +96,7 @@ func NewCmdPublish(f *cmdutil.Factory) *cobra.Command { if flagOrg == "" { flagOrg = hc.Organization } + if flagOrg == "" { return fmt.Errorf("organization is required: pass --org or run `prb auth login`") } diff --git a/pkg/cmd/datum/update/update.go b/pkg/cmd/datum/update/update.go index 8e46437ca..77b3b7309 100644 --- a/pkg/cmd/datum/update/update.go +++ b/pkg/cmd/datum/update/update.go @@ -83,9 +83,11 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("name") { input["name"] = flagName } + if cmd.Flags().Changed("data-classification") { input["dataClassification"] = flagClassification } + if cmd.Flags().Changed("owner") { if flagOwner == "" { input["ownerId"] = nil @@ -93,6 +95,7 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { input["ownerId"] = flagOwner } } + if cmd.Flags().Changed("thirdParty-ids") { input["thirdPartyIds"] = flagThirdPartyIDs } diff --git a/pkg/cmd/document/create/create.go b/pkg/cmd/document/create/create.go index 1b4bf02ef..7dae45c74 100644 --- a/pkg/cmd/document/create/create.go +++ b/pkg/cmd/document/create/create.go @@ -157,9 +157,11 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagTitle == "" { return fmt.Errorf("title is required; pass --title or run interactively") } + if flagDocumentType == "" { return fmt.Errorf("document type is required; pass --document-type or run interactively") } + if flagClassification == "" { return fmt.Errorf("classification is required; pass --classification or run interactively") } @@ -199,6 +201,7 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { ); err != nil { return err } + input["trustCenterVisibility"] = flagTrustCenterVisibility } diff --git a/pkg/cmd/document/delete-draft/delete_draft.go b/pkg/cmd/document/delete-draft/delete_draft.go index e3e0f86d3..2b315758c 100644 --- a/pkg/cmd/document/delete-draft/delete_draft.go +++ b/pkg/cmd/document/delete-draft/delete_draft.go @@ -47,6 +47,7 @@ func NewCmdDeleteDraft(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete draft for document %s?", args[0])). Value(&confirmed). @@ -54,6 +55,7 @@ func NewCmdDeleteDraft(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/document/delete/delete.go b/pkg/cmd/document/delete/delete.go index c3067a886..35d498ba7 100644 --- a/pkg/cmd/document/delete/delete.go +++ b/pkg/cmd/document/delete/delete.go @@ -45,6 +45,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete document %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/document/list-approval-decisions/list_approval_decisions.go b/pkg/cmd/document/list-approval-decisions/list_approval_decisions.go index c962ab149..e5b79f434 100644 --- a/pkg/cmd/document/list-approval-decisions/list_approval_decisions.go +++ b/pkg/cmd/document/list-approval-decisions/list_approval_decisions.go @@ -109,6 +109,7 @@ func NewCmdListApprovalDecisions(f *cmdutil.Factory) *cobra.Command { ); err != nil { return err } + if err := cmdutil.ValidateEnum( "order-direction", flagOrderDir, @@ -116,6 +117,7 @@ func NewCmdListApprovalDecisions(f *cmdutil.Factory) *cobra.Command { ); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -137,12 +139,15 @@ func NewCmdListApprovalDecisions(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("approval quorum %s not found", args[0]) } + if resp.Node.Typename != "DocumentVersionApprovalQuorum" { return nil, fmt.Errorf("expected DocumentVersionApprovalQuorum node, got %s", resp.Node.Typename) } + return &resp.Node.Decisions, nil }, ) diff --git a/pkg/cmd/document/list-approval-quorums/list_approval_quorums.go b/pkg/cmd/document/list-approval-quorums/list_approval_quorums.go index 0a1a9b837..921a94103 100644 --- a/pkg/cmd/document/list-approval-quorums/list_approval_quorums.go +++ b/pkg/cmd/document/list-approval-quorums/list_approval_quorums.go @@ -101,6 +101,7 @@ func NewCmdListApprovalQuorums(f *cmdutil.Factory) *cobra.Command { ); err != nil { return err } + if err := cmdutil.ValidateEnum( "order-direction", flagOrderDir, @@ -108,6 +109,7 @@ func NewCmdListApprovalQuorums(f *cmdutil.Factory) *cobra.Command { ); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -129,12 +131,15 @@ func NewCmdListApprovalQuorums(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("document version %s not found", args[0]) } + if resp.Node.Typename != "DocumentVersion" { return nil, fmt.Errorf("expected DocumentVersion node, got %s", resp.Node.Typename) } + return &resp.Node.ApprovalQuorums, nil }, ) diff --git a/pkg/cmd/document/list-versions/list_versions.go b/pkg/cmd/document/list-versions/list_versions.go index b681502d1..f350b5a1d 100644 --- a/pkg/cmd/document/list-versions/list_versions.go +++ b/pkg/cmd/document/list-versions/list_versions.go @@ -117,6 +117,7 @@ func NewCmdListVersions(f *cmdutil.Factory) *cobra.Command { ); err != nil { return err } + if err := cmdutil.ValidateEnum( "order-direction", flagOrderDir, @@ -124,6 +125,7 @@ func NewCmdListVersions(f *cmdutil.Factory) *cobra.Command { ); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -138,6 +140,7 @@ func NewCmdListVersions(f *cmdutil.Factory) *cobra.Command { ); err != nil { return err } + variables["filter"] = map[string]any{ "statuses": []string{flagStatus}, } @@ -158,12 +161,15 @@ func NewCmdListVersions(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("document %s not found", args[0]) } + if resp.Node.Typename != "Document" { return nil, fmt.Errorf("expected Document node, got %s", resp.Node.Typename) } + return &resp.Node.Versions, nil }, ) diff --git a/pkg/cmd/document/list/list.go b/pkg/cmd/document/list/list.go index 7c269f765..5cf8e1119 100644 --- a/pkg/cmd/document/list/list.go +++ b/pkg/cmd/document/list/list.go @@ -132,6 +132,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { ); err != nil { return err } + if err := cmdutil.ValidateEnum( "order-direction", flagOrderDir, @@ -139,6 +140,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { ); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -149,6 +151,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if flagQuery != "" { filter["query"] = flagQuery } + if flagWriteMode != "" { if err := cmdutil.ValidateEnum( "write-mode", @@ -157,8 +160,10 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { ); err != nil { return err } + filter["writeModes"] = []string{flagWriteMode} } + if flagDocumentType != "" { if err := cmdutil.ValidateEnum( "document-type", @@ -167,8 +172,10 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { ); err != nil { return err } + filter["documentTypes"] = []string{flagDocumentType} } + if flagClassification != "" { if err := cmdutil.ValidateEnum( "classification", @@ -177,8 +184,10 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { ); err != nil { return err } + filter["classifications"] = []string{flagClassification} } + if flagStatus != "" { if err := cmdutil.ValidateEnum( "status", @@ -187,8 +196,10 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { ); err != nil { return err } + filter["status"] = []string{flagStatus} } + if len(filter) > 0 { variables["filter"] = filter } @@ -208,12 +219,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrg) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + return &resp.Node.Documents, nil }, ) @@ -235,12 +249,14 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { title := "" docType := "" classification := "" + if len(doc.Versions.Edges) > 0 { v := doc.Versions.Edges[0].Node title = v.Title docType = v.DocumentType classification = v.Classification } + rows = append(rows, []string{ doc.ID, title, diff --git a/pkg/cmd/document/publish/publish.go b/pkg/cmd/document/publish/publish.go index e6f8b198b..e667b2cad 100644 --- a/pkg/cmd/document/publish/publish.go +++ b/pkg/cmd/document/publish/publish.go @@ -130,6 +130,7 @@ instead of publishing immediately. Approvers are ignored with --minor.`, v.Minor, v.Status, ) + return nil } diff --git a/pkg/cmd/document/update/update.go b/pkg/cmd/document/update/update.go index 3a1d57a17..3b9016576 100644 --- a/pkg/cmd/document/update/update.go +++ b/pkg/cmd/document/update/update.go @@ -99,9 +99,11 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("title") { input["title"] = flagTitle } + if cmd.Flags().Changed("content") { input["content"] = flagContent } + if cmd.Flags().Changed("document-type") { if err := cmdutil.ValidateEnum( "document-type", @@ -110,8 +112,10 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { ); err != nil { return err } + input["documentType"] = flagDocumentType } + if cmd.Flags().Changed("classification") { if err := cmdutil.ValidateEnum( "classification", @@ -120,8 +124,10 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { ); err != nil { return err } + input["classification"] = flagClassification } + if cmd.Flags().Changed("trust-center-visibility") { if err := cmdutil.ValidateEnum( "trust-center-visibility", @@ -130,6 +136,7 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { ); err != nil { return err } + input["trustCenterVisibility"] = flagTrustCenterVisibility } diff --git a/pkg/cmd/document/view-approval-decision/view_approval_decision.go b/pkg/cmd/document/view-approval-decision/view_approval_decision.go index f277c6f5f..a548639cc 100644 --- a/pkg/cmd/document/view-approval-decision/view_approval_decision.go +++ b/pkg/cmd/document/view-approval-decision/view_approval_decision.go @@ -142,6 +142,7 @@ func NewCmdViewApprovalDecision(f *cmdutil.Factory) *cobra.Command { if d.DecidedAt != nil { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Decided:"), cmdutil.FormatTime(*d.DecidedAt)) } + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(d.CreatedAt)) _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(d.UpdatedAt)) diff --git a/pkg/cmd/document/view-version/view_version.go b/pkg/cmd/document/view-version/view_version.go index 78ba8f363..403c40691 100644 --- a/pkg/cmd/document/view-version/view_version.go +++ b/pkg/cmd/document/view-version/view_version.go @@ -141,6 +141,7 @@ func NewCmdViewVersion(f *cmdutil.Factory) *cobra.Command { if v.PublishedAt != nil { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Published:"), cmdutil.FormatTime(*v.PublishedAt)) } + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(v.CreatedAt)) _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(v.UpdatedAt)) diff --git a/pkg/cmd/dpia/create/create.go b/pkg/cmd/dpia/create/create.go index b1f8472b3..81331ff93 100644 --- a/pkg/cmd/dpia/create/create.go +++ b/pkg/cmd/dpia/create/create.go @@ -113,15 +113,19 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagDescription != "" { input["description"] = flagDescription } + if flagNecessityAndProportionality != "" { input["necessityAndProportionality"] = flagNecessityAndProportionality } + if flagPotentialRisk != "" { input["potentialRisk"] = flagPotentialRisk } + if flagMitigations != "" { input["mitigations"] = flagMitigations } + if flagResidualRisk != "" { input["residualRisk"] = flagResidualRisk } diff --git a/pkg/cmd/dpia/delete/delete.go b/pkg/cmd/dpia/delete/delete.go index c909ba6ea..b08ae29d8 100644 --- a/pkg/cmd/dpia/delete/delete.go +++ b/pkg/cmd/dpia/delete/delete.go @@ -45,6 +45,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete data protection impact assessment %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/dpia/list/list.go b/pkg/cmd/dpia/list/list.go index 4d3d0f549..3eaf04a5b 100644 --- a/pkg/cmd/dpia/list/list.go +++ b/pkg/cmd/dpia/list/list.go @@ -59,6 +59,7 @@ func truncate(s string, max int) string { if len(s) <= max { return s } + return s[:max-3] + "..." } @@ -115,6 +116,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -136,12 +138,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrg) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + return &resp.Node.DataProtectionImpactAssessments, nil }, ) diff --git a/pkg/cmd/dpia/publish/publish.go b/pkg/cmd/dpia/publish/publish.go index 9444a3a70..0e786c7e8 100644 --- a/pkg/cmd/dpia/publish/publish.go +++ b/pkg/cmd/dpia/publish/publish.go @@ -96,6 +96,7 @@ func NewCmdPublish(f *cmdutil.Factory) *cobra.Command { if flagOrg == "" { flagOrg = hc.Organization } + if flagOrg == "" { return fmt.Errorf("organization is required: pass --org or run `prb auth login`") } diff --git a/pkg/cmd/dpia/update/update.go b/pkg/cmd/dpia/update/update.go index 596544261..bc6fe7bb8 100644 --- a/pkg/cmd/dpia/update/update.go +++ b/pkg/cmd/dpia/update/update.go @@ -84,15 +84,19 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("description") { input["description"] = flagDescription } + if cmd.Flags().Changed("necessity") { input["necessityAndProportionality"] = flagNecessityAndProportionality } + if cmd.Flags().Changed("potential-risk") { input["potentialRisk"] = flagPotentialRisk } + if cmd.Flags().Changed("mitigations") { input["mitigations"] = flagMitigations } + if cmd.Flags().Changed("residual-risk") { input["residualRisk"] = flagResidualRisk } diff --git a/pkg/cmd/evidence/delete/delete.go b/pkg/cmd/evidence/delete/delete.go index 6360dc965..d758bac4c 100644 --- a/pkg/cmd/evidence/delete/delete.go +++ b/pkg/cmd/evidence/delete/delete.go @@ -50,11 +50,14 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if !f.IOStreams.IsInteractive() { return fmt.Errorf("cannot delete evidence: confirmation required, use --yes to confirm") } + var confirmed bool + err := huh.NewConfirm().Title(fmt.Sprintf("Delete evidence %s?", args[0])).Value(&confirmed).Run() if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/evidence/list/list.go b/pkg/cmd/evidence/list/list.go index 8c242fe7f..90a728422 100644 --- a/pkg/cmd/evidence/list/list.go +++ b/pkg/cmd/evidence/list/list.go @@ -109,6 +109,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -130,12 +131,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("measure %s not found", flagMeasure) } + if resp.Node.Typename != "Measure" { return nil, fmt.Errorf("expected Measure node, got %s", resp.Node.Typename) } + return &resp.Node.Evidences, nil }, ) @@ -155,13 +159,16 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { rows := make([][]string, 0, len(evidences)) for _, e := range evidences { desc := "-" + if e.Description != nil && *e.Description != "" { d := *e.Description if len(d) > 60 { d = d[:57] + "..." } + desc = d } + rows = append(rows, []string{ e.ID, e.Type, diff --git a/pkg/cmd/evidence/upload/upload.go b/pkg/cmd/evidence/upload/upload.go index f48295c8a..b535a29ac 100644 --- a/pkg/cmd/evidence/upload/upload.go +++ b/pkg/cmd/evidence/upload/upload.go @@ -63,6 +63,7 @@ func NewCmdUpload(f *cmdutil.Factory) *cobra.Command { if err != nil { return fmt.Errorf("cannot open file: %w", err) } + defer func() { _ = file.Close() }() cfg, err := f.Config() diff --git a/pkg/cmd/evidence/view/view.go b/pkg/cmd/evidence/view/view.go index a5335bdc6..579ddf416 100644 --- a/pkg/cmd/evidence/view/view.go +++ b/pkg/cmd/evidence/view/view.go @@ -148,12 +148,15 @@ func NewCmdView(f *cmdutil.Factory) *cobra.Command { if n.File != nil { _, _ = fmt.Fprintf(out, "%s%s (%s)\n", label.Render("File:"), n.File.Filename, n.File.ContentType) } + if n.URL != "" { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("URL:"), n.URL) } + if n.Task != nil { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Task:"), n.Task.ID) } + if n.Description != nil && *n.Description != "" { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Description:"), *n.Description) } diff --git a/pkg/cmd/finding/create/create.go b/pkg/cmd/finding/create/create.go index c1d318506..c06b188ab 100644 --- a/pkg/cmd/finding/create/create.go +++ b/pkg/cmd/finding/create/create.go @@ -80,9 +80,11 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("kind", flagKind, []string{"MINOR_NONCONFORMITY", "MAJOR_NONCONFORMITY", "OBSERVATION", "EXCEPTION"}); err != nil { return err } + if err := cmdutil.ValidateEnum("status", flagStatus, []string{"OPEN", "IN_PROGRESS", "CLOSED", "RISK_ACCEPTED", "MITIGATED", "FALSE_POSITIVE"}); err != nil { return err } + if err := cmdutil.ValidateEnum("priority", flagPriority, []string{"LOW", "MEDIUM", "HIGH"}); err != nil { return err } @@ -115,27 +117,35 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagOwnerID != "" { input["ownerId"] = flagOwnerID } + if flagDescription != "" { input["description"] = flagDescription } + if flagSource != "" { input["source"] = flagSource } + if flagIdentifiedOn != "" { input["identifiedOn"] = flagIdentifiedOn } + if flagRootCause != "" { input["rootCause"] = flagRootCause } + if flagCorrectiveAction != "" { input["correctiveAction"] = flagCorrectiveAction } + if flagDueDate != "" { input["dueDate"] = flagDueDate } + if flagRiskID != "" { input["riskId"] = flagRiskID } + if flagEffectivenessChk != "" { input["effectivenessCheck"] = flagEffectivenessChk } diff --git a/pkg/cmd/finding/delete/delete.go b/pkg/cmd/finding/delete/delete.go index 346bfdce2..83beeeb8b 100644 --- a/pkg/cmd/finding/delete/delete.go +++ b/pkg/cmd/finding/delete/delete.go @@ -45,6 +45,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete finding %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/finding/list/list.go b/pkg/cmd/finding/list/list.go index bd0889098..30d167c60 100644 --- a/pkg/cmd/finding/list/list.go +++ b/pkg/cmd/finding/list/list.go @@ -110,6 +110,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "REFERENCE_ID", "IDENTIFIED_ON", "DUE_DATE", "STATUS", "PRIORITY", "KIND"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -117,12 +118,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { } filter := map[string]any{} + if flagKind != "" { if err := cmdutil.ValidateEnum("kind", flagKind, []string{"MINOR_NONCONFORMITY", "MAJOR_NONCONFORMITY", "OBSERVATION", "EXCEPTION"}); err != nil { return err } + filter["kind"] = flagKind } + if len(filter) > 0 { variables["filter"] = filter } @@ -142,12 +146,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrganization) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + return &resp.Node.Findings, nil }, ) @@ -170,6 +177,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if fi.DueDate != nil { dueDate = cmdutil.FormatTime(*fi.DueDate) } + rows = append(rows, []string{ fi.ID, fi.ReferenceID, diff --git a/pkg/cmd/finding/publish/publish.go b/pkg/cmd/finding/publish/publish.go index 608a2eb90..a2e80b825 100644 --- a/pkg/cmd/finding/publish/publish.go +++ b/pkg/cmd/finding/publish/publish.go @@ -96,6 +96,7 @@ func NewCmdPublish(f *cmdutil.Factory) *cobra.Command { if flagOrg == "" { flagOrg = hc.Organization } + if flagOrg == "" { return fmt.Errorf("organization is required: pass --org or run `prb auth login`") } diff --git a/pkg/cmd/finding/update/update.go b/pkg/cmd/finding/update/update.go index 22ae2357e..21fb452bb 100644 --- a/pkg/cmd/finding/update/update.go +++ b/pkg/cmd/finding/update/update.go @@ -98,6 +98,7 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { input["description"] = flagDescription } } + if cmd.Flags().Changed("source") { if flagSource == "" { input["source"] = nil @@ -105,6 +106,7 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { input["source"] = flagSource } } + if cmd.Flags().Changed("identified-on") { if flagIdentifiedOn == "" { input["identifiedOn"] = nil @@ -112,6 +114,7 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { input["identifiedOn"] = flagIdentifiedOn } } + if cmd.Flags().Changed("root-cause") { if flagRootCause == "" { input["rootCause"] = nil @@ -119,6 +122,7 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { input["rootCause"] = flagRootCause } } + if cmd.Flags().Changed("corrective-action") { if flagCorrectiveAction == "" { input["correctiveAction"] = nil @@ -126,9 +130,11 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { input["correctiveAction"] = flagCorrectiveAction } } + if cmd.Flags().Changed("owner-id") { input["ownerId"] = flagOwnerID } + if cmd.Flags().Changed("due-date") { if flagDueDate == "" { input["dueDate"] = nil @@ -136,18 +142,23 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { input["dueDate"] = flagDueDate } } + if cmd.Flags().Changed("status") { if err := cmdutil.ValidateEnum("status", flagStatus, []string{"OPEN", "IN_PROGRESS", "CLOSED", "RISK_ACCEPTED", "MITIGATED", "FALSE_POSITIVE"}); err != nil { return err } + input["status"] = flagStatus } + if cmd.Flags().Changed("priority") { if err := cmdutil.ValidateEnum("priority", flagPriority, []string{"LOW", "MEDIUM", "HIGH"}); err != nil { return err } + input["priority"] = flagPriority } + if cmd.Flags().Changed("risk-id") { if flagRiskID == "" { input["riskId"] = nil @@ -155,6 +166,7 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { input["riskId"] = flagRiskID } } + if cmd.Flags().Changed("effectiveness-check") { if flagEffectivenessChk == "" { input["effectivenessCheck"] = nil diff --git a/pkg/cmd/finding/view/view.go b/pkg/cmd/finding/view/view.go index 73d06b5d1..946e9fd48 100644 --- a/pkg/cmd/finding/view/view.go +++ b/pkg/cmd/finding/view/view.go @@ -156,24 +156,31 @@ func NewCmdView(f *cmdutil.Factory) *cobra.Command { if n.Description != nil && *n.Description != "" { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Description:"), *n.Description) } + if n.Source != nil && *n.Source != "" { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Source:"), *n.Source) } + if n.IdentifiedOn != nil && *n.IdentifiedOn != "" { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Identified On:"), cmdutil.FormatTime(*n.IdentifiedOn)) } + if n.DueDate != nil && *n.DueDate != "" { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Due Date:"), cmdutil.FormatTime(*n.DueDate)) } + if n.RootCause != nil && *n.RootCause != "" { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Root Cause:"), *n.RootCause) } + if n.CorrectiveAction != nil && *n.CorrectiveAction != "" { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Corrective Action:"), *n.CorrectiveAction) } + if n.Risk != nil { _, _ = fmt.Fprintf(out, "%s%s (%s)\n", label.Render("Risk:"), n.Risk.Name, n.Risk.ID) } + if n.EffectivenessCheck != nil && *n.EffectivenessCheck != "" { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Effectiveness Check:"), *n.EffectivenessCheck) } diff --git a/pkg/cmd/framework/delete/delete.go b/pkg/cmd/framework/delete/delete.go index 661a67332..ba42446ac 100644 --- a/pkg/cmd/framework/delete/delete.go +++ b/pkg/cmd/framework/delete/delete.go @@ -45,6 +45,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete framework %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/framework/list/list.go b/pkg/cmd/framework/list/list.go index 95b0fe6e9..58f43410c 100644 --- a/pkg/cmd/framework/list/list.go +++ b/pkg/cmd/framework/list/list.go @@ -106,6 +106,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -127,12 +128,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrg) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + return &resp.Node.Frameworks, nil }, ) @@ -155,6 +159,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if fw.Description != nil { desc = *fw.Description } + rows = append(rows, []string{ fw.ID, fw.Name, diff --git a/pkg/cmd/framework/update/update.go b/pkg/cmd/framework/update/update.go index 9596eca6e..c909c867b 100644 --- a/pkg/cmd/framework/update/update.go +++ b/pkg/cmd/framework/update/update.go @@ -81,6 +81,7 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("name") { input["name"] = flagName } + if cmd.Flags().Changed("description") { if flagDescription == "" { input["description"] = nil diff --git a/pkg/cmd/iostreams/iostreams.go b/pkg/cmd/iostreams/iostreams.go index 7983dcf54..d0aaef7e2 100644 --- a/pkg/cmd/iostreams/iostreams.go +++ b/pkg/cmd/iostreams/iostreams.go @@ -42,6 +42,7 @@ func (s *IOStreams) IsInteractive() bool { if s.ForceNonInteractive { return false } + return s.isStdinTTY() && s.isStdoutTTY() } @@ -49,6 +50,7 @@ func (s *IOStreams) isStdinTTY() bool { if f, ok := s.In.(*os.File); ok { return term.IsTerminal(int(f.Fd())) } + return false } @@ -56,6 +58,7 @@ func (s *IOStreams) isStdoutTTY() bool { if f, ok := s.Out.(*os.File); ok { return term.IsTerminal(int(f.Fd())) } + return false } @@ -67,6 +70,7 @@ func (s *IOStreams) IsStdoutTTY() bool { if s.ForceNonInteractive { return false } + return s.isStdoutTTY() } @@ -74,6 +78,7 @@ func (s *IOStreams) ColorEnabled() bool { if s.ForceNoColor { return false } + return s.isStdoutTTY() } @@ -96,6 +101,7 @@ func System() *IOStreams { func Test() (*IOStreams, *bytes.Buffer, *bytes.Buffer) { out := new(bytes.Buffer) errOut := new(bytes.Buffer) + return &IOStreams{ In: io.NopCloser(new(bytes.Buffer)), Out: out, diff --git a/pkg/cmd/measure/create/create.go b/pkg/cmd/measure/create/create.go index 20ee94526..812dc903d 100644 --- a/pkg/cmd/measure/create/create.go +++ b/pkg/cmd/measure/create/create.go @@ -120,6 +120,7 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagName == "" { return fmt.Errorf("name is required; pass --name or run interactively") } + if flagCategory == "" { return fmt.Errorf("category is required; pass --category or run interactively") } diff --git a/pkg/cmd/measure/delete/delete.go b/pkg/cmd/measure/delete/delete.go index 5a63ae6b6..3c98d15b0 100644 --- a/pkg/cmd/measure/delete/delete.go +++ b/pkg/cmd/measure/delete/delete.go @@ -45,6 +45,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete measure %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/measure/list/list.go b/pkg/cmd/measure/list/list.go index 1dd17525f..44bd1a8d2 100644 --- a/pkg/cmd/measure/list/list.go +++ b/pkg/cmd/measure/list/list.go @@ -117,6 +117,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "NAME"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -144,12 +145,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrg) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + return &resp.Node.Measures, nil }, ) diff --git a/pkg/cmd/measure/update/update.go b/pkg/cmd/measure/update/update.go index 6256c99ae..d1a18a068 100644 --- a/pkg/cmd/measure/update/update.go +++ b/pkg/cmd/measure/update/update.go @@ -85,16 +85,20 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("name") { input["name"] = flagName } + if cmd.Flags().Changed("description") { input["description"] = flagDescription } + if cmd.Flags().Changed("category") { input["category"] = flagCategory } + if cmd.Flags().Changed("state") { if err := cmdutil.ValidateEnum("state", flagState, []string{"NOT_STARTED", "IN_PROGRESS", "NOT_APPLICABLE", "IMPLEMENTED"}); err != nil { return err } + input["state"] = flagState } diff --git a/pkg/cmd/obligation/create/create.go b/pkg/cmd/obligation/create/create.go index 1d546b8db..ac1b55cef 100644 --- a/pkg/cmd/obligation/create/create.go +++ b/pkg/cmd/obligation/create/create.go @@ -158,12 +158,15 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagArea == "" { return fmt.Errorf("area is required; pass --area or run interactively") } + if flagSource == "" { return fmt.Errorf("source is required; pass --source or run interactively") } + if flagStatus == "" { return fmt.Errorf("status is required; pass --status or run interactively") } + if flagType == "" { return fmt.Errorf("type is required; pass --type or run interactively") } @@ -179,18 +182,23 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagRequirement != "" { input["requirement"] = flagRequirement } + if flagActionsToBeImplemented != "" { input["actionsToBeImplemented"] = flagActionsToBeImplemented } + if flagRegulator != "" { input["regulator"] = flagRegulator } + if flagOwner != "" { input["ownerId"] = flagOwner } + if flagLastReviewDate != "" { input["lastReviewDate"] = flagLastReviewDate } + if flagDueDate != "" { input["dueDate"] = flagDueDate } diff --git a/pkg/cmd/obligation/delete/delete.go b/pkg/cmd/obligation/delete/delete.go index d16607a39..34824addb 100644 --- a/pkg/cmd/obligation/delete/delete.go +++ b/pkg/cmd/obligation/delete/delete.go @@ -45,6 +45,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete obligation %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/obligation/list/list.go b/pkg/cmd/obligation/list/list.go index a50f7f345..82f086d48 100644 --- a/pkg/cmd/obligation/list/list.go +++ b/pkg/cmd/obligation/list/list.go @@ -117,6 +117,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "LAST_REVIEW_DATE", "DUE_DATE", "STATUS"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -138,12 +139,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrg) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + return &resp.Node.Obligations, nil }, ) @@ -166,6 +170,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if o.DueDate != nil { dueDate = *o.DueDate } + rows = append(rows, []string{ o.ID, o.Area, diff --git a/pkg/cmd/obligation/publish/publish.go b/pkg/cmd/obligation/publish/publish.go index a774b6763..07d6da04c 100644 --- a/pkg/cmd/obligation/publish/publish.go +++ b/pkg/cmd/obligation/publish/publish.go @@ -96,6 +96,7 @@ func NewCmdPublish(f *cmdutil.Factory) *cobra.Command { if flagOrg == "" { flagOrg = hc.Organization } + if flagOrg == "" { return fmt.Errorf("organization is required: pass --org or run `prb auth login`") } diff --git a/pkg/cmd/obligation/update/update.go b/pkg/cmd/obligation/update/update.go index e984257a1..e4f9d0b76 100644 --- a/pkg/cmd/obligation/update/update.go +++ b/pkg/cmd/obligation/update/update.go @@ -93,24 +93,31 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("area") { input["area"] = flagArea } + if cmd.Flags().Changed("source") { input["source"] = flagSource } + if cmd.Flags().Changed("status") { input["status"] = flagStatus } + if cmd.Flags().Changed("type") { input["type"] = flagType } + if cmd.Flags().Changed("requirement") { input["requirement"] = flagRequirement } + if cmd.Flags().Changed("actions-to-be-implemented") { input["actionsToBeImplemented"] = flagActionsToBeImplemented } + if cmd.Flags().Changed("regulator") { input["regulator"] = flagRegulator } + if cmd.Flags().Changed("owner") { if flagOwner == "" { input["ownerId"] = nil @@ -118,9 +125,11 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { input["ownerId"] = flagOwner } } + if cmd.Flags().Changed("last-review-date") { input["lastReviewDate"] = flagLastReviewDate } + if cmd.Flags().Changed("due-date") { input["dueDate"] = flagDueDate } diff --git a/pkg/cmd/org/list/list.go b/pkg/cmd/org/list/list.go index 12d5729a4..647f5602f 100644 --- a/pkg/cmd/org/list/list.go +++ b/pkg/cmd/org/list/list.go @@ -127,6 +127,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + return &resp.Viewer.Profiles, nil }, ) @@ -147,6 +148,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { for _, p := range profiles { orgID := "" orgName := "" + if p.Organization != nil { orgID = p.Organization.ID orgName = p.Organization.Name diff --git a/pkg/cmd/processing-activity/create/create.go b/pkg/cmd/processing-activity/create/create.go index 478b44702..87904521f 100644 --- a/pkg/cmd/processing-activity/create/create.go +++ b/pkg/cmd/processing-activity/create/create.go @@ -154,15 +154,19 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagPurpose != "" { input["purpose"] = flagPurpose } + if flagRole != "" { input["role"] = flagRole } + if flagLawfulBasis != "" { input["lawfulBasis"] = flagLawfulBasis } + if flagDataSubjectCategory != "" { input["dataSubjectCategory"] = flagDataSubjectCategory } + if flagPersonalDataCategory != "" { input["personalDataCategory"] = flagPersonalDataCategory } diff --git a/pkg/cmd/processing-activity/delete/delete.go b/pkg/cmd/processing-activity/delete/delete.go index cf320f94d..d9b814080 100644 --- a/pkg/cmd/processing-activity/delete/delete.go +++ b/pkg/cmd/processing-activity/delete/delete.go @@ -45,6 +45,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete processing activity %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/processing-activity/list/list.go b/pkg/cmd/processing-activity/list/list.go index 06587b60f..2d41409a2 100644 --- a/pkg/cmd/processing-activity/list/list.go +++ b/pkg/cmd/processing-activity/list/list.go @@ -113,6 +113,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "NAME"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -134,12 +135,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrg) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + return &resp.Node.ProcessingActivities, nil }, ) diff --git a/pkg/cmd/processing-activity/publish/publish.go b/pkg/cmd/processing-activity/publish/publish.go index 5446c7fd1..c30f8a3b9 100644 --- a/pkg/cmd/processing-activity/publish/publish.go +++ b/pkg/cmd/processing-activity/publish/publish.go @@ -96,6 +96,7 @@ func NewCmdPublish(f *cmdutil.Factory) *cobra.Command { if flagOrg == "" { flagOrg = hc.Organization } + if flagOrg == "" { return fmt.Errorf("organization is required: pass --org or run `prb auth login`") } diff --git a/pkg/cmd/processing-activity/update/update.go b/pkg/cmd/processing-activity/update/update.go index 6165e3f4a..0da77069f 100644 --- a/pkg/cmd/processing-activity/update/update.go +++ b/pkg/cmd/processing-activity/update/update.go @@ -85,12 +85,15 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("name") { input["name"] = flagName } + if cmd.Flags().Changed("purpose") { input["purpose"] = flagPurpose } + if cmd.Flags().Changed("role") { input["role"] = flagRole } + if cmd.Flags().Changed("lawful-basis") { input["lawfulBasis"] = flagLawfulBasis } diff --git a/pkg/cmd/rights-request/create/create.go b/pkg/cmd/rights-request/create/create.go index a66826b24..60a3ec998 100644 --- a/pkg/cmd/rights-request/create/create.go +++ b/pkg/cmd/rights-request/create/create.go @@ -144,9 +144,11 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagDataSubject == "" { return fmt.Errorf("data subject is required; pass --data-subject or run interactively") } + if flagType == "" { return fmt.Errorf("request type is required; pass --type or run interactively") } + if flagState == "" { return fmt.Errorf("request state is required; pass --state or run interactively") } @@ -161,12 +163,15 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagContact != "" { input["contact"] = flagContact } + if flagDetails != "" { input["details"] = flagDetails } + if flagDeadline != "" { input["deadline"] = flagDeadline } + if flagActionTaken != "" { input["actionTaken"] = flagActionTaken } diff --git a/pkg/cmd/rights-request/delete/delete.go b/pkg/cmd/rights-request/delete/delete.go index f31df7cbe..65023add9 100644 --- a/pkg/cmd/rights-request/delete/delete.go +++ b/pkg/cmd/rights-request/delete/delete.go @@ -45,6 +45,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete rights request %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/rights-request/list/list.go b/pkg/cmd/rights-request/list/list.go index 445f583fa..5e7154b04 100644 --- a/pkg/cmd/rights-request/list/list.go +++ b/pkg/cmd/rights-request/list/list.go @@ -115,6 +115,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "DEADLINE", "STATE", "TYPE"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -136,12 +137,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrg) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + return &resp.Node.RightsRequests, nil }, ) @@ -164,6 +168,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if r.Deadline != nil { deadline = *r.Deadline } + rows = append(rows, []string{ r.ID, r.DataSubject, diff --git a/pkg/cmd/rights-request/update/update.go b/pkg/cmd/rights-request/update/update.go index 54b4e17b2..ccb9b1ba0 100644 --- a/pkg/cmd/rights-request/update/update.go +++ b/pkg/cmd/rights-request/update/update.go @@ -88,21 +88,27 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("type") { input["requestType"] = flagType } + if cmd.Flags().Changed("state") { input["requestState"] = flagState } + if cmd.Flags().Changed("data-subject") { input["dataSubject"] = flagDataSubject } + if cmd.Flags().Changed("contact") { input["contact"] = flagContact } + if cmd.Flags().Changed("details") { input["details"] = flagDetails } + if cmd.Flags().Changed("deadline") { input["deadline"] = flagDeadline } + if cmd.Flags().Changed("action-taken") { input["actionTaken"] = flagActionTaken } diff --git a/pkg/cmd/risk/create/create.go b/pkg/cmd/risk/create/create.go index dec4b367e..2d6f7ab36 100644 --- a/pkg/cmd/risk/create/create.go +++ b/pkg/cmd/risk/create/create.go @@ -155,9 +155,11 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagName == "" { return fmt.Errorf("name is required; pass --name or run interactively") } + if flagCategory == "" { return fmt.Errorf("category is required; pass --category or run interactively") } + if flagTreatment == "" { return fmt.Errorf("treatment is required; pass --treatment or run interactively") } @@ -174,15 +176,19 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagDescription != "" { input["description"] = flagDescription } + if flagNote != "" { input["note"] = flagNote } + if flagOwner != "" { input["ownerId"] = flagOwner } + if cmd.Flags().Changed("residual-likelihood") { input["residualLikelihood"] = flagResidualLikelihood } + if cmd.Flags().Changed("residual-impact") { input["residualImpact"] = flagResidualImpact } diff --git a/pkg/cmd/risk/delete/delete.go b/pkg/cmd/risk/delete/delete.go index 3588005ad..b586f0cfc 100644 --- a/pkg/cmd/risk/delete/delete.go +++ b/pkg/cmd/risk/delete/delete.go @@ -45,6 +45,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete risk %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/risk/list/list.go b/pkg/cmd/risk/list/list.go index d11f95a9c..a070adc85 100644 --- a/pkg/cmd/risk/list/list.go +++ b/pkg/cmd/risk/list/list.go @@ -121,6 +121,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "NAME", "CATEGORY", "TREATMENT", "INHERENT_RISK_SCORE", "RESIDUAL_RISK_SCORE"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -148,12 +149,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrg) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + return &resp.Node.Risks, nil }, ) diff --git a/pkg/cmd/risk/publish/publish.go b/pkg/cmd/risk/publish/publish.go index a2ea178fa..43781acf4 100644 --- a/pkg/cmd/risk/publish/publish.go +++ b/pkg/cmd/risk/publish/publish.go @@ -96,6 +96,7 @@ func NewCmdPublish(f *cmdutil.Factory) *cobra.Command { if flagOrg == "" { flagOrg = hc.Organization } + if flagOrg == "" { return fmt.Errorf("organization is required: pass --org or run `prb auth login`") } diff --git a/pkg/cmd/risk/update/update.go b/pkg/cmd/risk/update/update.go index 5db389691..50a30994f 100644 --- a/pkg/cmd/risk/update/update.go +++ b/pkg/cmd/risk/update/update.go @@ -95,30 +95,39 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("name") { input["name"] = flagName } + if cmd.Flags().Changed("category") { input["category"] = flagCategory } + if cmd.Flags().Changed("treatment") { input["treatment"] = flagTreatment } + if cmd.Flags().Changed("inherent-likelihood") { input["inherentLikelihood"] = flagInherentLikelihood } + if cmd.Flags().Changed("inherent-impact") { input["inherentImpact"] = flagInherentImpact } + if cmd.Flags().Changed("residual-likelihood") { input["residualLikelihood"] = flagResidualLikelihood } + if cmd.Flags().Changed("residual-impact") { input["residualImpact"] = flagResidualImpact } + if cmd.Flags().Changed("description") { input["description"] = flagDescription } + if cmd.Flags().Changed("note") { input["note"] = flagNote } + if cmd.Flags().Changed("owner") { if flagOwner == "" { input["ownerId"] = nil diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index ed5fefd6d..06919ff0a 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -67,9 +67,11 @@ func NewCmdRoot(f *cmdutil.Factory) *cobra.Command { if noInteractive, _ := cmd.Flags().GetBool("no-interactive"); noInteractive { f.IOStreams.ForceNonInteractive = true } + if noColor, _ := cmd.Flags().GetBool("no-color"); noColor { f.IOStreams.ForceNoColor = true } + f.IOStreams.ApplyColorProfile() }, } diff --git a/pkg/cmd/scim/delete/delete.go b/pkg/cmd/scim/delete/delete.go index 0745e795c..4e0bbb0ea 100644 --- a/pkg/cmd/scim/delete/delete.go +++ b/pkg/cmd/scim/delete/delete.go @@ -48,6 +48,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete SCIM configuration %s? This will also remove the associated bridge.", args[0])). Value(&confirmed). @@ -55,6 +56,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/scim/event/list/list.go b/pkg/cmd/scim/event/list/list.go index a62e966d1..f7b5b3988 100644 --- a/pkg/cmd/scim/event/list/list.go +++ b/pkg/cmd/scim/event/list/list.go @@ -125,12 +125,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("SCIM configuration %s not found", args[0]) } + if resp.Node.Typename != "SCIMConfiguration" { return nil, fmt.Errorf("expected SCIMConfiguration node, got %s", resp.Node.Typename) } + return &resp.Node.Events, nil }, ) diff --git a/pkg/cmd/scim/regenerate-token/regenerate_token.go b/pkg/cmd/scim/regenerate-token/regenerate_token.go index 9d85943c2..869ebde3d 100644 --- a/pkg/cmd/scim/regenerate-token/regenerate_token.go +++ b/pkg/cmd/scim/regenerate-token/regenerate_token.go @@ -61,6 +61,7 @@ func NewCmdRegenerateToken(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title("Regenerate SCIM token? The current token will be invalidated."). Value(&confirmed). @@ -68,6 +69,7 @@ func NewCmdRegenerateToken(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/scim/view/view.go b/pkg/cmd/scim/view/view.go index a908a5f34..4a1b28297 100644 --- a/pkg/cmd/scim/view/view.go +++ b/pkg/cmd/scim/view/view.go @@ -152,7 +152,9 @@ func NewCmdView(f *cmdutil.Factory) *cobra.Command { if *flagOutput == cmdutil.OutputJSON { return cmdutil.PrintJSON(f.IOStreams.Out, nil) } + _, _ = fmt.Fprintln(f.IOStreams.Out, "No SCIM configuration found.") + return nil } diff --git a/pkg/cmd/soa/delete/delete.go b/pkg/cmd/soa/delete/delete.go index 530df1984..2ce042625 100644 --- a/pkg/cmd/soa/delete/delete.go +++ b/pkg/cmd/soa/delete/delete.go @@ -45,6 +45,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete statement of applicability %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/soa/list/list.go b/pkg/cmd/soa/list/list.go index e6d31bba0..a3ececd53 100644 --- a/pkg/cmd/soa/list/list.go +++ b/pkg/cmd/soa/list/list.go @@ -108,6 +108,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"NAME", "CREATED_AT"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -129,12 +130,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrg) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + return &resp.Node.StatementsOfApplicability, nil }, ) @@ -146,6 +150,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if soas == nil { soas = []statementOfApplicability{} } + return cmdutil.PrintJSON(f.IOStreams.Out, soas) } diff --git a/pkg/cmd/soa/statement/add/add.go b/pkg/cmd/soa/statement/add/add.go index 6bde9b4b8..c892e0e25 100644 --- a/pkg/cmd/soa/statement/add/add.go +++ b/pkg/cmd/soa/statement/add/add.go @@ -82,12 +82,14 @@ func NewCmdAdd(f *cmdutil.Factory) *cobra.Command { if flagApplicable && flagNotApplicable { return fmt.Errorf("cannot set both --applicable and --not-applicable") } + if !flagApplicable && !flagNotApplicable { if !f.IOStreams.IsInteractive() { return fmt.Errorf("either --applicable or --not-applicable is required") } var choice string + err := huh.NewSelect[string](). Title("Is this control applicable?"). Options( @@ -99,6 +101,7 @@ func NewCmdAdd(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + flagApplicable = choice == "applicable" } @@ -154,10 +157,12 @@ func NewCmdAdd(f *cmdutil.Factory) *cobra.Command { } s := resp.CreateApplicabilityStatement.ApplicabilityStatementEdge.Node + applicable := "not applicable" if s.Applicability { applicable = "applicable" } + _, _ = fmt.Fprintf( f.IOStreams.Out, "Added statement %s: control %s (%s) marked as %s\n", diff --git a/pkg/cmd/soa/statement/list/list.go b/pkg/cmd/soa/statement/list/list.go index e44ef03a1..064d7dbcf 100644 --- a/pkg/cmd/soa/statement/list/list.go +++ b/pkg/cmd/soa/statement/list/list.go @@ -107,6 +107,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "CONTROL_SECTION_TITLE"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -128,12 +129,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("statement of applicability %s not found", args[0]) } + if resp.Node.Typename != "StatementOfApplicability" { return nil, fmt.Errorf("expected StatementOfApplicability node, got %s", resp.Node.Typename) } + return &resp.Node.ApplicabilityStatements, nil }, ) @@ -156,6 +160,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if s.Applicability { applicable = "Yes" } + rows = append(rows, []string{ s.ID, s.Control.SectionTitle, diff --git a/pkg/cmd/soa/statement/remove/remove.go b/pkg/cmd/soa/statement/remove/remove.go index 0c105a2ca..b6dd1e10f 100644 --- a/pkg/cmd/soa/statement/remove/remove.go +++ b/pkg/cmd/soa/statement/remove/remove.go @@ -45,6 +45,7 @@ func NewCmdRemove(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Remove applicability statement %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdRemove(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/soa/statement/update/update.go b/pkg/cmd/soa/statement/update/update.go index 4221c8ea1..fedcd36d6 100644 --- a/pkg/cmd/soa/statement/update/update.go +++ b/pkg/cmd/soa/statement/update/update.go @@ -70,6 +70,7 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if flagApplicable && flagNotApplicable { return fmt.Errorf("cannot set both --applicable and --not-applicable") } + if !flagApplicable && !flagNotApplicable && !cmd.Flags().Changed("justification") { return fmt.Errorf("at least one of --applicable, --not-applicable, or --justification is required") } @@ -118,10 +119,12 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { } s := resp.UpdateApplicabilityStatement.ApplicabilityStatement + applicable := "not applicable" if s.Applicability { applicable = "applicable" } + _, _ = fmt.Fprintf( f.IOStreams.Out, "Updated statement %s: control %s (%s) marked as %s\n", diff --git a/pkg/cmd/soa/update/update.go b/pkg/cmd/soa/update/update.go index d4b943bda..fd1ae0c87 100644 --- a/pkg/cmd/soa/update/update.go +++ b/pkg/cmd/soa/update/update.go @@ -79,6 +79,7 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("name") { input["name"] = flagName } + if cmd.Flags().Changed("owner") { if flagOwner == "" { input["ownerId"] = nil diff --git a/pkg/cmd/task/create/create.go b/pkg/cmd/task/create/create.go index 61749039b..a1b8f511d 100644 --- a/pkg/cmd/task/create/create.go +++ b/pkg/cmd/task/create/create.go @@ -139,18 +139,23 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagDescription != "" { input["description"] = flagDescription } + if flagPriority != "" { input["priority"] = flagPriority } + if flagMeasure != "" { input["measureId"] = flagMeasure } + if flagTimeEstimate != "" { input["timeEstimate"] = flagTimeEstimate } + if flagAssignedTo != "" { input["assignedToId"] = flagAssignedTo } + if flagDeadline != "" { input["deadline"] = flagDeadline } diff --git a/pkg/cmd/task/delete/delete.go b/pkg/cmd/task/delete/delete.go index 7300457db..e3a64d476 100644 --- a/pkg/cmd/task/delete/delete.go +++ b/pkg/cmd/task/delete/delete.go @@ -45,6 +45,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete task %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/task/list/list.go b/pkg/cmd/task/list/list.go index de7c81d46..8beb33f3c 100644 --- a/pkg/cmd/task/list/list.go +++ b/pkg/cmd/task/list/list.go @@ -115,6 +115,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"PRIORITY_RANK", "CREATED_AT"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -136,12 +137,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrg) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + return &resp.Node.Tasks, nil }, ) @@ -164,6 +168,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if t.Deadline != nil { deadline = *t.Deadline } + rows = append(rows, []string{ t.ID, t.Name, diff --git a/pkg/cmd/task/update/update.go b/pkg/cmd/task/update/update.go index c3f2d773b..1424d5038 100644 --- a/pkg/cmd/task/update/update.go +++ b/pkg/cmd/task/update/update.go @@ -89,21 +89,27 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("name") { input["name"] = flagName } + if cmd.Flags().Changed("description") { input["description"] = flagDescription } + if cmd.Flags().Changed("state") { input["state"] = flagState } + if cmd.Flags().Changed("priority") { input["priority"] = flagPriority } + if cmd.Flags().Changed("time-estimate") { input["timeEstimate"] = flagTimeEstimate } + if cmd.Flags().Changed("deadline") { input["deadline"] = flagDeadline } + if cmd.Flags().Changed("assigned-to") { if flagAssignedTo == "" { input["assignedToId"] = nil @@ -111,6 +117,7 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { input["assignedToId"] = flagAssignedTo } } + if cmd.Flags().Changed("measure") { if flagMeasure == "" { input["measureId"] = nil diff --git a/pkg/cmd/thirdpartymgmt/assess/assess.go b/pkg/cmd/thirdpartymgmt/assess/assess.go index cbbff4d9e..1565a5887 100644 --- a/pkg/cmd/thirdpartymgmt/assess/assess.go +++ b/pkg/cmd/thirdpartymgmt/assess/assess.go @@ -103,6 +103,7 @@ func NewCmdAssess(f *cmdutil.Factory) *cobra.Command { if err != nil { return fmt.Errorf("cannot read procedure file: %w", err) } + input["procedure"] = string(data) } diff --git a/pkg/cmd/thirdpartymgmt/create/create.go b/pkg/cmd/thirdpartymgmt/create/create.go index 39b4bc311..cc5bd2dac 100644 --- a/pkg/cmd/thirdpartymgmt/create/create.go +++ b/pkg/cmd/thirdpartymgmt/create/create.go @@ -145,6 +145,7 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagName == "" { return fmt.Errorf("name is required; pass --name or run interactively") } + if flagCategory == "" { return fmt.Errorf("category is required; pass --category or run interactively") } @@ -158,12 +159,15 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagDescription != "" { input["description"] = flagDescription } + if flagLegalName != "" { input["legalName"] = flagLegalName } + if flagAddress != "" { input["headquarterAddress"] = flagAddress } + if flagWebsite != "" { input["websiteUrl"] = flagWebsite } diff --git a/pkg/cmd/thirdpartymgmt/delete/delete.go b/pkg/cmd/thirdpartymgmt/delete/delete.go index 3c37147dd..ec963a353 100644 --- a/pkg/cmd/thirdpartymgmt/delete/delete.go +++ b/pkg/cmd/thirdpartymgmt/delete/delete.go @@ -45,6 +45,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete thirdParty %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/thirdpartymgmt/list/list.go b/pkg/cmd/thirdpartymgmt/list/list.go index b6347ef3c..1ff1ee4a0 100644 --- a/pkg/cmd/thirdpartymgmt/list/list.go +++ b/pkg/cmd/thirdpartymgmt/list/list.go @@ -111,6 +111,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"NAME", "CREATED_AT", "UPDATED_AT"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -132,12 +133,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrg) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + return &resp.Node.ThirdParties, nil }, ) diff --git a/pkg/cmd/thirdpartymgmt/publish/publish.go b/pkg/cmd/thirdpartymgmt/publish/publish.go index 43d249634..e4a362cbb 100644 --- a/pkg/cmd/thirdpartymgmt/publish/publish.go +++ b/pkg/cmd/thirdpartymgmt/publish/publish.go @@ -96,6 +96,7 @@ func NewCmdPublish(f *cmdutil.Factory) *cobra.Command { if flagOrg == "" { flagOrg = hc.Organization } + if flagOrg == "" { return fmt.Errorf("organization is required: pass --org or run `prb auth login`") } diff --git a/pkg/cmd/thirdpartymgmt/update/update.go b/pkg/cmd/thirdpartymgmt/update/update.go index e08459d4b..32d5ce0d8 100644 --- a/pkg/cmd/thirdpartymgmt/update/update.go +++ b/pkg/cmd/thirdpartymgmt/update/update.go @@ -85,18 +85,23 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("name") { input["name"] = flagName } + if cmd.Flags().Changed("description") { input["description"] = flagDescription } + if cmd.Flags().Changed("category") { input["category"] = flagCategory } + if cmd.Flags().Changed("legal-name") { input["legalName"] = flagLegalName } + if cmd.Flags().Changed("address") { input["headquarterAddress"] = flagAddress } + if cmd.Flags().Changed("website") { input["websiteUrl"] = flagWebsite } diff --git a/pkg/cmd/tia/create/create.go b/pkg/cmd/tia/create/create.go index be2487f85..2579c8c04 100644 --- a/pkg/cmd/tia/create/create.go +++ b/pkg/cmd/tia/create/create.go @@ -97,15 +97,19 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagDataSubjects != "" { input["dataSubjects"] = flagDataSubjects } + if flagLegalMechanism != "" { input["legalMechanism"] = flagLegalMechanism } + if flagTransfer != "" { input["transfer"] = flagTransfer } + if flagLocalLawRisk != "" { input["localLawRisk"] = flagLocalLawRisk } + if flagSupplementaryMeasures != "" { input["supplementaryMeasures"] = flagSupplementaryMeasures } diff --git a/pkg/cmd/tia/delete/delete.go b/pkg/cmd/tia/delete/delete.go index c34fb7f62..1f8a59219 100644 --- a/pkg/cmd/tia/delete/delete.go +++ b/pkg/cmd/tia/delete/delete.go @@ -45,6 +45,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete transfer impact assessment %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/tia/list/list.go b/pkg/cmd/tia/list/list.go index 8b7da6050..c8b6c0a33 100644 --- a/pkg/cmd/tia/list/list.go +++ b/pkg/cmd/tia/list/list.go @@ -59,6 +59,7 @@ func truncate(s string, max int) string { if len(s) <= max { return s } + return s[:max-3] + "..." } @@ -115,6 +116,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -136,12 +138,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrg) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + return &resp.Node.TransferImpactAssessments, nil }, ) diff --git a/pkg/cmd/tia/publish/publish.go b/pkg/cmd/tia/publish/publish.go index ed3c804de..843a711c8 100644 --- a/pkg/cmd/tia/publish/publish.go +++ b/pkg/cmd/tia/publish/publish.go @@ -96,6 +96,7 @@ func NewCmdPublish(f *cmdutil.Factory) *cobra.Command { if flagOrg == "" { flagOrg = hc.Organization } + if flagOrg == "" { return fmt.Errorf("organization is required: pass --org or run `prb auth login`") } diff --git a/pkg/cmd/tia/update/update.go b/pkg/cmd/tia/update/update.go index 5d617a002..17056f165 100644 --- a/pkg/cmd/tia/update/update.go +++ b/pkg/cmd/tia/update/update.go @@ -84,15 +84,19 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("data-subjects") { input["dataSubjects"] = flagDataSubjects } + if cmd.Flags().Changed("legal-mechanism") { input["legalMechanism"] = flagLegalMechanism } + if cmd.Flags().Changed("transfer") { input["transfer"] = flagTransfer } + if cmd.Flags().Changed("local-law-risk") { input["localLawRisk"] = flagLocalLawRisk } + if cmd.Flags().Changed("supplementary-measures") { input["supplementaryMeasures"] = flagSupplementaryMeasures } diff --git a/pkg/cmd/tracker-pattern/create/create.go b/pkg/cmd/tracker-pattern/create/create.go index cf93d8ed3..cd2a51726 100644 --- a/pkg/cmd/tracker-pattern/create/create.go +++ b/pkg/cmd/tracker-pattern/create/create.go @@ -91,6 +91,7 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { return err } } + if flagMatchType == "" { if err := huh.NewSelect[string](). Title("Match type"). @@ -102,6 +103,7 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { return err } } + if flagDisplayName == "" { if err := huh.NewInput().Title("Display name").Value(&flagDisplayName).Run(); err != nil { return err @@ -112,9 +114,11 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagPattern == "" { return fmt.Errorf("pattern is required; pass --pattern or run interactively") } + if flagMatchType == "" { return fmt.Errorf("match-type is required; pass --match-type or run interactively") } + if flagDisplayName == "" { return fmt.Errorf("display-name is required; pass --display-name or run interactively") } @@ -128,6 +132,7 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagDescription != "" { input["description"] = flagDescription } + if cmd.Flags().Changed("max-age-seconds") { input["maxAgeSeconds"] = flagMaxAge } diff --git a/pkg/cmd/tracker-pattern/delete/delete.go b/pkg/cmd/tracker-pattern/delete/delete.go index d55e4ce7e..d2ef073e6 100644 --- a/pkg/cmd/tracker-pattern/delete/delete.go +++ b/pkg/cmd/tracker-pattern/delete/delete.go @@ -46,10 +46,12 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if !f.IOStreams.IsInteractive() { return fmt.Errorf("cannot delete tracker pattern: confirmation required, use --yes to confirm") } + var confirmed bool if err := huh.NewConfirm().Title(fmt.Sprintf("Delete tracker pattern %s?", args[0])).Value(&confirmed).Run(); err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/tracker-pattern/list/list.go b/pkg/cmd/tracker-pattern/list/list.go index c7e4bf48d..53ec5dc67 100644 --- a/pkg/cmd/tracker-pattern/list/list.go +++ b/pkg/cmd/tracker-pattern/list/list.go @@ -115,12 +115,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("cookie category %s not found", flagCategoryID) } + if resp.Node.Typename != "CookieCategory" { return nil, fmt.Errorf("expected CookieCategory node, got %s", resp.Node.Typename) } + return &resp.Node.TrackerPatterns, nil }, ) @@ -143,14 +146,17 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if p.Excluded { excluded = "yes" } + source := "" if p.Source != nil { source = *p.Source } + lastMatched := "" if p.LastMatchedAt != nil { lastMatched = cmdutil.FormatTime(*p.LastMatchedAt) } + rows = append(rows, []string{p.ID, p.Pattern, p.MatchType, p.TrackerType, p.DisplayName, source, excluded, lastMatched}) } diff --git a/pkg/cmd/tracker-pattern/update/update.go b/pkg/cmd/tracker-pattern/update/update.go index bc5a867be..30e772737 100644 --- a/pkg/cmd/tracker-pattern/update/update.go +++ b/pkg/cmd/tracker-pattern/update/update.go @@ -81,9 +81,11 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("description") { input["description"] = flagDescription } + if cmd.Flags().Changed("max-age-seconds") { input["maxAgeSeconds"] = flagMaxAge } + if cmd.Flags().Changed("excluded") { input["excluded"] = flagExcluded } diff --git a/pkg/cmd/tracker-pattern/view/view.go b/pkg/cmd/tracker-pattern/view/view.go index f58e7f07e..41c39524c 100644 --- a/pkg/cmd/tracker-pattern/view/view.go +++ b/pkg/cmd/tracker-pattern/view/view.go @@ -124,16 +124,20 @@ func NewCmdView(f *cmdutil.Factory) *cobra.Command { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Match Type:"), v.MatchType) _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Tracker Type:"), v.TrackerType) _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Source:"), v.Source) + _, _ = fmt.Fprintf(out, "%s%v\n", label.Render("Excluded:"), v.Excluded) if v.MaxAgeSeconds != nil { _, _ = fmt.Fprintf(out, "%s%d\n", label.Render("Max Age (seconds):"), *v.MaxAgeSeconds) } + if v.Description != nil && *v.Description != "" { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Description:"), *v.Description) } + if v.LastMatchedAt != nil { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Last Matched:"), cmdutil.FormatTime(*v.LastMatchedAt)) } + _, _ = fmt.Fprintln(out) _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(v.CreatedAt)) _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(v.UpdatedAt)) diff --git a/pkg/cmd/tracker-resource/create/create.go b/pkg/cmd/tracker-resource/create/create.go index 838da54c4..e2033b84d 100644 --- a/pkg/cmd/tracker-resource/create/create.go +++ b/pkg/cmd/tracker-resource/create/create.go @@ -102,16 +102,19 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { return err } } + if flagOrigin == "" { if err := huh.NewInput().Title("Origin").Value(&flagOrigin).Run(); err != nil { return err } } + if flagPath == "" { if err := huh.NewInput().Title("Path").Value(&flagPath).Run(); err != nil { return err } } + if flagDisplayName == "" { if err := huh.NewInput().Title("Display name").Value(&flagDisplayName).Run(); err != nil { return err @@ -122,12 +125,15 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagResourceType == "" { return fmt.Errorf("resource-type is required; pass --resource-type or run interactively") } + if flagOrigin == "" { return fmt.Errorf("origin is required; pass --origin or run interactively") } + if flagPath == "" { return fmt.Errorf("path is required; pass --path or run interactively") } + if flagDisplayName == "" { return fmt.Errorf("display-name is required; pass --display-name or run interactively") } diff --git a/pkg/cmd/tracker-resource/delete/delete.go b/pkg/cmd/tracker-resource/delete/delete.go index aec453d17..07a7f2fb6 100644 --- a/pkg/cmd/tracker-resource/delete/delete.go +++ b/pkg/cmd/tracker-resource/delete/delete.go @@ -46,10 +46,12 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if !f.IOStreams.IsInteractive() { return fmt.Errorf("cannot delete tracker resource: confirmation required, use --yes to confirm") } + var confirmed bool if err := huh.NewConfirm().Title(fmt.Sprintf("Delete tracker resource %s?", args[0])).Value(&confirmed).Run(); err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/tracker-resource/list/list.go b/pkg/cmd/tracker-resource/list/list.go index e84f09c78..ddaa5b8c8 100644 --- a/pkg/cmd/tracker-resource/list/list.go +++ b/pkg/cmd/tracker-resource/list/list.go @@ -113,12 +113,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("cookie category %s not found", flagCategoryID) } + if resp.Node.Typename != "CookieCategory" { return nil, fmt.Errorf("expected CookieCategory node, got %s", resp.Node.Typename) } + return &resp.Node.TrackerResources, nil }, ) @@ -141,10 +144,12 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if r.Excluded { excluded = "yes" } + lastDetected := "" if r.LastDetectedAt != nil { lastDetected = cmdutil.FormatTime(*r.LastDetectedAt) } + rows = append(rows, []string{r.ID, r.Type, r.Origin, r.Path, r.DisplayName, excluded, lastDetected}) } diff --git a/pkg/cmd/tracker-resource/update/update.go b/pkg/cmd/tracker-resource/update/update.go index ea9bce0ad..de7acbc3c 100644 --- a/pkg/cmd/tracker-resource/update/update.go +++ b/pkg/cmd/tracker-resource/update/update.go @@ -81,9 +81,11 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("display-name") { input["displayName"] = flagDisplayName } + if cmd.Flags().Changed("description") { input["description"] = flagDescription } + if cmd.Flags().Changed("excluded") { input["excluded"] = flagExcluded } diff --git a/pkg/cmd/tracker-resource/view/view.go b/pkg/cmd/tracker-resource/view/view.go index b4a0a27aa..4aae7ab81 100644 --- a/pkg/cmd/tracker-resource/view/view.go +++ b/pkg/cmd/tracker-resource/view/view.go @@ -119,13 +119,16 @@ func NewCmdView(f *cmdutil.Factory) *cobra.Command { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Type:"), v.Type) _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Origin:"), v.Origin) _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Path:"), v.Path) + _, _ = fmt.Fprintf(out, "%s%v\n", label.Render("Excluded:"), v.Excluded) if v.Description != "" { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Description:"), v.Description) } + if v.LastDetectedAt != nil { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Last Detected:"), cmdutil.FormatTime(*v.LastDetectedAt)) } + _, _ = fmt.Fprintln(out) _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(v.CreatedAt)) _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(v.UpdatedAt)) diff --git a/pkg/cmd/trust-center/file/delete/delete.go b/pkg/cmd/trust-center/file/delete/delete.go index 39f6c4607..40b4512d1 100644 --- a/pkg/cmd/trust-center/file/delete/delete.go +++ b/pkg/cmd/trust-center/file/delete/delete.go @@ -45,6 +45,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete trust center file %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/trust-center/file/list/list.go b/pkg/cmd/trust-center/file/list/list.go index 55ecdc5ff..b7ce0291b 100644 --- a/pkg/cmd/trust-center/file/list/list.go +++ b/pkg/cmd/trust-center/file/list/list.go @@ -115,6 +115,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"NAME", "CREATED_AT", "UPDATED_AT"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -136,12 +137,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrg) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + return &resp.Node.TrustCenterFiles, nil }, ) diff --git a/pkg/cmd/trust-center/reference/create/create.go b/pkg/cmd/trust-center/reference/create/create.go index 314b79849..4cffd7d54 100644 --- a/pkg/cmd/trust-center/reference/create/create.go +++ b/pkg/cmd/trust-center/reference/create/create.go @@ -189,6 +189,7 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { if flagDescription != "" { input["description"] = flagDescription } + if flagWebsite != "" { input["websiteUrl"] = flagWebsite } diff --git a/pkg/cmd/trust-center/reference/delete/delete.go b/pkg/cmd/trust-center/reference/delete/delete.go index 5c77fcf65..e09adbe9c 100644 --- a/pkg/cmd/trust-center/reference/delete/delete.go +++ b/pkg/cmd/trust-center/reference/delete/delete.go @@ -45,6 +45,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete reference %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/trust-center/reference/list/list.go b/pkg/cmd/trust-center/reference/list/list.go index 2b915e483..554615fff 100644 --- a/pkg/cmd/trust-center/reference/list/list.go +++ b/pkg/cmd/trust-center/reference/list/list.go @@ -119,6 +119,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"RANK", "NAME", "CREATED_AT", "UPDATED_AT"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -142,15 +143,19 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrg) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + if resp.Node.TrustCenter == nil { return nil, fmt.Errorf("trust center not found for organization %s", flagOrg) } + return &resp.Node.TrustCenter.References, nil }, ) @@ -173,6 +178,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if r.WebsiteUrl != nil { website = *r.WebsiteUrl } + rows = append(rows, []string{ r.ID, r.Name, diff --git a/pkg/cmd/trust-center/update/update.go b/pkg/cmd/trust-center/update/update.go index 77ec26117..6bbc6dc0e 100644 --- a/pkg/cmd/trust-center/update/update.go +++ b/pkg/cmd/trust-center/update/update.go @@ -143,10 +143,12 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("active") { input["active"] = flagActive } + if cmd.Flags().Changed("search-engine-indexing") { if err := cmdutil.ValidateEnum("search-engine-indexing", flagSearchEngineIndexing, []string{"INDEXABLE", "NOT_INDEXABLE"}); err != nil { return err } + input["searchEngineIndexing"] = flagSearchEngineIndexing } diff --git a/pkg/cmd/user/list/list.go b/pkg/cmd/user/list/list.go index e32e433a7..36d685bce 100644 --- a/pkg/cmd/user/list/list.go +++ b/pkg/cmd/user/list/list.go @@ -122,6 +122,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrder, []string{"FULL_NAME", "CREATED_AT", "KIND"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrder, "direction": flagOrderDir, @@ -134,6 +135,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("contract-ended", flagContractEnded, []string{"true", "false"}); err != nil { return err } + filter["contractEnded"] = flagContractEnded == "true" } @@ -141,6 +143,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("state", flagState, []string{"ACTIVE", "INACTIVE"}); err != nil { return err } + filter["state"] = flagState } @@ -163,12 +166,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("organization %s not found", flagOrg) } + if resp.Node.Typename != "Organization" { return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) } + return &resp.Node.Profiles, nil }, ) diff --git a/pkg/cmd/user/view/view.go b/pkg/cmd/user/view/view.go index d6c23cbe7..0747515c9 100644 --- a/pkg/cmd/user/view/view.go +++ b/pkg/cmd/user/view/view.go @@ -139,6 +139,7 @@ func NewCmdView(f *cmdutil.Factory) *cobra.Command { if len(p.AdditionalEmailAddresses) > 0 { _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s\n", bold.Render("Additional Emails")) for _, email := range p.AdditionalEmailAddresses { _, _ = fmt.Fprintf(out, " %s\n", email) @@ -149,6 +150,7 @@ func NewCmdView(f *cmdutil.Factory) *cobra.Command { if p.ContractStartDate != nil { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Contract Start:"), *p.ContractStartDate) } + if p.ContractEndDate != nil { _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Contract End:"), *p.ContractEndDate) } diff --git a/pkg/cmd/webhook/delete/delete.go b/pkg/cmd/webhook/delete/delete.go index 44a09aeb5..290f255cf 100644 --- a/pkg/cmd/webhook/delete/delete.go +++ b/pkg/cmd/webhook/delete/delete.go @@ -45,6 +45,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { } var confirmed bool + err := huh.NewConfirm(). Title(fmt.Sprintf("Delete webhook subscription %s?", args[0])). Value(&confirmed). @@ -52,6 +53,7 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { if err != nil { return err } + if !confirmed { return nil } diff --git a/pkg/cmd/webhook/event/list/list.go b/pkg/cmd/webhook/event/list/list.go index 290d9aaf7..9bdbee398 100644 --- a/pkg/cmd/webhook/event/list/list.go +++ b/pkg/cmd/webhook/event/list/list.go @@ -97,6 +97,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -118,12 +119,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + if resp.Node == nil { return nil, fmt.Errorf("webhook subscription %s not found", args[0]) } + if resp.Node.Typename != "WebhookSubscription" { return nil, fmt.Errorf("expected WebhookSubscription node, got %s", resp.Node.Typename) } + return &resp.Node.Events, nil }, ) @@ -135,6 +139,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if events == nil { events = []webhookEvent{} } + return cmdutil.PrintJSON(f.IOStreams.Out, events) } diff --git a/pkg/cmd/webhook/list/list.go b/pkg/cmd/webhook/list/list.go index f69f42904..216b27284 100644 --- a/pkg/cmd/webhook/list/list.go +++ b/pkg/cmd/webhook/list/list.go @@ -96,6 +96,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT"}); err != nil { return err } + variables["orderBy"] = map[string]any{ "field": flagOrderBy, "direction": flagOrderDir, @@ -118,6 +119,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if err := json.Unmarshal(data, &resp); err != nil { return nil, err } + return &resp.Viewer.Organization.WebhookSubscriptions, nil }, ) @@ -129,6 +131,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command { if webhooks == nil { webhooks = []webhookSubscription{} } + return cmdutil.PrintJSON(f.IOStreams.Out, webhooks) } diff --git a/pkg/cmd/webhook/update/update.go b/pkg/cmd/webhook/update/update.go index 886361236..d5c018d75 100644 --- a/pkg/cmd/webhook/update/update.go +++ b/pkg/cmd/webhook/update/update.go @@ -74,6 +74,7 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { return fmt.Errorf("invalid --event value %q: valid values are %s", e, strings.Join(shared.ValidEvents, ", ")) } } + input["selectedEvents"] = flagEvents } diff --git a/pkg/connector/apikey.go b/pkg/connector/apikey.go index 6deb35c71..fb4db5913 100644 --- a/pkg/connector/apikey.go +++ b/pkg/connector/apikey.go @@ -42,11 +42,13 @@ func (c *APIKeyConnection) Client(ctx context.Context) (*http.Client, error) { tokenType: "Bearer", underlying: httpclient.DefaultPooledTransport(httpclient.WithSSRFProtection()), } + return &http.Client{Transport: transport}, nil } func (c APIKeyConnection) MarshalJSON() ([]byte, error) { type Alias APIKeyConnection + return json.Marshal(&struct { Type string `json:"type"` Alias @@ -58,10 +60,12 @@ func (c APIKeyConnection) MarshalJSON() ([]byte, error) { func (c *APIKeyConnection) UnmarshalJSON(data []byte) error { type Alias APIKeyConnection + aux := &struct { *Alias }{ Alias: (*Alias)(c), } + return json.Unmarshal(data, &aux) } diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 362cb5777..34a425d38 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -70,6 +70,7 @@ func UnmarshalConnection(protocol string, provider string, data []byte) (Connect if err := json.Unmarshal(data, &slackConn); err != nil { return nil, fmt.Errorf("cannot unmarshal slack connection: %w", err) } + return &slackConn, nil default: @@ -77,6 +78,7 @@ func UnmarshalConnection(protocol string, provider string, data []byte) (Connect if err := json.Unmarshal(data, &conn); err != nil { return nil, fmt.Errorf("cannot unmarshal oauth2 connection: %w", err) } + return &conn, nil } @@ -85,6 +87,7 @@ func UnmarshalConnection(protocol string, provider string, data []byte) (Connect if err := json.Unmarshal(data, &conn); err != nil { return nil, fmt.Errorf("cannot unmarshal api key connection: %w", err) } + return &conn, nil } diff --git a/pkg/connector/oauth2.go b/pkg/connector/oauth2.go index 200d89290..094e8fcbe 100644 --- a/pkg/connector/oauth2.go +++ b/pkg/connector/oauth2.go @@ -143,11 +143,13 @@ func (c *OAuth2Connector) Initiate( ConnectorID: opts.ConnectorID, RequestedScopes: opts.Scopes, } + if r != nil { if continueURL := r.URL.Query().Get("continue"); continueURL != "" { stateData.ContinueURL = continueURL } } + return c.InitiateWithState(ctx, stateData, opts) } @@ -166,6 +168,7 @@ func (c *OAuth2Connector) InitiateWithState( if err != nil { return "", fmt.Errorf("cannot generate PKCE verifier: %w", err) } + stateData.CodeVerifier = verifier } @@ -179,6 +182,7 @@ func (c *OAuth2Connector) InitiateWithState( authCodeQuery.Set("client_id", c.ClientID) authCodeQuery.Set("redirect_uri", c.RedirectURI) authCodeQuery.Set("response_type", "code") + if len(opts.Scopes) > 0 { authCodeQuery.Set("scope", strings.Join(opts.Scopes, " ")) } @@ -200,6 +204,7 @@ func (c *OAuth2Connector) InitiateWithState( if incrementalAuth && k == "prompt" && v == "consent" { continue } + authCodeQuery.Set(k, v) } @@ -220,6 +225,7 @@ func generatePKCEVerifier() (string, error) { if _, err := rand.Read(b); err != nil { return "", fmt.Errorf("cannot read random bytes: %w", err) } + return base64.RawURLEncoding.EncodeToString(b), nil } @@ -277,6 +283,7 @@ func (c *OAuth2Connector) CompleteWithState(ctx context.Context, r *http.Request if err != nil { return nil, nil, fmt.Errorf("cannot post token URL: %w", err) } + defer func() { _ = tokenResp.Body.Close() }() if tokenResp.StatusCode != http.StatusOK { @@ -351,6 +358,7 @@ func (c *OAuth2Connector) buildTokenRequest(ctx context.Context, code, redirectU if codeVerifier != "" { body["code_verifier"] = codeVerifier } + jsonBody, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("cannot marshal token request body: %w", err) @@ -370,6 +378,7 @@ func (c *OAuth2Connector) buildTokenRequest(ctx context.Context, code, redirectU req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", "Probo Connector") req.Header.Set("Authorization", basicAuthHeader(c.ClientID, c.ClientSecret)) + return req, nil case "basic-form": @@ -378,6 +387,7 @@ func (c *OAuth2Connector) buildTokenRequest(ctx context.Context, code, redirectU formData.Set("code", code) formData.Set("redirect_uri", redirectURI) formData.Set("grant_type", "authorization_code") + if codeVerifier != "" { formData.Set("code_verifier", codeVerifier) } @@ -396,6 +406,7 @@ func (c *OAuth2Connector) buildTokenRequest(ctx context.Context, code, redirectU req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", "Probo Connector") req.Header.Set("Authorization", basicAuthHeader(c.ClientID, c.ClientSecret)) + return req, nil default: @@ -406,6 +417,7 @@ func (c *OAuth2Connector) buildTokenRequest(ctx context.Context, code, redirectU formData.Set("code", code) formData.Set("redirect_uri", redirectURI) formData.Set("grant_type", "authorization_code") + if codeVerifier != "" { formData.Set("code_verifier", codeVerifier) } @@ -423,6 +435,7 @@ func (c *OAuth2Connector) buildTokenRequest(ctx context.Context, code, redirectU req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8") req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", "Probo Connector") + return req, nil } } @@ -457,6 +470,7 @@ func (c *OAuth2Connection) ClientWithOptions(ctx context.Context, opts ...httpcl client := &http.Client{ Transport: transport, } + return client, nil } @@ -481,6 +495,7 @@ func (c *OAuth2Connection) RefreshableClient(ctx context.Context, cfg OAuth2Refr // Determine auth style based on TokenEndpointAuth authStyle := oauth2.AuthStyleInParams + switch cfg.TokenEndpointAuth { case "basic-form", "basic-json": authStyle = oauth2.AuthStyleInHeader @@ -529,6 +544,7 @@ func (c *OAuth2Connection) RefreshableClient(ctx context.Context, cfg OAuth2Refr // Update the connection with the potentially refreshed token c.AccessToken = newToken.AccessToken c.ExpiresAt = newToken.Expiry + c.TokenType = newToken.TokenType if newToken.RefreshToken != "" { c.RefreshToken = newToken.RefreshToken @@ -558,6 +574,7 @@ func (c *OAuth2Connection) clientCredentialsClient(ctx context.Context, opts ... formData := url.Values{} formData.Set("grant_type", "client_credentials") + if c.Scope != "" { formData.Set("scope", c.Scope) } @@ -585,6 +602,7 @@ func (c *OAuth2Connection) clientCredentialsClient(ctx context.Context, opts ... if err != nil { return nil, fmt.Errorf("cannot post client credentials token URL: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { @@ -609,9 +627,11 @@ func (c *OAuth2Connection) clientCredentialsClient(ctx context.Context, opts ... if rawToken.TokenType != "" { c.TokenType = rawToken.TokenType } + if c.TokenType == "" { c.TokenType = "Bearer" } + if rawToken.ExpiresIn > 0 { c.ExpiresAt = time.Now().Add(time.Duration(rawToken.ExpiresIn) * time.Second) } @@ -627,6 +647,7 @@ func (c *OAuth2Connection) clientCredentialsClient(ctx context.Context, opts ... func (c OAuth2Connection) MarshalJSON() ([]byte, error) { type Alias OAuth2Connection + return json.Marshal(&struct { Type string `json:"type"` Alias @@ -638,11 +659,13 @@ func (c OAuth2Connection) MarshalJSON() ([]byte, error) { func (c *OAuth2Connection) UnmarshalJSON(data []byte) error { type Alias OAuth2Connection + aux := &struct { *Alias }{ Alias: (*Alias)(c), } + return json.Unmarshal(data, &aux) } @@ -660,5 +683,6 @@ func (t *oauth2Transport) RoundTrip(req *http.Request) (*http.Response, error) { // string), so we always send "Bearer" -- the only scheme any connector in // this codebase actually needs. req2.Header.Set("Authorization", "Bearer "+t.token) + return t.underlying.RoundTrip(req2) } diff --git a/pkg/connector/oauth2_grant_type.go b/pkg/connector/oauth2_grant_type.go index b269af166..775af1ffc 100644 --- a/pkg/connector/oauth2_grant_type.go +++ b/pkg/connector/oauth2_grant_type.go @@ -26,5 +26,6 @@ func (g OAuth2GrantType) IsValid() bool { case OAuth2GrantTypeAuthorizationCode, OAuth2GrantTypeClientCredentials: return true } + return false } diff --git a/pkg/connector/oauth2_test.go b/pkg/connector/oauth2_test.go index 18d1e8ee0..8638b13ba 100644 --- a/pkg/connector/oauth2_test.go +++ b/pkg/connector/oauth2_test.go @@ -183,6 +183,7 @@ func TestBuildTokenRequest_BasicJSON(t *testing.T) { require.NoError(t, err) var jsonBody map[string]string + err = json.Unmarshal(body, &jsonBody) require.NoError(t, err) @@ -193,6 +194,7 @@ func TestBuildTokenRequest_BasicJSON(t *testing.T) { // JSON body must NOT contain client credentials _, hasClientID := jsonBody["client_id"] _, hasClientSecret := jsonBody["client_secret"] + assert.False(t, hasClientID, "JSON body should not contain client_id") assert.False(t, hasClientSecret, "JSON body should not contain client_secret") } @@ -636,12 +638,14 @@ func TestInitiateWithState_PKCE(t *testing.T) { t.Parallel() var capturedVerifier string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) assert.NoError(t, err) form, err := url.ParseQuery(string(body)) assert.NoError(t, err) + capturedVerifier = form.Get("code_verifier") w.Header().Set("Content-Type", "application/json") @@ -676,6 +680,7 @@ func TestInitiateWithState_PKCE(t *testing.T) { payload, err := DecodeOAuth2StatePayload(stateToken) require.NoError(t, err) + expectedVerifier := payload.Data.CodeVerifier require.NotEmpty(t, expectedVerifier) @@ -705,11 +710,13 @@ func TestApplyProviderDefaults_AuthURLTemplating(t *testing.T) { // test so we do not have to wait for a real Vercel-style provider // to land. Restore on teardown. const fakeProvider = "TEST_TEMPLATED_AUTH_URL" + previous, hadPrevious := providerDefinitions[fakeProvider] providerDefinitions[fakeProvider] = providerDefinition{ AuthURL: "https://example.com/integrations/{integration_slug}/new", TokenURL: "https://example.com/oauth/token", } + t.Cleanup(func() { if hadPrevious { providerDefinitions[fakeProvider] = previous diff --git a/pkg/connector/pagerduty.go b/pkg/connector/pagerduty.go index dbf0dc0ef..3ad2aef89 100644 --- a/pkg/connector/pagerduty.go +++ b/pkg/connector/pagerduty.go @@ -29,14 +29,17 @@ func AbsorbPagerDutyTokenResponse(state *OAuth2State, body []byte) { if state == nil || state.Provider != PagerDutyProvider { return } + var pd struct { Subdomain string `json:"subdomain"` } if err := json.Unmarshal(body, &pd); err != nil || pd.Subdomain == "" { return } + if state.ProviderMetadata == nil { state.ProviderMetadata = map[string]string{} } + state.ProviderMetadata["subdomain"] = pd.Subdomain } diff --git a/pkg/connector/registry.go b/pkg/connector/registry.go index bb93c4195..d803e049c 100644 --- a/pkg/connector/registry.go +++ b/pkg/connector/registry.go @@ -39,21 +39,25 @@ func NewConnectorRegistry() *ConnectorRegistry { func (r *ConnectorRegistry) Register(provider string, c Connector) error { r.Lock() defer r.Unlock() + if _, ok := r.connectors[provider]; ok { return fmt.Errorf("cannot register connector %q: already registered", provider) } r.connectors[provider] = c + return nil } func (r *ConnectorRegistry) Get(provider string) (Connector, error) { r.RLock() defer r.RUnlock() + c, ok := r.connectors[provider] if !ok { return nil, fmt.Errorf("cannot find connector %q", provider) } + return c, nil } diff --git a/pkg/connector/scopes.go b/pkg/connector/scopes.go index 3ad8a4a91..e2c8501f8 100644 --- a/pkg/connector/scopes.go +++ b/pkg/connector/scopes.go @@ -31,16 +31,21 @@ func ParseScopeString(s string) []string { if len(fields) == 0 { return []string{} } + seen := make(map[string]struct{}, len(fields)) + out := make([]string, 0, len(fields)) for _, f := range fields { if _, ok := seen[f]; ok { continue } + seen[f] = struct{}{} out = append(out, f) } + sort.Strings(out) + return out } @@ -50,9 +55,11 @@ func FormatScopeString(scopes []string) string { if len(scopes) == 0 { return "" } + sorted := make([]string, len(scopes)) copy(sorted, scopes) sort.Strings(sorted) + return strings.Join(sorted, " ") } @@ -61,18 +68,23 @@ func FormatScopeString(scopes []string) string { // result is a fresh slice and never aliases any input. func UnionScopes(scopeSets ...[]string) []string { seen := map[string]struct{}{} + for _, set := range scopeSets { for _, s := range set { if s == "" { continue } + seen[s] = struct{}{} } } + out := make([]string, 0, len(seen)) for s := range seen { out = append(out, s) } + sort.Strings(out) + return out } diff --git a/pkg/connector/slack.go b/pkg/connector/slack.go index d7dcb6584..61fa38844 100644 --- a/pkg/connector/slack.go +++ b/pkg/connector/slack.go @@ -120,9 +120,11 @@ func ParseSlackTokenResponse(body []byte, oauth2Conn OAuth2Connection, organizat if slackResponse.Error != "" { return nil, nil, fmt.Errorf("cannot complete Slack OAuth2 flow: %s", slackResponse.Error) } + if !slackResponse.Ok { return nil, nil, fmt.Errorf("cannot complete Slack OAuth2 flow: ok=false") } + if oauth2Conn.AccessToken == "" { return nil, nil, fmt.Errorf("cannot complete Slack OAuth2 flow: missing access token") } diff --git a/pkg/connector/slack_test.go b/pkg/connector/slack_test.go index 50903b99a..47bbb7fd0 100644 --- a/pkg/connector/slack_test.go +++ b/pkg/connector/slack_test.go @@ -36,6 +36,7 @@ func TestParseSlackTokenResponse(t *testing.T) { t.Run("with incoming webhook", func(t *testing.T) { t.Parallel() + body := []byte(`{"ok":true,"incoming_webhook":{"url":"https://hooks.slack.com/services/T/B/X","channel":"#general","channel_id":"C123"}}`) conn, returnedOrgID, err := ParseSlackTokenResponse(body, base, orgID) @@ -52,6 +53,7 @@ func TestParseSlackTokenResponse(t *testing.T) { t.Run("without incoming webhook", func(t *testing.T) { t.Parallel() + body := []byte(`{"ok":true}`) conn, returnedOrgID, err := ParseSlackTokenResponse(body, base, orgID) @@ -67,6 +69,7 @@ func TestParseSlackTokenResponse(t *testing.T) { t.Run("slack error response", func(t *testing.T) { t.Parallel() + body := []byte(`{"ok":false,"error":"invalid_code"}`) conn, returnedOrgID, err := ParseSlackTokenResponse(body, base, orgID) @@ -78,6 +81,7 @@ func TestParseSlackTokenResponse(t *testing.T) { t.Run("missing access token", func(t *testing.T) { t.Parallel() + body := []byte(`{"ok":true}`) connWithoutToken := base connWithoutToken.AccessToken = "" diff --git a/pkg/connector/vercel.go b/pkg/connector/vercel.go index 4211a0083..cdaabdf42 100644 --- a/pkg/connector/vercel.go +++ b/pkg/connector/vercel.go @@ -43,12 +43,14 @@ func FetchVercelUser(ctx context.Context, client *http.Client) (VercelUser, erro if err != nil { return VercelUser{}, fmt.Errorf("cannot create vercel user request: %w", err) } + req.Header.Set("Accept", "application/json") resp, err := client.Do(req) if err != nil { return VercelUser{}, fmt.Errorf("cannot execute vercel user request: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { @@ -61,6 +63,7 @@ func FetchVercelUser(ctx context.Context, client *http.Client) (VercelUser, erro if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { return VercelUser{}, fmt.Errorf("cannot decode vercel user response: %w", err) } + return body.User, nil } @@ -77,14 +80,17 @@ func FetchVercelUserID(ctx context.Context, accessToken string) (string, error) if err != nil { return "", fmt.Errorf("cannot create vercel user request: %w", err) } + req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", "Bearer "+accessToken) client := httpclient.DefaultClient(httpclient.WithSSRFProtection()) + resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("cannot execute vercel user request: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { @@ -97,5 +103,6 @@ func FetchVercelUserID(ctx context.Context, accessToken string) (string, error) if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { return "", fmt.Errorf("cannot decode vercel user response: %w", err) } + return body.User.ID, nil } diff --git a/pkg/cookiebanner/anonymize.go b/pkg/cookiebanner/anonymize.go index de8a107e3..555f94d4a 100644 --- a/pkg/cookiebanner/anonymize.go +++ b/pkg/cookiebanner/anonymize.go @@ -31,5 +31,6 @@ func AnonymizeIP(raw string) string { } mask := net.CIDRMask(48, 128) + return ip.Mask(mask).String() } diff --git a/pkg/cookiebanner/equal.go b/pkg/cookiebanner/equal.go index ee32194af..c99218cdd 100644 --- a/pkg/cookiebanner/equal.go +++ b/pkg/cookiebanner/equal.go @@ -27,6 +27,7 @@ func ptrEqual[T comparable](a, b *T) bool { if a == nil || b == nil { return a == b } + return *a == *b } @@ -38,8 +39,10 @@ func jsonEqual(a, b json.RawMessage) (bool, error) { if err := json.Unmarshal(a, &av); err != nil { return false, fmt.Errorf("cannot unmarshal first json blob: %w", err) } + if err := json.Unmarshal(b, &bv); err != nil { return false, fmt.Errorf("cannot unmarshal second json blob: %w", err) } + return reflect.DeepEqual(av, bv), nil } diff --git a/pkg/cookiebanner/pattern_analysis_worker.go b/pkg/cookiebanner/pattern_analysis_worker.go index f3ad65f8f..996db5374 100644 --- a/pkg/cookiebanner/pattern_analysis_worker.go +++ b/pkg/cookiebanner/pattern_analysis_worker.go @@ -60,9 +60,11 @@ func durationBucket(maxAge *int) int { remaining := *maxAge total := 0 + for _, u := range durationUnits { if remaining >= u.seconds-u.snap { count := remaining / u.seconds + leftover := remaining - count*u.seconds if leftover >= u.seconds-u.snap { count++ @@ -72,9 +74,11 @@ func durationBucket(maxAge *int) int { } else { remaining = leftover } + total += count * u.seconds } } + return total } @@ -120,6 +124,7 @@ func (h *patternAnalysisHandler) Claim(ctx context.Context) (coredata.CookieBann if errors.Is(err, coredata.ErrResourceNotFound) { return coredata.CookieBanner{}, worker.ErrNoTask } + return coredata.CookieBanner{}, fmt.Errorf("cannot claim pattern analysis task: %w", err) } @@ -133,11 +138,14 @@ func (h *patternAnalysisHandler) Process(ctx context.Context, banner coredata.Co scope := coredata.NewScopeFromObjectID(banner.ID) var uncategorised coredata.CookieCategory + hasUncategorised := true + if err := uncategorised.LoadUncategorisedByCookieBannerID(ctx, tx, scope, banner.ID); err != nil { if !errors.Is(err, coredata.ErrResourceNotFound) { return fmt.Errorf("cannot load uncategorised category: %w", err) } + hasUncategorised = false } @@ -156,8 +164,10 @@ func (h *patternAnalysisHandler) Process(ctx context.Context, banner coredata.Co mergeGroups := findMergeGroups(exactPatterns, patternMergeThreshold) consentChanged := false + for key, group := range mergeGroups { var maxAge *int + if key.durationBucket >= 0 { v := key.durationBucket maxAge = &v @@ -187,6 +197,7 @@ func (h *patternAnalysisHandler) Process(ctx context.Context, banner coredata.Co if err != nil { return fmt.Errorf("cannot insert glob pattern %q: %w", key.template, err) } + if !inserted { if err := globPattern.LoadByBannerIDTypeAndPattern(ctx, tx, scope, banner.ID, key.trackerType, key.template, maxAge); err != nil { return fmt.Errorf("cannot load existing glob pattern %q: %w", key.template, err) @@ -266,19 +277,24 @@ func findMergeGroups( if tmpl, ok := heuristicTemplate(p.Pattern); ok { key := mergeGroupKey{categoryID: p.CookieCategoryID, trackerType: p.TrackerType, template: tmpl, durationBucket: bucket} + mk := memberKey{key, p} if !seen[mk] { seen[mk] = true + templateCounts[key] = append(templateCounts[key], p) } + heuristicKeys[key] = true } for _, tmpl := range templateCandidates(p.Pattern) { key := mergeGroupKey{categoryID: p.CookieCategoryID, trackerType: p.TrackerType, template: tmpl, durationBucket: bucket} + mk := memberKey{key, p} if !seen[mk] { seen[mk] = true + templateCounts[key] = append(templateCounts[key], p) } } @@ -292,12 +308,15 @@ func findMergeGroups( } var candidates []candidate + for key, pats := range templateCounts { isH := heuristicKeys[key] + effectiveThreshold := threshold if isH { effectiveThreshold = 1 } + if len(pats) >= effectiveThreshold { candidates = append(candidates, candidate{key, len(strings.ReplaceAll(key.template, "*", "")), isH, pats}) } @@ -312,12 +331,15 @@ func findMergeGroups( if candidates[i].isHeuristic != candidates[j].isHeuristic { return candidates[i].isHeuristic } + if candidates[i].fixedChars != candidates[j].fixedChars { return candidates[i].fixedChars > candidates[j].fixedChars } + if len(candidates[i].patterns) != len(candidates[j].patterns) { return len(candidates[i].patterns) > len(candidates[j].patterns) } + return candidates[i].key.template < candidates[j].key.template }, ) @@ -332,6 +354,7 @@ func findMergeGroups( } var unassigned []*coredata.TrackerPattern + for _, p := range c.patterns { if !assigned[p] { unassigned = append(unassigned, p) @@ -361,6 +384,7 @@ func heuristicTemplate(name string) (string, bool) { var prefix strings.Builder for len(tokens) > 1 && tokens[0] == "" { prefix.WriteString(string(seps[0])) + tokens = tokens[1:] seps = seps[1:] } @@ -378,21 +402,28 @@ func heuristicTemplate(name string) (string, bool) { } changed := false - var resultTokens []string - var resultSeps []byte + + var ( + resultTokens []string + resultSeps []byte + ) + for i, t := range tokens { if looksVariable(t) { changed = true + if len(resultTokens) == 0 || resultTokens[len(resultTokens)-1] != "*" { if i > 0 { resultSeps = append(resultSeps, seps[i-1]) } + resultTokens = append(resultTokens, "*") } } else { if i > 0 { resultSeps = append(resultSeps, seps[i-1]) } + resultTokens = append(resultTokens, t) } } @@ -435,6 +466,7 @@ func looksVariable(token string) bool { hasLetter := false allHex := true allDigits := true + for _, ch := range token { switch { case ch >= '0' && ch <= '9': @@ -478,25 +510,31 @@ func isUUIDShape(s string) bool { if len(s) != 36 { return false } + for i, ch := range s { if i == 8 || i == 13 || i == 18 || i == 23 { if ch != '-' { return false } + continue } + if (ch < '0' || ch > '9') && (ch < 'a' || ch > 'f') && (ch < 'A' || ch > 'F') { return false } } + return true } func splitTokens(name string) ([]string, []byte) { underscoreParts := strings.Split(name, "_") - var tokens []string - var seps []byte + var ( + tokens []string + seps []byte + ) for i, part := range underscoreParts { if i > 0 { @@ -510,6 +548,7 @@ func splitTokens(name string) ([]string, []byte) { if j > 0 { seps = append(seps, '-') } + tokens = append(tokens, sub) } } @@ -524,12 +563,15 @@ func splitTokens(name string) ([]string, []byte) { func joinTokens(tokens []string, seps []byte) string { var b strings.Builder + for i, t := range tokens { if i > 0 { b.WriteByte(seps[i-1]) } + b.WriteString(t) } + return b.String() } @@ -542,12 +584,14 @@ func globMatch(pattern, name string) bool { if !strings.HasPrefix(name, parts[0]) { return false } + name = name[len(parts[0]):] last := parts[len(parts)-1] if !strings.HasSuffix(name, last) { return false } + name = name[:len(name)-len(last)] for _, part := range parts[1 : len(parts)-1] { @@ -555,6 +599,7 @@ func globMatch(pattern, name string) bool { if idx == -1 { return false } + name = name[idx+len(part):] } @@ -567,7 +612,9 @@ func bestSource(patterns []*coredata.TrackerPattern) *coredata.CookieSource { return p.Source } } + src := coredata.CookieSourcePreExisting + return &src } @@ -582,6 +629,7 @@ func (h *patternAnalysisHandler) adoptUncategorisedPatterns( if errors.Is(err, coredata.ErrResourceNotFound) { return false, nil } + return false, fmt.Errorf("cannot load uncategorised category: %w", err) } @@ -609,6 +657,7 @@ func (h *patternAnalysisHandler) adoptUncategorisedPatterns( ) exactMatchType := coredata.TrackerPatternMatchTypeExact + var uncategorisedExact coredata.TrackerPatterns if err := uncategorisedExact.LoadAllByCookieBannerID( ctx, @@ -622,8 +671,10 @@ func (h *patternAnalysisHandler) adoptUncategorisedPatterns( } adopted := false + for _, ep := range uncategorisedExact { var match *coredata.TrackerPattern + epBucket := durationBucket(ep.MaxAgeSeconds) for _, gp := range globPatterns { if ep.TrackerType == gp.TrackerType && globMatch(gp.Pattern, ep.Pattern) && durationBucket(gp.MaxAgeSeconds) == epBucket { @@ -646,6 +697,7 @@ func (h *patternAnalysisHandler) adoptUncategorisedPatterns( } adopted = true + h.logger.InfoCtx( ctx, "adopted uncategorised exact pattern into glob pattern", diff --git a/pkg/cookiebanner/pattern_analysis_worker_test.go b/pkg/cookiebanner/pattern_analysis_worker_test.go index eeeba240d..601b38f46 100644 --- a/pkg/cookiebanner/pattern_analysis_worker_test.go +++ b/pkg/cookiebanner/pattern_analysis_worker_test.go @@ -245,8 +245,10 @@ func TestHeuristicTemplate(t *testing.T) { tt.name, func(t *testing.T) { t.Parallel() + tmpl, changed := heuristicTemplate(tt.input) assert.Equal(t, tt.changed, changed) + if changed { assert.Equal(t, tt.template, tmpl) } @@ -329,6 +331,7 @@ func TestTemplateCandidates(t *testing.T) { tt.name, func(t *testing.T) { t.Parallel() + result := templateCandidates(tt.input) assert.Equal(t, tt.expected, result) }, @@ -506,6 +509,7 @@ func TestSplitTokens(t *testing.T) { tt.name, func(t *testing.T) { t.Parallel() + tokens, seps := splitTokens(tt.input) assert.Equal(t, tt.tokens, tokens) assert.Equal(t, tt.seps, seps) @@ -988,6 +992,7 @@ func TestDurationBucket(t *testing.T) { tt.name, func(t *testing.T) { t.Parallel() + result := durationBucket(tt.maxAge) assert.Equal(t, tt.expected, result) }, diff --git a/pkg/cookiebanner/search_tracker_patterns_tool.go b/pkg/cookiebanner/search_tracker_patterns_tool.go index 355554052..bb63d3f33 100644 --- a/pkg/cookiebanner/search_tracker_patterns_tool.go +++ b/pkg/cookiebanner/search_tracker_patterns_tool.go @@ -51,6 +51,7 @@ func searchTrackerPatternsTool(pgClient *pg.Client) agent.Tool { ctx, func(ctx context.Context, conn pg.Querier) error { var patterns coredata.CommonTrackerPatterns + results, err := patterns.FindByKeyword(ctx, conn, p.Query, 10) if err != nil { return err diff --git a/pkg/cookiebanner/service.go b/pkg/cookiebanner/service.go index 43c7ab3fc..d6840ba8d 100644 --- a/pkg/cookiebanner/service.go +++ b/pkg/cookiebanner/service.go @@ -328,6 +328,7 @@ func (r *UpsertCookieBannerTranslationRequest) Validate() error { } } } + continue } @@ -340,6 +341,7 @@ func (r *UpsertCookieBannerTranslationRequest) Validate() error { if key == "banner_description" { validators = append(validators, validator.ContainsSubstring("{{cookie_policy_link}}")) } + v.Check(s, "translations."+key, validators...) } @@ -353,10 +355,12 @@ func (r *CreateTrackerPatternRequest) Validate() error { v.Check(string(r.TrackerType), "tracker_type", validator.Required(), validator.OneOfSlice( func() []string { types := coredata.TrackerTypes() + s := make([]string, len(types)) for i, t := range types { s[i] = string(t) } + return s }(), )) @@ -364,15 +368,18 @@ func (r *CreateTrackerPatternRequest) Validate() error { v.Check(string(r.MatchType), "match_type", validator.Required(), validator.OneOfSlice( func() []string { types := coredata.TrackerPatternMatchTypes() + s := make([]string, len(types)) for i, t := range types { s[i] = string(t) } + return s }(), )) v.Check(r.Pattern, "pattern", func(value any) *validator.ValidationError { s, _ := value.(string) + switch r.MatchType { case coredata.TrackerPatternMatchTypeGlob: if strings.Count(s, "*") != 1 { @@ -389,6 +396,7 @@ func (r *CreateTrackerPatternRequest) Validate() error { } } } + return nil }) v.Check(r.DisplayName, "display_name", validator.Required(), validator.SafeTextNoNewLine(255)) @@ -401,6 +409,7 @@ func (r *UpdateTrackerPatternRequest) Validate() error { v := validator.New() v.Check(r.TrackerPatternID, "tracker_pattern_id", validator.Required(), validator.GID(coredata.TrackerPatternEntityType)) + if r.Description != nil { v.Check(*r.Description, "description", validator.SafeText(1000)) } @@ -415,10 +424,12 @@ func (r *CreateTrackerResourceRequest) Validate() error { v.Check(string(r.ResourceType), "resource_type", validator.Required(), validator.OneOfSlice( func() []string { types := coredata.TrackerResourceTypes() + s := make([]string, len(types)) for i, t := range types { s[i] = string(t) } + return s }(), )) @@ -434,9 +445,11 @@ func (r *UpdateTrackerResourceRequest) Validate() error { v := validator.New() v.Check(r.TrackerResourceID, "tracker_resource_id", validator.Required(), validator.GID(coredata.TrackerResourceEntityType)) + if r.DisplayName != nil { v.Check(*r.DisplayName, "display_name", validator.SafeTextNoNewLine(255)) } + if r.Description != nil { v.Check(*r.Description, "description", validator.SafeText(1000)) } @@ -472,8 +485,8 @@ func (s *Service) ensureDraftVersion( snapshot := buildSnapshot(banner, categories, allPatterns) var latest coredata.CookieBannerVersion - err := latest.LoadLatestByCookieBannerID(ctx, tx, scope, banner.ID) + err := latest.LoadLatestByCookieBannerID(ctx, tx, scope, banner.ID) if err == nil { if latestSnapshot, snapErr := latest.GetSnapshot(); snapErr == nil && snapshotsEqual(snapshot, latestSnapshot) { return &latest, nil @@ -483,10 +496,12 @@ func (s *Service) ensureDraftVersion( if err := latest.SetSnapshot(snapshot); err != nil { return nil, fmt.Errorf("cannot set snapshot: %w", err) } + latest.UpdatedAt = time.Now() if err := latest.Update(ctx, tx, scope); err != nil { return nil, fmt.Errorf("cannot update draft version: %w", err) } + return &latest, nil } } @@ -509,6 +524,7 @@ func (s *Service) ensureDraftVersion( if err != nil { return nil, fmt.Errorf("cannot determine next version: %w", err) } + version.Version = nextVersion if err := version.SetSnapshot(snapshot); err != nil { @@ -588,6 +604,7 @@ func (s *Service) CreateCookieBanner( if errors.Is(err, coredata.ErrResourceAlreadyExists) { return ErrOriginAlreadyInUse } + return fmt.Errorf("cannot insert cookie banner: %w", err) } @@ -597,6 +614,7 @@ func (s *Service) CreateCookieBanner( if gcmConsentTypes == nil { gcmConsentTypes = []string{} } + category := &coredata.CookieCategory{ ID: gid.New(scope.GetTenantID(), coredata.CookieCategoryEntityType), OrganizationID: banner.OrganizationID, @@ -620,6 +638,7 @@ func (s *Service) CreateCookieBanner( if dc.Kind == coredata.CookieCategoryKindNecessary { consentMaxAge := req.ConsentExpiryDays * 86400 + consentPattern := &coredata.TrackerPattern{ ID: gid.New(scope.GetTenantID(), coredata.TrackerPatternEntityType), OrganizationID: banner.OrganizationID, @@ -657,6 +676,7 @@ func (s *Service) CreateCookieBanner( } } } + if len(catMap) > 0 { blob["categories"] = catMap } @@ -710,6 +730,7 @@ func (s *Service) GetCookieBanner( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrBannerNotFound } + return fmt.Errorf("cannot load cookie banner: %w", err) } @@ -760,6 +781,7 @@ func (s *Service) GetActiveCookieBanner( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrBannerNotFound } + return fmt.Errorf("cannot load cookie banner: %w", err) } @@ -810,8 +832,10 @@ func (s *Service) CountCookieBannersForOrganization( err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { - var banners coredata.CookieBanners - var err error + var ( + banners coredata.CookieBanners + err error + ) count, err = banners.CountByOrganizationID(ctx, conn, scope, organizationID, filter) if err != nil { @@ -846,6 +870,7 @@ func (s *Service) UpdateCookieBanner( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrBannerNotFound } + return fmt.Errorf("cannot load cookie banner: %w", err) } @@ -864,15 +889,19 @@ func (s *Service) UpdateCookieBanner( if req.Name != nil { banner.Name = *req.Name } + if req.PrivacyPolicyURL != nil { banner.PrivacyPolicyURL = req.PrivacyPolicyURL } + if req.CookiePolicyURL != nil { banner.CookiePolicyURL = *req.CookiePolicyURL } + if req.ConsentExpiryDays != nil { banner.ConsentExpiryDays = *req.ConsentExpiryDays } + if req.DefaultLanguage != nil { banner.DefaultLanguage = *req.DefaultLanguage } @@ -883,6 +912,7 @@ func (s *Service) UpdateCookieBanner( if errors.Is(err, coredata.ErrResourceAlreadyExists) { return ErrOriginAlreadyInUse } + return fmt.Errorf("cannot update cookie banner: %w", err) } @@ -916,6 +946,7 @@ func (s *Service) PublishCookieBannerVersion( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrNoDraftVersion } + return fmt.Errorf("cannot load latest version: %w", err) } @@ -954,6 +985,7 @@ func (s *Service) ActivateCookieBanner( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrBannerNotFound } + return fmt.Errorf("cannot load cookie banner: %w", err) } @@ -968,6 +1000,7 @@ func (s *Service) ActivateCookieBanner( if errors.Is(err, coredata.ErrResourceAlreadyExists) { return ErrOriginAlreadyInUse } + return fmt.Errorf("cannot update cookie banner: %w", err) } @@ -995,6 +1028,7 @@ func (s *Service) DeactivateCookieBanner( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrBannerNotFound } + return fmt.Errorf("cannot load cookie banner: %w", err) } @@ -1032,6 +1066,7 @@ func (s *Service) DeleteCookieBanner( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrBannerNotFound } + return fmt.Errorf("cannot load cookie banner: %w", err) } @@ -1063,6 +1098,7 @@ func (s *Service) CreateCookieCategory( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrBannerNotFound } + return fmt.Errorf("cannot load cookie banner: %w", err) } @@ -1086,6 +1122,7 @@ func (s *Service) CreateCookieCategory( if errors.Is(err, coredata.ErrResourceAlreadyExists) { return ErrCategorySlugAlreadyExists } + return fmt.Errorf("cannot insert cookie category: %w", err) } @@ -1117,6 +1154,7 @@ func (s *Service) GetCookieCategory( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrCategoryNotFound } + return fmt.Errorf("cannot load cookie category: %w", err) } @@ -1189,8 +1227,10 @@ func (s *Service) CountCookieCategoriesForBanner( err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { - var categories coredata.CookieCategories - var err error + var ( + categories coredata.CookieCategories + err error + ) count, err = categories.CountConsentCategoriesByCookieBannerID(ctx, conn, scope, bannerID) if err != nil { @@ -1225,6 +1265,7 @@ func (s *Service) UpdateCookieCategory( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrCategoryNotFound } + return fmt.Errorf("cannot load cookie category: %w", err) } @@ -1241,25 +1282,31 @@ func (s *Service) UpdateCookieCategory( if req.Name != nil { category.Name = *req.Name } + if req.Slug != nil { category.Slug = *req.Slug } + if req.Description != nil { category.Description = *req.Description } + if req.GCMConsentTypes != nil { category.GCMConsentTypes = *req.GCMConsentTypes } + if posthogChanged { if *req.PostHogConsent && category.Kind != coredata.CookieCategoryKindNormal { return ErrPostHogConsentKindInvalid } + if *req.PostHogConsent { var categories coredata.CookieCategories if err := categories.ClearPostHogConsentByBannerID(ctx, tx, scope, category.CookieBannerID); err != nil { return fmt.Errorf("cannot clear posthog consent: %w", err) } } + category.PostHogConsent = *req.PostHogConsent } @@ -1269,6 +1316,7 @@ func (s *Service) UpdateCookieCategory( if errors.Is(err, coredata.ErrResourceAlreadyExists) { return ErrCategorySlugAlreadyExists } + return fmt.Errorf("cannot update cookie category: %w", err) } @@ -1305,6 +1353,7 @@ func (s *Service) ReorderCookieCategory( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrCategoryNotFound } + return fmt.Errorf("cannot load cookie category: %w", err) } @@ -1350,6 +1399,7 @@ func (s *Service) DeleteCookieCategory( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrCategoryNotFound } + return fmt.Errorf("cannot load cookie category: %w", err) } @@ -1396,6 +1446,7 @@ func (s *Service) GetCookieBannerVersion( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrVersionNotFound } + return fmt.Errorf("cannot load cookie banner version: %w", err) } @@ -1444,8 +1495,10 @@ func (s *Service) CountCookieBannerVersionsForBanner( err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { - var versions coredata.CookieBannerVersions - var err error + var ( + versions coredata.CookieBannerVersions + err error + ) count, err = versions.CountByCookieBannerID(ctx, conn, scope, bannerID) if err != nil { @@ -1527,8 +1580,10 @@ func (s *Service) CountCookieConsentRecordsForBanner( err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { - var records coredata.CookieConsentRecords - var err error + var ( + records coredata.CookieConsentRecords + err error + ) count, err = records.CountByCookieBannerID(ctx, conn, scope, bannerID, filter) if err != nil { @@ -1562,6 +1617,7 @@ func (s *Service) GetActiveBannerConfig( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrBannerNotFound } + return fmt.Errorf("cannot load active cookie banner: %w", err) } @@ -1572,6 +1628,7 @@ func (s *Service) GetActiveBannerConfig( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrNoPublishedVersion } + return fmt.Errorf("cannot load latest published version: %w", err) } @@ -1601,6 +1658,7 @@ func (s *Service) GetActiveBannerConfig( } config.Regulation = regulation + config.ConsentMode = ConsentModeForRegulation(regulation) if !isLegacySDK(sdkVersion) { remapTextsForConsentMode(config.Texts, config.ConsentMode) @@ -1622,6 +1680,7 @@ func buildBannerConfig( } resolvedLang := defaultLang + if lang != "" { if _, ok := translations[lang]; ok { resolvedLang = lang @@ -1634,6 +1693,7 @@ func buildBannerConfig( categories = append(categories, c) } } + texts := make(map[string]string) if t, ok := translations[resolvedLang]; ok { @@ -1642,14 +1702,17 @@ func buildBannerConfig( if len(t.Categories) == len(categories) { translated := make([]coredata.CookieBannerVersionSnapshotCategory, len(categories)) copy(translated, categories) + for i, ct := range t.Categories { if ct.Name != "" { translated[i].Name = ct.Name } + if ct.Description != "" { translated[i].Description = ct.Description } } + categories = translated } } @@ -1707,6 +1770,7 @@ func isLegacySDK(version string) bool { func parseMajorMinor(version string) (major, minor int, ok bool) { v := strings.TrimPrefix(version, "v") + parts := strings.SplitN(v, ".", 3) if len(parts) < 2 { return 0, 0, false @@ -1740,13 +1804,16 @@ func (s *Service) SetShowBranding( ctx, func(ctx context.Context, tx pg.Tx) error { var banner coredata.CookieBanner + banner.ID = bannerID if err := banner.UpdateShowBranding(ctx, tx, coredata.NewNoScope(), show); err != nil { if errors.Is(err, coredata.ErrResourceNotFound) { return ErrBannerNotFound } + return fmt.Errorf("cannot update show_branding: %w", err) } + return nil }, ) @@ -1771,14 +1838,15 @@ func (s *Service) UpsertCookieBannerTranslation( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrBannerNotFound } + return fmt.Errorf("cannot load cookie banner: %w", err) } now := time.Now() var existing coredata.CookieBannerTranslation - err := existing.LoadByCookieBannerIDAndLanguage(ctx, tx, scope, req.CookieBannerID, req.Language) + err := existing.LoadByCookieBannerIDAndLanguage(ctx, tx, scope, req.CookieBannerID, req.Language) if err == nil { same, eqErr := jsonEqual(existing.Translations, req.Translations) if eqErr == nil && same { @@ -1787,10 +1855,12 @@ func (s *Service) UpsertCookieBannerTranslation( } existing.Translations = req.Translations + existing.UpdatedAt = now if err := existing.Update(ctx, tx, scope); err != nil { return fmt.Errorf("cannot update cookie banner translation: %w", err) } + result = &existing } else if errors.Is(err, coredata.ErrResourceNotFound) { t := &coredata.CookieBannerTranslation{ @@ -1805,6 +1875,7 @@ func (s *Service) UpsertCookieBannerTranslation( if err := t.Insert(ctx, tx, scope); err != nil { return fmt.Errorf("cannot insert cookie banner translation: %w", err) } + result = t } else { return fmt.Errorf("cannot load cookie banner translation: %w", err) @@ -1855,6 +1926,7 @@ func (s *Service) GetVisitorConsent( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrBannerNotFound } + return fmt.Errorf("cannot load active cookie banner: %w", err) } @@ -1865,6 +1937,7 @@ func (s *Service) GetVisitorConsent( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrConsentNotFound } + return fmt.Errorf("cannot load consent record: %w", err) } @@ -1915,6 +1988,7 @@ func (s *Service) RecordConsent( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrBannerNotFound } + return fmt.Errorf("cannot load active cookie banner: %w", err) } @@ -1925,6 +1999,7 @@ func (s *Service) RecordConsent( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrVersionNotFound } + return fmt.Errorf("cannot load cookie banner version: %w", err) } @@ -1995,6 +2070,7 @@ func (s *Service) ReportDetectedTrackers( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrBannerNotFound } + return fmt.Errorf("cannot load cookie banner: %w", err) } @@ -2005,6 +2081,7 @@ func (s *Service) ReportDetectedTrackers( inserted := 0 now := time.Now() + var matchedPatternIDs []gid.GID for _, dc := range req.Cookies { @@ -2063,6 +2140,7 @@ func (s *Service) ReportDetectedTrackers( if err != nil { return err } + if wasInserted { inserted++ } @@ -2107,6 +2185,7 @@ func (s *Service) reportDetectedTracker( matchedPatternIDs *[]gid.GID, ) error { var matchedPattern coredata.TrackerPattern + err := matchedPattern.FindMatchingPattern(ctx, tx, scope, banner.ID, info.TrackerType, info.Identifier) if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) { return fmt.Errorf("cannot find matching tracker pattern: %w", err) @@ -2138,10 +2217,12 @@ func (s *Service) reportDetectedTracker( CreatedAt: now, UpdatedAt: now, } + wasInserted, err := newPattern.InsertIfNotExists(ctx, tx, scope) if err != nil { return fmt.Errorf("cannot insert tracker pattern: %w", err) } + if wasInserted { patternID = &newPattern.ID *inserted++ @@ -2150,11 +2231,13 @@ func (s *Service) reportDetectedTracker( if err := existingPattern.FindMatchingPattern(ctx, tx, scope, banner.ID, info.TrackerType, info.Identifier); err != nil { return fmt.Errorf("cannot load existing tracker pattern: %w", err) } + patternID = &existingPattern.ID } } var initiatorDomain *string + if info.InitiatorURL != nil { if domain := uri.ExtractDomain(*info.InitiatorURL); domain != "" { initiatorDomain = &domain @@ -2199,6 +2282,7 @@ func (s *Service) reportDetectedResource( } origin := u.Scheme + "://" + u.Host + path := u.Path if path == "" { path = "/" @@ -2246,6 +2330,7 @@ func (s *Service) CreateTrackerPattern( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrCategoryNotFound } + return fmt.Errorf("cannot load cookie category: %w", err) } @@ -2271,6 +2356,7 @@ func (s *Service) CreateTrackerPattern( if errors.Is(err, coredata.ErrResourceAlreadyExists) { return ErrPatternAlreadyExists } + return fmt.Errorf("cannot insert tracker pattern: %w", err) } @@ -2321,10 +2407,13 @@ func (s *Service) CountTrackerPatternsForCategory( err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { - var patterns coredata.TrackerPatterns - var err error + var ( + patterns coredata.TrackerPatterns + err error + ) count, err = patterns.CountByCookieCategoryID(ctx, conn, scope, categoryID) + return err }, ) @@ -2349,6 +2438,7 @@ func (s *Service) GetTrackerPattern( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrTrackerPatternNotFound } + return fmt.Errorf("cannot load tracker pattern: %w", err) } @@ -2380,6 +2470,7 @@ func (s *Service) UpdateTrackerPattern( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrTrackerPatternNotFound } + return fmt.Errorf("cannot load tracker pattern: %w", err) } @@ -2396,9 +2487,11 @@ func (s *Service) UpdateTrackerPattern( if req.MaxAgeSeconds != nil { pattern.MaxAgeSeconds = *req.MaxAgeSeconds } + if req.Description != nil { pattern.Description = *req.Description } + if req.Excluded != nil { pattern.Excluded = *req.Excluded } @@ -2438,6 +2531,7 @@ func (s *Service) DeleteTrackerPattern( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrTrackerPatternNotFound } + return fmt.Errorf("cannot load tracker pattern: %w", err) } @@ -2473,6 +2567,7 @@ func (s *Service) MoveTrackerPatternToCategory( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrTrackerPatternNotFound } + return fmt.Errorf("cannot load tracker pattern: %w", err) } @@ -2481,6 +2576,7 @@ func (s *Service) MoveTrackerPatternToCategory( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrCategoryNotFound } + return fmt.Errorf("cannot load target cookie category: %w", err) } @@ -2562,8 +2658,10 @@ func (s *Service) CountUncategorisedTrackerPatterns( err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { - var patterns coredata.TrackerPatterns - var err error + var ( + patterns coredata.TrackerPatterns + err error + ) count, err = patterns.CountUncategorisedByCookieBannerID(ctx, conn, scope, bannerID, filter) if err != nil { @@ -2590,8 +2688,10 @@ func (s *Service) CountDetectedTrackersByPatternID( err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { - var trackers coredata.DetectedTrackers - var err error + var ( + trackers coredata.DetectedTrackers + err error + ) count, err = trackers.CountByTrackerPatternID(ctx, conn, scope, trackerPatternID) if err != nil { @@ -2627,6 +2727,7 @@ func (s *Service) CreateTrackerResource( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrCategoryNotFound } + return fmt.Errorf("cannot load cookie category: %w", err) } @@ -2650,6 +2751,7 @@ func (s *Service) CreateTrackerResource( if errors.Is(err, coredata.ErrResourceAlreadyExists) { return ErrResourceAlreadyExists } + return fmt.Errorf("cannot insert tracker resource: %w", err) } @@ -2677,6 +2779,7 @@ func (s *Service) GetTrackerResource( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrTrackerResourceNotFound } + return fmt.Errorf("cannot load tracker resource: %w", err) } @@ -2708,6 +2811,7 @@ func (s *Service) UpdateTrackerResource( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrTrackerResourceNotFound } + return fmt.Errorf("cannot load tracker resource: %w", err) } @@ -2722,9 +2826,11 @@ func (s *Service) UpdateTrackerResource( if req.DisplayName != nil { resource.DisplayName = *req.DisplayName } + if req.Description != nil { resource.Description = *req.Description } + if req.Excluded != nil { resource.Excluded = *req.Excluded } @@ -2758,6 +2864,7 @@ func (s *Service) DeleteTrackerResource( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrTrackerResourceNotFound } + return fmt.Errorf("cannot load tracker resource: %w", err) } @@ -2785,6 +2892,7 @@ func (s *Service) MoveTrackerResourceToCategory( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrTrackerResourceNotFound } + return fmt.Errorf("cannot load tracker resource: %w", err) } @@ -2793,6 +2901,7 @@ func (s *Service) MoveTrackerResourceToCategory( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrCategoryNotFound } + return fmt.Errorf("cannot load target cookie category: %w", err) } @@ -2860,10 +2969,13 @@ func (s *Service) CountTrackerResourcesForCategory( err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { - var resources coredata.TrackerResources - var err error + var ( + resources coredata.TrackerResources + err error + ) count, err = resources.CountByCookieCategoryID(ctx, conn, scope, categoryID) + return err }, ) @@ -2911,8 +3023,10 @@ func (s *Service) CountUncategorisedTrackerResources( err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { - var resources coredata.TrackerResources - var err error + var ( + resources coredata.TrackerResources + err error + ) count, err = resources.CountUncategorisedByCookieBannerID(ctx, conn, scope, bannerID, filter) if err != nil { diff --git a/pkg/cookiebanner/service_test.go b/pkg/cookiebanner/service_test.go index 1747092a2..2776b2283 100644 --- a/pkg/cookiebanner/service_test.go +++ b/pkg/cookiebanner/service_test.go @@ -30,6 +30,7 @@ func TestSnapshotsEqual(t *testing.T) { baseSnapshot := func() coredata.CookieBannerVersionSnapshot { policy := "https://example.com/privacy" maxAge := 3600 + return coredata.CookieBannerVersionSnapshot{ PrivacyPolicyURL: &policy, CookiePolicyURL: "https://example.com/cookies", diff --git a/pkg/cookiebanner/snapshot.go b/pkg/cookiebanner/snapshot.go index 0e83b311b..624eff6f3 100644 --- a/pkg/cookiebanner/snapshot.go +++ b/pkg/cookiebanner/snapshot.go @@ -65,6 +65,7 @@ func sortConsentCategories(categories coredata.CookieCategories) { if d := snapshotCategoryKindOrder(a.Kind) - snapshotCategoryKindOrder(b.Kind); d != 0 { return d } + return bytes.Compare(a.ID[:], b.ID[:]) }) } @@ -77,10 +78,12 @@ func buildSnapshot( sortConsentCategories(categories) cookiesByCategory := make(map[gid.GID]coredata.CookieItems) + for _, p := range allPatterns { if p.TrackerType != coredata.TrackerTypeCookie { continue } + cookiesByCategory[p.CookieCategoryID] = append( cookiesByCategory[p.CookieCategoryID], coredata.CookieItem{ @@ -97,10 +100,12 @@ func buildSnapshot( if cookies == nil { cookies = coredata.CookieItems{} } + gcmConsentTypes := c.GCMConsentTypes if gcmConsentTypes == nil { gcmConsentTypes = []string{} } + snapshotCategories[i] = coredata.CookieBannerVersionSnapshotCategory{ Name: c.Name, Slug: c.Slug, @@ -138,15 +143,19 @@ func buildSnapshotTranslations( Description string `json:"description"` } `json:"categories"` } + _ = json.Unmarshal(t.Translations, &raw) ui := make(map[string]string) + var flat map[string]json.RawMessage + _ = json.Unmarshal(t.Translations, &flat) for k, v := range flat { if k == "categories" || k == "cookies" { continue } + var s string if json.Unmarshal(v, &s) == nil { ui[k] = s @@ -161,9 +170,11 @@ func buildSnapshotTranslations( Name: ct.Name, Description: ct.Description, } + continue } } + catTranslations[i] = coredata.CookieBannerVersionSnapshotCategoryTranslation{ Name: c.Name, Description: c.Description, diff --git a/pkg/cookiebanner/tracker_mapping_agent.go b/pkg/cookiebanner/tracker_mapping_agent.go index 765c98d80..133f95526 100644 --- a/pkg/cookiebanner/tracker_mapping_agent.go +++ b/pkg/cookiebanner/tracker_mapping_agent.go @@ -87,6 +87,7 @@ func buildTrackerMappingAgent( func trackerMappingInstructions(_ context.Context, _ *agent.Agent) string { categories := coredata.ThirdPartyCategories() + parts := make([]string, len(categories)) for i, c := range categories { parts[i] = string(c) diff --git a/pkg/cookiebanner/tracker_mapping_worker.go b/pkg/cookiebanner/tracker_mapping_worker.go index 7d4acb6a3..053e9055b 100644 --- a/pkg/cookiebanner/tracker_mapping_worker.go +++ b/pkg/cookiebanner/tracker_mapping_worker.go @@ -75,6 +75,7 @@ func (h *trackerMappingHandler) Claim(ctx context.Context) (coredata.TrackerPatt if errors.Is(err, coredata.ErrResourceNotFound) { return coredata.TrackerPattern{}, worker.ErrNoTask } + return coredata.TrackerPattern{}, fmt.Errorf("cannot claim tracker mapping task: %w", err) } @@ -85,9 +86,11 @@ func (h *trackerMappingHandler) Process(ctx context.Context, tp coredata.Tracker return h.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { - var commonPatternID *gid.GID - var thirdPartyID *gid.GID - var err error + var ( + commonPatternID *gid.GID + thirdPartyID *gid.GID + err error + ) commonPatternID, thirdPartyID, err = h.matchByPattern(ctx, tx, tp) if err != nil { @@ -143,12 +146,15 @@ func (h *trackerMappingHandler) matchByPattern( if errors.Is(err, coredata.ErrResourceNotFound) { return nil, nil, nil } + return nil, nil, fmt.Errorf("cannot load common tracker pattern: %w", err) } var thirdPartyID *gid.GID + if commonPattern.CommonThirdPartyID != nil { var err error + thirdPartyID, err = h.resolveThirdParty(ctx, conn, tp, &commonPattern) if err != nil { return nil, nil, fmt.Errorf("cannot resolve third party from pattern match: %w", err) @@ -164,6 +170,7 @@ func (h *trackerMappingHandler) matchByDomain( tp coredata.TrackerPattern, ) (*gid.GID, *gid.GID, error) { var trackers coredata.DetectedTrackers + domains, err := trackers.LoadInitiatorDomainsByTrackerPatternID(ctx, tx, tp.ID, 10) if err != nil { return nil, nil, fmt.Errorf("cannot load initiator domains: %w", err) @@ -174,6 +181,7 @@ func (h *trackerMappingHandler) matchByDomain( } filter := coredata.NewCommonThirdPartyDomainFilter(domains) + var matchedDomains coredata.CommonThirdPartyDomains if err := matchedDomains.Load(ctx, tx, 1, filter); err != nil { return nil, nil, fmt.Errorf("cannot load common third party domain by domain match: %w", err) @@ -217,6 +225,7 @@ func (h *trackerMappingHandler) identifyWithAgent( tp coredata.TrackerPattern, ) (*gid.GID, *gid.GID, error) { var trackers coredata.DetectedTrackers + domains, err := trackers.LoadInitiatorDomainsByTrackerPatternID(ctx, tx, tp.ID, 5) if err != nil { h.logger.WarnCtx(ctx, "cannot load initiator domains for agent", log.Error(err)) @@ -244,6 +253,7 @@ func (h *trackerMappingHandler) identifyWithAgent( log.Error(err), log.String("pattern", tp.Pattern), ) + return nil, nil, nil } @@ -256,6 +266,7 @@ func (h *trackerMappingHandler) identifyWithAgent( log.String("pattern", tp.Pattern), log.Float64("confidence", identification.Confidence), ) + return nil, nil, nil } @@ -420,6 +431,7 @@ func (h *trackerMappingHandler) resolveThirdParty( if errors.Is(err, coredata.ErrResourceNotFound) { return nil, nil } + return nil, fmt.Errorf("cannot resolve third party: %w", err) } diff --git a/pkg/coredata/access_entry.go b/pkg/coredata/access_entry.go index 3426e38ba..b87a06245 100644 --- a/pkg/coredata/access_entry.go +++ b/pkg/coredata/access_entry.go @@ -77,6 +77,7 @@ func (e *AccessEntry) AuthorizationAttributes(ctx context.Context, conn pg.Queri if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query access entry authorization attributes: %w", err) } @@ -139,6 +140,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect access entry: %w", err) } @@ -243,6 +245,7 @@ VALUES ( "created_at": e.CreatedAt, "updated_at": e.UpdatedAt, } + _, err := conn.Exec(ctx, q, args) if err != nil { return fmt.Errorf("cannot insert access_entry: %w", err) @@ -529,6 +532,7 @@ func (e *AccessEntry) LoadOrganizationID( if errors.Is(err, pgx.ErrNoRows) { return gid.GID{}, ErrResourceNotFound } + return gid.GID{}, fmt.Errorf("cannot load organization id for access entry: %w", err) } @@ -731,13 +735,16 @@ WHERE %s defer rows.Close() var result []BaselineAccountEntry + for rows.Next() { var entry BaselineAccountEntry if err := rows.Scan(&entry.AccountKey, &entry.Email, &entry.FullName); err != nil { return nil, fmt.Errorf("cannot scan baseline entry: %w", err) } + result = append(result, entry) } + if err := rows.Err(); err != nil { return nil, fmt.Errorf("cannot iterate baseline entries: %w", err) } @@ -795,13 +802,16 @@ ORDER BY defer rows.Close() var result []MembershipAccount + for rows.Next() { var account MembershipAccount if err := rows.Scan(&account.ID, &account.Email, &account.FullName, &account.State, &account.Role, &account.CreatedAt); err != nil { return nil, fmt.Errorf("cannot scan membership account: %w", err) } + result = append(result, account) } + if err := rows.Err(); err != nil { return nil, fmt.Errorf("cannot iterate membership accounts: %w", err) } diff --git a/pkg/coredata/access_entry_account_type.go b/pkg/coredata/access_entry_account_type.go index 599820d78..17972d0d4 100644 --- a/pkg/coredata/access_entry_account_type.go +++ b/pkg/coredata/access_entry_account_type.go @@ -39,6 +39,7 @@ func (a AccessEntryAccountType) String() string { func (a *AccessEntryAccountType) Scan(value any) error { var str string + switch v := value.(type) { case string: str = v @@ -56,6 +57,7 @@ func (a *AccessEntryAccountType) Scan(value any) error { default: return fmt.Errorf("cannot parse AccessEntryAccountType: invalid value %q", str) } + return nil } diff --git a/pkg/coredata/access_entry_account_type_test.go b/pkg/coredata/access_entry_account_type_test.go index d982232c0..e3664f87b 100644 --- a/pkg/coredata/access_entry_account_type_test.go +++ b/pkg/coredata/access_entry_account_type_test.go @@ -36,17 +36,20 @@ func TestAccessEntryAccountTypeScan(t *testing.T) { t.Parallel() var got AccessEntryAccountType + err := got.Scan(tt.input) if tt.wantErr { if err == nil { t.Fatalf("Scan(%v) expected error", tt.input) } + return } if err != nil { t.Fatalf("Scan(%v) returned error: %v", tt.input, err) } + if got != tt.want { t.Fatalf("Scan(%v) = %q, want %q", tt.input, got, tt.want) } @@ -61,6 +64,7 @@ func TestAccessEntryAccountTypeValue(t *testing.T) { if err != nil { t.Fatalf("Value() returned error: %v", err) } + if got != "USER" { t.Fatalf("Value() = %q, want %q", got, "USER") } diff --git a/pkg/coredata/access_entry_decision.go b/pkg/coredata/access_entry_decision.go index e1a2de7fe..8afc94642 100644 --- a/pkg/coredata/access_entry_decision.go +++ b/pkg/coredata/access_entry_decision.go @@ -35,6 +35,7 @@ func (d AccessEntryDecision) String() string { func (d *AccessEntryDecision) Scan(value any) error { var str string + switch v := value.(type) { case string: str = v @@ -58,6 +59,7 @@ func (d *AccessEntryDecision) Scan(value any) error { default: return fmt.Errorf("cannot parse AccessEntryDecision: invalid value %q", str) } + return nil } diff --git a/pkg/coredata/access_entry_decision_history.go b/pkg/coredata/access_entry_decision_history.go index 7e0305e2a..73be2fbf0 100644 --- a/pkg/coredata/access_entry_decision_history.go +++ b/pkg/coredata/access_entry_decision_history.go @@ -100,6 +100,7 @@ func (h *AccessEntryDecisionHistory) AuthorizationAttributes( if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot load authorization attributes: %w", err) } diff --git a/pkg/coredata/access_entry_decision_test.go b/pkg/coredata/access_entry_decision_test.go index 67e90e627..1a7bc560c 100644 --- a/pkg/coredata/access_entry_decision_test.go +++ b/pkg/coredata/access_entry_decision_test.go @@ -39,17 +39,20 @@ func TestAccessEntryDecisionScan(t *testing.T) { t.Parallel() var got AccessEntryDecision + err := got.Scan(tt.input) if tt.wantErr { if err == nil { t.Fatalf("Scan(%v) expected error", tt.input) } + return } if err != nil { t.Fatalf("Scan(%v) returned error: %v", tt.input, err) } + if got != tt.want { t.Fatalf("Scan(%v) = %q, want %q", tt.input, got, tt.want) } @@ -78,6 +81,7 @@ func TestAccessEntryDecisionValue(t *testing.T) { if err != nil { t.Fatalf("Value() returned error: %v", err) } + if got != tt.want { t.Fatalf("Value() = %q, want %q", got, tt.want) } diff --git a/pkg/coredata/access_entry_filter.go b/pkg/coredata/access_entry_filter.go index c6c0d87a8..db3f6544c 100644 --- a/pkg/coredata/access_entry_filter.go +++ b/pkg/coredata/access_entry_filter.go @@ -89,18 +89,23 @@ func (f *AccessEntryFilter) SQLArguments() pgx.StrictNamedArgs { if f.Decision != nil { args["filter_decision"] = string(*f.Decision) } + if f.Flag != nil { args["filter_flag"] = string(*f.Flag) } + if f.IncrementalTag != nil { args["filter_incremental_tag"] = string(*f.IncrementalTag) } + if f.IsAdmin != nil { args["filter_is_admin"] = *f.IsAdmin } + if f.AuthMethod != nil { args["filter_auth_method"] = string(*f.AuthMethod) } + if f.AccountType != nil { args["filter_account_type"] = string(*f.AccountType) } diff --git a/pkg/coredata/access_entry_flag.go b/pkg/coredata/access_entry_flag.go index 4ac41990f..1404d7ee8 100644 --- a/pkg/coredata/access_entry_flag.go +++ b/pkg/coredata/access_entry_flag.go @@ -45,6 +45,7 @@ func (f AccessEntryFlag) String() string { func (f *AccessEntryFlag) Scan(value any) error { var str string + switch v := value.(type) { case string: str = v @@ -88,6 +89,7 @@ func (f *AccessEntryFlag) Scan(value any) error { default: return fmt.Errorf("cannot parse AccessEntryFlag: invalid value %q", str) } + return nil } diff --git a/pkg/coredata/access_entry_flag_test.go b/pkg/coredata/access_entry_flag_test.go index d1be9b9a1..60662052d 100644 --- a/pkg/coredata/access_entry_flag_test.go +++ b/pkg/coredata/access_entry_flag_test.go @@ -40,17 +40,20 @@ func TestAccessEntryFlagScan(t *testing.T) { t.Parallel() var got AccessEntryFlag + err := got.Scan(tt.input) if tt.wantErr { if err == nil { t.Fatalf("Scan(%v) expected error", tt.input) } + return } if err != nil { t.Fatalf("Scan(%v) returned error: %v", tt.input, err) } + if got != tt.want { t.Fatalf("Scan(%v) = %q, want %q", tt.input, got, tt.want) } @@ -65,6 +68,7 @@ func TestAccessEntryFlagValue(t *testing.T) { if err != nil { t.Fatalf("Value() returned error: %v", err) } + if got != "NONE" { t.Fatalf("Value() = %q, want %q", got, "NONE") } diff --git a/pkg/coredata/access_entry_incremental_tag.go b/pkg/coredata/access_entry_incremental_tag.go index dddfc54d1..f3ba65701 100644 --- a/pkg/coredata/access_entry_incremental_tag.go +++ b/pkg/coredata/access_entry_incremental_tag.go @@ -33,6 +33,7 @@ func (t AccessEntryIncrementalTag) String() string { func (t *AccessEntryIncrementalTag) Scan(value any) error { var str string + switch v := value.(type) { case string: str = v diff --git a/pkg/coredata/access_entry_incremental_tag_test.go b/pkg/coredata/access_entry_incremental_tag_test.go index 6133483c8..c91d8b71b 100644 --- a/pkg/coredata/access_entry_incremental_tag_test.go +++ b/pkg/coredata/access_entry_incremental_tag_test.go @@ -37,17 +37,20 @@ func TestAccessEntryIncrementalTagScan(t *testing.T) { t.Parallel() var got AccessEntryIncrementalTag + err := got.Scan(tt.input) if tt.wantErr { if err == nil { t.Fatalf("Scan(%v) expected error", tt.input) } + return } if err != nil { t.Fatalf("Scan(%v) returned error: %v", tt.input, err) } + if got != tt.want { t.Fatalf("Scan(%v) = %q, want %q", tt.input, got, tt.want) } @@ -62,6 +65,7 @@ func TestAccessEntryIncrementalTagValue(t *testing.T) { if err != nil { t.Fatalf("Value() returned error: %v", err) } + if got != "NEW" { t.Fatalf("Value() = %q, want %q", got, "NEW") } diff --git a/pkg/coredata/access_entry_order_field.go b/pkg/coredata/access_entry_order_field.go index 1e6b40903..d38282dc7 100644 --- a/pkg/coredata/access_entry_order_field.go +++ b/pkg/coredata/access_entry_order_field.go @@ -29,6 +29,7 @@ func (p AccessEntryOrderField) Column() string { case AccessEntryOrderFieldCreatedAt: return "created_at" } + panic(fmt.Sprintf("unsupported order by: %s", p)) } @@ -37,6 +38,7 @@ func (p AccessEntryOrderField) IsValid() bool { case AccessEntryOrderFieldCreatedAt: return true } + return false } @@ -53,5 +55,6 @@ func (p *AccessEntryOrderField) UnmarshalText(text []byte) error { if !p.IsValid() { return fmt.Errorf("%s is not a valid AccessEntryOrderField", string(text)) } + return nil } diff --git a/pkg/coredata/access_entry_statistics.go b/pkg/coredata/access_entry_statistics.go index 76178ee9e..60408b4dd 100644 --- a/pkg/coredata/access_entry_statistics.go +++ b/pkg/coredata/access_entry_statistics.go @@ -62,14 +62,19 @@ GROUP BY decision; defer rows.Close() for rows.Next() { - var decision AccessEntryDecision - var count int + var ( + decision AccessEntryDecision + count int + ) + if err := rows.Scan(&decision, &count); err != nil { return fmt.Errorf("cannot scan decision count: %w", err) } + s.DecisionCounts[decision] = count s.TotalCount += count } + if err := rows.Err(); err != nil { return fmt.Errorf("cannot iterate decision counts: %w", err) } @@ -91,13 +96,18 @@ GROUP BY f; defer rows.Close() for rows.Next() { - var flag AccessEntryFlag - var count int + var ( + flag AccessEntryFlag + count int + ) + if err := rows.Scan(&flag, &count); err != nil { return fmt.Errorf("cannot scan flag count: %w", err) } + s.FlagCounts[flag] = count } + if err := rows.Err(); err != nil { return fmt.Errorf("cannot iterate flag counts: %w", err) } @@ -119,13 +129,18 @@ GROUP BY incremental_tag; defer rows.Close() for rows.Next() { - var tag AccessEntryIncrementalTag - var count int + var ( + tag AccessEntryIncrementalTag + count int + ) + if err := rows.Scan(&tag, &count); err != nil { return fmt.Errorf("cannot scan incremental tag count: %w", err) } + s.IncrementalTagCounts[tag] = count } + if err := rows.Err(); err != nil { return fmt.Errorf("cannot iterate incremental tag counts: %w", err) } @@ -169,14 +184,19 @@ GROUP BY decision; defer rows.Close() for rows.Next() { - var decision AccessEntryDecision - var count int + var ( + decision AccessEntryDecision + count int + ) + if err := rows.Scan(&decision, &count); err != nil { return fmt.Errorf("cannot scan decision count: %w", err) } + s.DecisionCounts[decision] = count s.TotalCount += count } + if err := rows.Err(); err != nil { return fmt.Errorf("cannot iterate decision counts: %w", err) } @@ -199,13 +219,18 @@ GROUP BY f; defer rows.Close() for rows.Next() { - var flag AccessEntryFlag - var count int + var ( + flag AccessEntryFlag + count int + ) + if err := rows.Scan(&flag, &count); err != nil { return fmt.Errorf("cannot scan flag count: %w", err) } + s.FlagCounts[flag] = count } + if err := rows.Err(); err != nil { return fmt.Errorf("cannot iterate flag counts: %w", err) } @@ -228,13 +253,18 @@ GROUP BY incremental_tag; defer rows.Close() for rows.Next() { - var tag AccessEntryIncrementalTag - var count int + var ( + tag AccessEntryIncrementalTag + count int + ) + if err := rows.Scan(&tag, &count); err != nil { return fmt.Errorf("cannot scan incremental tag count: %w", err) } + s.IncrementalTagCounts[tag] = count } + if err := rows.Err(); err != nil { return fmt.Errorf("cannot iterate incremental tag counts: %w", err) } diff --git a/pkg/coredata/access_entry_upsert_test.go b/pkg/coredata/access_entry_upsert_test.go index ab3c8af89..d6639d5fb 100644 --- a/pkg/coredata/access_entry_upsert_test.go +++ b/pkg/coredata/access_entry_upsert_test.go @@ -52,19 +52,23 @@ func newTestPgClient(t *testing.T) *pg.Client { // registry every time to avoid "duplicate collector" panics when tests // run in parallel. opts := []pg.Option{pg.WithRegisterer(prometheus.NewRegistry())} + if u.Host != "" { host := u.Host if u.Port() == "" { host = net.JoinHostPort(u.Hostname(), "5432") } + opts = append(opts, pg.WithAddr(host)) } + if u.User != nil { opts = append(opts, pg.WithUser(u.User.Username())) if password, ok := u.User.Password(); ok { opts = append(opts, pg.WithPassword(password)) } } + if len(u.Path) > 1 { opts = append(opts, pg.WithDatabase(u.Path[1:])) } @@ -146,15 +150,19 @@ func seedAccessEntryFixture(t *testing.T, ctx context.Context, client *pg.Client if _, err := tx.Exec(ctx, `DELETE FROM access_entries WHERE access_review_campaign_id = $1`, campaignID); err != nil { return err } + if _, err := tx.Exec(ctx, `DELETE FROM access_review_campaigns WHERE id = $1`, campaignID); err != nil { return err } + if _, err := tx.Exec(ctx, `DELETE FROM access_sources WHERE id = $1`, sourceID); err != nil { return err } + if _, err := tx.Exec(ctx, `DELETE FROM organizations WHERE id = $1`, organizationID); err != nil { return err } + return nil }) }) @@ -231,6 +239,7 @@ func TestAccessEntry_Upsert_FreezesDecidedFields(t *testing.T) { DecidedAt: &decisionTime, UpdatedAt: decisionTime, } + require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { return decided.Update(ctx, tx, fx.scope) })) @@ -267,12 +276,14 @@ func TestAccessEntry_Upsert_FreezesDecidedFields(t *testing.T) { CreatedAt: t2, UpdatedAt: t2, } + require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { return refresh.Upsert(ctx, tx, fx.scope) })) // Step 4: Load and assert the freeze semantics. loaded := &coredata.AccessEntry{} + require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { return loaded.LoadByID(ctx, conn, fx.scope, entryID) })) @@ -341,6 +352,7 @@ func TestAccessEntry_Upsert_RefreshesSourceTrackingFields(t *testing.T) { CreatedAt: t0, UpdatedAt: t0, } + require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { return first.Upsert(ctx, tx, fx.scope) })) @@ -366,11 +378,13 @@ func TestAccessEntry_Upsert_RefreshesSourceTrackingFields(t *testing.T) { CreatedAt: t1, UpdatedAt: t1, } + require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { return second.Upsert(ctx, tx, fx.scope) })) loaded := &coredata.AccessEntry{} + require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { return loaded.LoadByID(ctx, conn, fx.scope, entryID) })) @@ -428,11 +442,13 @@ func TestAccessEntry_Upsert_InsertsActiveAccount(t *testing.T) { CreatedAt: t0, UpdatedAt: t0, } + require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { return entry.Upsert(ctx, tx, fx.scope) })) loaded := &coredata.AccessEntry{} + require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { return loaded.LoadByID(ctx, conn, fx.scope, entryID) })) diff --git a/pkg/coredata/access_review_campaign.go b/pkg/coredata/access_review_campaign.go index d68def4a7..dab2f9944 100644 --- a/pkg/coredata/access_review_campaign.go +++ b/pkg/coredata/access_review_campaign.go @@ -61,6 +61,7 @@ func (c *AccessReviewCampaign) AuthorizationAttributes(ctx context.Context, conn if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query access review campaign authorization attributes: %w", err) } @@ -107,6 +108,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect access review campaign: %w", err) } @@ -163,6 +165,7 @@ VALUES ( "created_at": c.CreatedAt, "updated_at": c.UpdatedAt, } + _, err := conn.Exec(ctx, q, args) if err != nil { return fmt.Errorf("cannot insert access_review_campaign: %w", err) @@ -357,6 +360,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect access review campaign: %w", err) } diff --git a/pkg/coredata/access_review_campaign_order_field.go b/pkg/coredata/access_review_campaign_order_field.go index 8a8e296e6..096e347f0 100644 --- a/pkg/coredata/access_review_campaign_order_field.go +++ b/pkg/coredata/access_review_campaign_order_field.go @@ -29,6 +29,7 @@ func (p AccessReviewCampaignOrderField) Column() string { case AccessReviewCampaignOrderFieldCreatedAt: return "created_at" } + panic(fmt.Sprintf("unsupported order by: %s", p)) } @@ -37,6 +38,7 @@ func (p AccessReviewCampaignOrderField) IsValid() bool { case AccessReviewCampaignOrderFieldCreatedAt: return true } + return false } @@ -53,5 +55,6 @@ func (p *AccessReviewCampaignOrderField) UnmarshalText(text []byte) error { if !p.IsValid() { return fmt.Errorf("%s is not a valid AccessReviewCampaignOrderField", string(text)) } + return nil } diff --git a/pkg/coredata/access_review_campaign_scope_system.go b/pkg/coredata/access_review_campaign_scope_system.go index 6fce5c3d7..5483adee5 100644 --- a/pkg/coredata/access_review_campaign_scope_system.go +++ b/pkg/coredata/access_review_campaign_scope_system.go @@ -127,6 +127,7 @@ FOR UPDATE if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot lock campaign: %w", err) } diff --git a/pkg/coredata/access_review_campaign_source_fetch.go b/pkg/coredata/access_review_campaign_source_fetch.go index f0c2fb1f4..ba5be7ef5 100644 --- a/pkg/coredata/access_review_campaign_source_fetch.go +++ b/pkg/coredata/access_review_campaign_source_fetch.go @@ -97,6 +97,7 @@ INSERT INTO access_review_campaign_source_fetches ( "created_at": f.CreatedAt, "updated_at": f.UpdatedAt, } + _, err := conn.Exec(ctx, q, args) if err != nil { return fmt.Errorf("cannot insert campaign source fetch: %w", err) @@ -197,6 +198,7 @@ LIMIT 1 if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect campaign source fetch: %w", err) } @@ -293,6 +295,7 @@ FOR UPDATE SKIP LOCKED if errors.Is(err, pgx.ErrNoRows) { return ErrNoAccessReviewCampaignSourceFetchAvailable } + return fmt.Errorf("cannot collect campaign source fetch: %w", err) } diff --git a/pkg/coredata/access_review_campaign_source_fetch_status.go b/pkg/coredata/access_review_campaign_source_fetch_status.go index cd09685f7..68635f1d8 100644 --- a/pkg/coredata/access_review_campaign_source_fetch_status.go +++ b/pkg/coredata/access_review_campaign_source_fetch_status.go @@ -38,6 +38,7 @@ func (s AccessReviewCampaignSourceFetchStatus) String() string { func (s *AccessReviewCampaignSourceFetchStatus) Scan(value any) error { var str string + switch v := value.(type) { case string: str = v diff --git a/pkg/coredata/access_review_campaign_source_fetch_status_test.go b/pkg/coredata/access_review_campaign_source_fetch_status_test.go index 28d9fd147..31680f435 100644 --- a/pkg/coredata/access_review_campaign_source_fetch_status_test.go +++ b/pkg/coredata/access_review_campaign_source_fetch_status_test.go @@ -22,12 +22,15 @@ func TestAccessReviewCampaignSourceFetchStatusIsTerminal(t *testing.T) { if AccessReviewCampaignSourceFetchStatusQueued.IsTerminal() { t.Fatalf("QUEUED should not be terminal") } + if AccessReviewCampaignSourceFetchStatusFetching.IsTerminal() { t.Fatalf("FETCHING should not be terminal") } + if !AccessReviewCampaignSourceFetchStatusSuccess.IsTerminal() { t.Fatalf("SUCCESS should be terminal") } + if !AccessReviewCampaignSourceFetchStatusFailed.IsTerminal() { t.Fatalf("FAILED should be terminal") } @@ -64,17 +67,20 @@ func TestAccessReviewCampaignSourceFetchStatusScan(t *testing.T) { t.Parallel() var got AccessReviewCampaignSourceFetchStatus + err := got.Scan(tt.input) if tt.wantErr { if err == nil { t.Fatalf("Scan(%v) expected error", tt.input) } + return } if err != nil { t.Fatalf("Scan(%v) returned error: %v", tt.input, err) } + if got != tt.want { t.Fatalf("Scan(%v) = %q, want %q", tt.input, got, tt.want) } diff --git a/pkg/coredata/access_review_campaign_status.go b/pkg/coredata/access_review_campaign_status.go index a1892a8e8..267f90f66 100644 --- a/pkg/coredata/access_review_campaign_status.go +++ b/pkg/coredata/access_review_campaign_status.go @@ -35,6 +35,7 @@ func (s AccessReviewCampaignStatus) String() string { func (s *AccessReviewCampaignStatus) Scan(value any) error { var str string + switch v := value.(type) { case string: str = v @@ -58,6 +59,7 @@ func (s *AccessReviewCampaignStatus) Scan(value any) error { default: return fmt.Errorf("cannot parse AccessReviewCampaignStatus: invalid value %q", str) } + return nil } diff --git a/pkg/coredata/access_review_campaign_status_test.go b/pkg/coredata/access_review_campaign_status_test.go index 7fefc7f6a..9db1d7ac9 100644 --- a/pkg/coredata/access_review_campaign_status_test.go +++ b/pkg/coredata/access_review_campaign_status_test.go @@ -39,17 +39,20 @@ func TestAccessReviewCampaignStatusScan(t *testing.T) { t.Parallel() var got AccessReviewCampaignStatus + err := got.Scan(tt.input) if tt.wantErr { if err == nil { t.Fatalf("Scan(%v) expected error", tt.input) } + return } if err != nil { t.Fatalf("Scan(%v) returned error: %v", tt.input, err) } + if got != tt.want { t.Fatalf("Scan(%v) = %q, want %q", tt.input, got, tt.want) } @@ -78,6 +81,7 @@ func TestAccessReviewCampaignStatusValue(t *testing.T) { if err != nil { t.Fatalf("Value() returned error: %v", err) } + if got != tt.want { t.Fatalf("Value() = %q, want %q", got, tt.want) } diff --git a/pkg/coredata/access_source.go b/pkg/coredata/access_source.go index 023eccfc7..7d12f588b 100644 --- a/pkg/coredata/access_source.go +++ b/pkg/coredata/access_source.go @@ -60,6 +60,7 @@ func (as *AccessSource) AuthorizationAttributes(ctx context.Context, conn pg.Que if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query access source authorization attributes: %w", err) } @@ -105,6 +106,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect access source: %w", err) } @@ -158,6 +160,7 @@ VALUES ( "created_at": as.CreatedAt, "updated_at": as.UpdatedAt, } + _, err := conn.Exec(ctx, q, args) if err != nil { return fmt.Errorf("cannot insert access_source: %w", err) @@ -426,9 +429,11 @@ FOR UPDATE SKIP LOCKED; if errors.Is(err, pgx.ErrNoRows) { return ErrNoAccessSourceNameSyncAvailable } + return fmt.Errorf("cannot collect unsynced access source: %w", err) } *as = row + return nil } diff --git a/pkg/coredata/access_source_category.go b/pkg/coredata/access_source_category.go index 2a2b248bd..9ff8ed5f6 100644 --- a/pkg/coredata/access_source_category.go +++ b/pkg/coredata/access_source_category.go @@ -43,6 +43,7 @@ func (c AccessSourceCategory) String() string { func (c *AccessSourceCategory) Scan(value any) error { var str string + switch v := value.(type) { case string: str = v @@ -64,6 +65,7 @@ func (c *AccessSourceCategory) Scan(value any) error { default: return fmt.Errorf("cannot parse AccessSourceCategory: invalid value %q", str) } + return nil } diff --git a/pkg/coredata/access_source_category_test.go b/pkg/coredata/access_source_category_test.go index b62f6946e..13010e53c 100644 --- a/pkg/coredata/access_source_category_test.go +++ b/pkg/coredata/access_source_category_test.go @@ -38,17 +38,20 @@ func TestAccessSourceCategoryScan(t *testing.T) { t.Parallel() var got AccessSourceCategory + err := got.Scan(tt.input) if tt.wantErr { if err == nil { t.Fatalf("Scan(%v) expected error", tt.input) } + return } if err != nil { t.Fatalf("Scan(%v) returned error: %v", tt.input, err) } + if got != tt.want { t.Fatalf("Scan(%v) = %q, want %q", tt.input, got, tt.want) } @@ -63,6 +66,7 @@ func TestAccessSourceCategoryValue(t *testing.T) { if err != nil { t.Fatalf("Value() returned error: %v", err) } + if got != "SAAS" { t.Fatalf("Value() = %q, want %q", got, "SAAS") } diff --git a/pkg/coredata/access_source_order_field.go b/pkg/coredata/access_source_order_field.go index 944c414e9..3efa157e1 100644 --- a/pkg/coredata/access_source_order_field.go +++ b/pkg/coredata/access_source_order_field.go @@ -29,6 +29,7 @@ func (p AccessSourceOrderField) Column() string { case AccessSourceOrderFieldCreatedAt: return "created_at" } + panic(fmt.Sprintf("unsupported order by: %s", p)) } @@ -37,6 +38,7 @@ func (p AccessSourceOrderField) IsValid() bool { case AccessSourceOrderFieldCreatedAt: return true } + return false } @@ -53,5 +55,6 @@ func (p *AccessSourceOrderField) UnmarshalText(text []byte) error { if !p.IsValid() { return fmt.Errorf("%s is not a valid AccessSourceOrderField", string(text)) } + return nil } diff --git a/pkg/coredata/agent_run.go b/pkg/coredata/agent_run.go index ba55ace9d..4a7a18a2f 100644 --- a/pkg/coredata/agent_run.go +++ b/pkg/coredata/agent_run.go @@ -88,6 +88,7 @@ func (e *AgentRun) AuthorizationAttributes(ctx context.Context, conn pg.Querier) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot load agent run authorization attributes: %w", err) } @@ -138,6 +139,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot load agent run: %w", err) } @@ -191,6 +193,7 @@ FOR UPDATE; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot load agent run: %w", err) } @@ -485,6 +488,7 @@ FOR UPDATE SKIP LOCKED; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot load pending agent run: %w", err) } @@ -584,6 +588,7 @@ func NewPGCheckpointer(pgClient *pg.Client, opts ...PGCheckpointerOption) *PGChe for _, opt := range opts { opt(s) } + return s } @@ -661,6 +666,7 @@ WHERE if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot load checkpoint: %w", err) } diff --git a/pkg/coredata/agent_run_order_field.go b/pkg/coredata/agent_run_order_field.go index 96eaa6d74..56fe36556 100644 --- a/pkg/coredata/agent_run_order_field.go +++ b/pkg/coredata/agent_run_order_field.go @@ -38,6 +38,7 @@ func (p AgentRunOrderField) IsValid() bool { case AgentRunOrderFieldCreatedAt: return true } + return false } @@ -50,6 +51,7 @@ func (p *AgentRunOrderField) UnmarshalText(text []byte) error { if !p.IsValid() { return fmt.Errorf("%s is not a valid AgentRunOrderField", string(text)) } + return nil } diff --git a/pkg/coredata/applicability_statement.go b/pkg/coredata/applicability_statement.go index 30c575fa8..86d601f2a 100644 --- a/pkg/coredata/applicability_statement.go +++ b/pkg/coredata/applicability_statement.go @@ -65,6 +65,7 @@ func (s *ApplicabilityStatement) AuthorizationAttributes(ctx context.Context, co if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query applicability statement authorization attributes: %w", err) } @@ -131,6 +132,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect applicability statement: %w", err) } @@ -194,10 +196,12 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect applicability statement: %w", err) } *sac = control + return nil } @@ -243,8 +247,8 @@ VALUES ( "created_at": sac.CreatedAt, "updated_at": sac.UpdatedAt, } - _, err := conn.Exec(ctx, q, args) + _, err := conn.Exec(ctx, q, args) if err != nil { var pgErr *pgconn.PgError if errors.As(err, &pgErr) { @@ -357,6 +361,7 @@ WHERE statement_of_applicability_id IN (SELECT id FROM current_soa) q = fmt.Sprintf(q, scope.SQLFragment()) _, err := conn.Exec(ctx, q, args) + return err } @@ -453,6 +458,7 @@ WHERE } *sacs = controls + return nil } @@ -503,6 +509,7 @@ ORDER BY } *sacs = controls + return nil } @@ -599,5 +606,6 @@ WHERE } *sacs = controls + return nil } diff --git a/pkg/coredata/applicability_statement_order_field.go b/pkg/coredata/applicability_statement_order_field.go index 2c9f73839..3fc7eca53 100644 --- a/pkg/coredata/applicability_statement_order_field.go +++ b/pkg/coredata/applicability_statement_order_field.go @@ -52,5 +52,6 @@ func (p *ApplicabilityStatementOrderField) UnmarshalText(text []byte) error { *p = ApplicabilityStatementOrderField(val) return nil } + return fmt.Errorf("invalid ApplicabilityStatementOrderField value: %q", val) } diff --git a/pkg/coredata/asset.go b/pkg/coredata/asset.go index e60dbb72b..e6f93f243 100644 --- a/pkg/coredata/asset.go +++ b/pkg/coredata/asset.go @@ -63,6 +63,7 @@ func (a *Asset) AuthorizationAttributes(ctx context.Context, conn pg.Querier) (m if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query asset authorization attributes: %w", err) } @@ -446,6 +447,7 @@ WHERE if errors.Is(err, pgx.ErrNoRows) { return nil, nil } + if err != nil { return nil, fmt.Errorf("cannot get asset list document ID: %w", err) } diff --git a/pkg/coredata/audit.go b/pkg/coredata/audit.go index 843789410..12f89f5df 100644 --- a/pkg/coredata/audit.go +++ b/pkg/coredata/audit.go @@ -69,6 +69,7 @@ func (a *Audit) AuthorizationAttributes(ctx context.Context, conn pg.Querier) (m if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query audit authorization attributes: %w", err) } @@ -150,6 +151,7 @@ WHERE row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count audits: %w", err) @@ -553,6 +555,7 @@ WHERE row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count audits: %w", err) @@ -595,6 +598,7 @@ WHERE row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count audits: %w", err) diff --git a/pkg/coredata/audit_filter.go b/pkg/coredata/audit_filter.go index fb1641751..0bd84a718 100644 --- a/pkg/coredata/audit_filter.go +++ b/pkg/coredata/audit_filter.go @@ -45,6 +45,7 @@ func (f *AuditFilter) SQLArguments() pgx.NamedArgs { for i, v := range f.trustCenterVisibilities { visibilities[i] = v.String() } + args["trust_center_visibilities"] = visibilities } diff --git a/pkg/coredata/audit_log_actor_type.go b/pkg/coredata/audit_log_actor_type.go index 13011b6e9..97cb2cf48 100644 --- a/pkg/coredata/audit_log_actor_type.go +++ b/pkg/coredata/audit_log_actor_type.go @@ -36,6 +36,7 @@ func (a AuditLogActorType) IsValid() bool { case AuditLogActorTypeUser, AuditLogActorTypeAPIKey, AuditLogActorTypeSystem: return true } + return false } @@ -48,11 +49,13 @@ func (a *AuditLogActorType) UnmarshalText(text []byte) error { if !a.IsValid() { return fmt.Errorf("%s is not a valid AuditLogActorType", string(text)) } + return nil } func (a *AuditLogActorType) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v diff --git a/pkg/coredata/audit_log_entry.go b/pkg/coredata/audit_log_entry.go index 1118175e2..4c69a24ba 100644 --- a/pkg/coredata/audit_log_entry.go +++ b/pkg/coredata/audit_log_entry.go @@ -61,6 +61,7 @@ func (e *AuditLogEntry) AuthorizationAttributes(ctx context.Context, conn pg.Que if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query audit log entry authorization attributes: %w", err) } @@ -159,10 +160,12 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect audit log entry: %w", err) } *e = entry + return nil } @@ -211,6 +214,7 @@ WHERE } *es = entries + return nil } diff --git a/pkg/coredata/audit_log_entry_order_field.go b/pkg/coredata/audit_log_entry_order_field.go index c4645ee09..9cf304572 100644 --- a/pkg/coredata/audit_log_entry_order_field.go +++ b/pkg/coredata/audit_log_entry_order_field.go @@ -29,6 +29,7 @@ func (p AuditLogEntryOrderField) Column() string { case AuditLogEntryOrderFieldCreatedAt: return "created_at" } + panic(fmt.Sprintf("unsupported order by: %s", p)) } @@ -41,6 +42,7 @@ func (p AuditLogEntryOrderField) IsValid() bool { case AuditLogEntryOrderFieldCreatedAt: return true } + return false } @@ -53,5 +55,6 @@ func (p *AuditLogEntryOrderField) UnmarshalText(text []byte) error { if !p.IsValid() { return fmt.Errorf("%s is not a valid AuditLogEntryOrderField", string(text)) } + return nil } diff --git a/pkg/coredata/audit_order_field.go b/pkg/coredata/audit_order_field.go index 5db6e1f61..d1525f148 100644 --- a/pkg/coredata/audit_order_field.go +++ b/pkg/coredata/audit_order_field.go @@ -49,5 +49,6 @@ func (p *AuditOrderField) UnmarshalText(text []byte) error { *p = AuditOrderField(val) return nil } + return fmt.Errorf("invalid AuditOrderField value: %q", val) } diff --git a/pkg/coredata/audit_state.go b/pkg/coredata/audit_state.go index 121cc6083..227076c90 100644 --- a/pkg/coredata/audit_state.go +++ b/pkg/coredata/audit_state.go @@ -45,6 +45,7 @@ func (as AuditState) String() string { func (as *AuditState) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -68,6 +69,7 @@ func (as *AuditState) Scan(value any) error { default: return fmt.Errorf("invalid AuditState value: %q", s) } + return nil } diff --git a/pkg/coredata/auth_method.go b/pkg/coredata/auth_method.go index 15c276c91..3255362ab 100644 --- a/pkg/coredata/auth_method.go +++ b/pkg/coredata/auth_method.go @@ -45,6 +45,7 @@ func (a AccessEntryAuthMethod) String() string { func (a *AccessEntryAuthMethod) Scan(value any) error { var str string + switch v := value.(type) { case string: str = v @@ -68,6 +69,7 @@ func (a *AccessEntryAuthMethod) Scan(value any) error { default: return fmt.Errorf("cannot parse AccessEntryAuthMethod: invalid value %q", str) } + return nil } diff --git a/pkg/coredata/business_impact.go b/pkg/coredata/business_impact.go index fc0126c94..cabbd7b63 100644 --- a/pkg/coredata/business_impact.go +++ b/pkg/coredata/business_impact.go @@ -60,6 +60,7 @@ func (i *BusinessImpact) Scan(value any) error { default: return fmt.Errorf("unsupported type for BusinessImpact: %T", value) } + return nil } @@ -89,6 +90,7 @@ func (i *BusinessImpact) UnmarshalJSON(data []byte) error { default: return fmt.Errorf("invalid BusinessImpact value: %q", s) } + return nil } @@ -106,5 +108,6 @@ func (i *BusinessImpact) UnmarshalText(text []byte) error { default: return fmt.Errorf("invalid BusinessImpact value: %q", s) } + return nil } diff --git a/pkg/coredata/cached_certificate.go b/pkg/coredata/cached_certificate.go index 24cd40919..ba76d5f8b 100644 --- a/pkg/coredata/cached_certificate.go +++ b/pkg/coredata/cached_certificate.go @@ -58,6 +58,7 @@ LIMIT 1 ` args := pgx.NamedArgs{"domain": domain} + rows, err := conn.Query(ctx, q, args) if err != nil { return fmt.Errorf("cannot query certificate cache: %w", err) @@ -69,6 +70,7 @@ LIMIT 1 } *cc = cache + return nil } @@ -136,6 +138,7 @@ func (cc *CachedCertificates) CountAll(ctx context.Context, conn pg.Querier) (in q := `SELECT COUNT(*) FROM cached_certificates` var count int + err := conn.QueryRow(ctx, q, pgx.NamedArgs{}).Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count certificate cache: %w", err) diff --git a/pkg/coredata/common_third_party.go b/pkg/coredata/common_third_party.go index c3685f8f9..387ab6f4a 100644 --- a/pkg/coredata/common_third_party.go +++ b/pkg/coredata/common_third_party.go @@ -102,6 +102,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect common third party: %w", err) } @@ -158,6 +159,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect common third party by name: %w", err) } diff --git a/pkg/coredata/common_third_party_domain.go b/pkg/coredata/common_third_party_domain.go index dfd0b1ff8..cb6a69710 100644 --- a/pkg/coredata/common_third_party_domain.go +++ b/pkg/coredata/common_third_party_domain.go @@ -70,6 +70,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect common third party domain: %w", err) } @@ -113,6 +114,7 @@ INSERT INTO common_third_party_domains ( return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot insert common third party domain: %w", err) } diff --git a/pkg/coredata/common_third_party_domain_filter.go b/pkg/coredata/common_third_party_domain_filter.go index cb7a71a8e..643778732 100644 --- a/pkg/coredata/common_third_party_domain_filter.go +++ b/pkg/coredata/common_third_party_domain_filter.go @@ -41,5 +41,6 @@ func (f *CommonThirdPartyDomainFilter) SQLArguments() pgx.StrictNamedArgs { if len(f.domains) > 0 { args["filter_domains"] = f.domains } + return args } diff --git a/pkg/coredata/common_third_party_filter.go b/pkg/coredata/common_third_party_filter.go index d8468cb75..4ab1bb351 100644 --- a/pkg/coredata/common_third_party_filter.go +++ b/pkg/coredata/common_third_party_filter.go @@ -41,5 +41,6 @@ func (f *CommonThirdPartyFilter) SQLArguments() pgx.StrictNamedArgs { if f.name != nil { args["filter_name"] = *f.name } + return args } diff --git a/pkg/coredata/common_tracker_pattern.go b/pkg/coredata/common_tracker_pattern.go index 757ae6068..dc4071662 100644 --- a/pkg/coredata/common_tracker_pattern.go +++ b/pkg/coredata/common_tracker_pattern.go @@ -78,6 +78,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect common tracker pattern: %w", err) } @@ -130,6 +131,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect common tracker pattern: %w", err) } @@ -340,6 +342,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return nil, nil } + return nil, fmt.Errorf("cannot collect common tracker pattern: %w", err) } diff --git a/pkg/coredata/compliance_external_url.go b/pkg/coredata/compliance_external_url.go index 3753e3079..a5a140a26 100644 --- a/pkg/coredata/compliance_external_url.go +++ b/pkg/coredata/compliance_external_url.go @@ -49,6 +49,7 @@ func (c ComplianceExternalURL) CursorKey(orderBy ComplianceExternalURLOrderField case ComplianceExternalURLOrderFieldRank: return page.NewCursorKey(c.ID, c.Rank) } + panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } @@ -60,6 +61,7 @@ func (c *ComplianceExternalURL) AuthorizationAttributes(ctx context.Context, con if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query compliance external URL authorization attributes: %w", err) } @@ -104,6 +106,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect compliance external URL: %w", err) } diff --git a/pkg/coredata/compliance_framework.go b/pkg/coredata/compliance_framework.go index b7b479b4d..c71b5866f 100644 --- a/pkg/coredata/compliance_framework.go +++ b/pkg/coredata/compliance_framework.go @@ -52,6 +52,7 @@ func (c ComplianceFramework) CursorKey(orderBy ComplianceFrameworkOrderField) pa case ComplianceFrameworkOrderFieldRank: return page.NewCursorKey(c.ID, c.Rank) } + panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } @@ -63,6 +64,7 @@ func (c *ComplianceFramework) AuthorizationAttributes(ctx context.Context, conn if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query compliance framework authorization attributes: %w", err) } @@ -107,6 +109,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect compliance framework: %w", err) } @@ -158,6 +161,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect compliance framework: %w", err) } @@ -214,6 +218,7 @@ RETURNING rank; return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot insert compliance framework: %w", err) } diff --git a/pkg/coredata/connector.go b/pkg/coredata/connector.go index 1c90ebaf3..ba3d8394b 100644 --- a/pkg/coredata/connector.go +++ b/pkg/coredata/connector.go @@ -41,11 +41,13 @@ func (j *jsonRawMessageOrNull) Scan(src any) error { *j = nil return nil } + switch v := src.(type) { case []byte: cp := make(jsonRawMessageOrNull, len(v)) copy(cp, v) *j = cp + return nil case string: *j = jsonRawMessageOrNull(v) @@ -91,6 +93,7 @@ func (c *Connector) AuthorizationAttributes(ctx context.Context, conn pg.Querier if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query connector authorization attributes: %w", err) } @@ -152,10 +155,12 @@ func (c *Connector) LoadOneByOrganizationIDAndProvider( if ci != cj { return ci > cj } + return connectors[i].UpdatedAt.After(connectors[j].UpdatedAt) }) *c = *connectors[0] + return nil } @@ -166,6 +171,7 @@ func connectorScopeCount(c *Connector) int { if c == nil || c.Connection == nil { return 0 } + return len(c.Connection.Scopes()) } @@ -265,6 +271,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect connector row: %w", err) } diff --git a/pkg/coredata/connector_protocol.go b/pkg/coredata/connector_protocol.go index d0f435eea..757eb0404 100644 --- a/pkg/coredata/connector_protocol.go +++ b/pkg/coredata/connector_protocol.go @@ -39,6 +39,7 @@ func (cp ConnectorProtocol) String() string { func (cp *ConnectorProtocol) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -56,6 +57,7 @@ func (cp *ConnectorProtocol) Scan(value any) error { default: return fmt.Errorf("invalid ConnectorProtocol value: %q", s) } + return nil } diff --git a/pkg/coredata/connector_provider.go b/pkg/coredata/connector_provider.go index fa84aa6f5..9737de78a 100644 --- a/pkg/coredata/connector_provider.go +++ b/pkg/coredata/connector_provider.go @@ -88,6 +88,7 @@ func (cp ConnectorProvider) String() string { func (cp *ConnectorProvider) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -153,6 +154,7 @@ func (cp *ConnectorProvider) Scan(value any) error { default: return fmt.Errorf("invalid ConnectorProvider value: %q", s) } + return nil } diff --git a/pkg/coredata/connector_settings.go b/pkg/coredata/connector_settings.go index e31d3c1bf..71039527c 100644 --- a/pkg/coredata/connector_settings.go +++ b/pkg/coredata/connector_settings.go @@ -89,7 +89,9 @@ func (c *Connector) SetSettings(v any) error { if err != nil { return fmt.Errorf("cannot marshal connector settings: %w", err) } + c.RawSettings = data + return nil } @@ -103,8 +105,10 @@ func ConnectorSettings[T any](c *Connector) (T, error) { if len(c.RawSettings) == 0 || string(c.RawSettings) == "null" { return s, nil } + if err := json.Unmarshal(c.RawSettings, &s); err != nil { return s, fmt.Errorf("cannot unmarshal connector settings: %w", err) } + return s, nil } diff --git a/pkg/coredata/control.go b/pkg/coredata/control.go index 786dcbba0..ef2eae1a1 100644 --- a/pkg/coredata/control.go +++ b/pkg/coredata/control.go @@ -66,6 +66,7 @@ func (c *Control) AuthorizationAttributes(ctx context.Context, conn pg.Querier) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query control authorization attributes: %w", err) } @@ -652,6 +653,7 @@ LIMIT 1; args := pgx.StrictNamedArgs{"framework_id": frameworkID, "section_title": sectionTitle} maps.Copy(args, scope.SQLArguments()) + rows, err := conn.Query(ctx, q, args) if err != nil { return fmt.Errorf("cannot query controls: %w", err) @@ -701,6 +703,7 @@ LIMIT 1; args := pgx.StrictNamedArgs{"control_id": controlID} maps.Copy(args, scope.SQLArguments()) + rows, err := conn.Query(ctx, q, args) if err != nil { return fmt.Errorf("cannot query controls: %w", err) @@ -816,8 +819,8 @@ VALUES ( "created_at": c.CreatedAt, "updated_at": c.UpdatedAt, } - _, err := conn.Exec(ctx, q, args) + _, err := conn.Exec(ctx, q, args) if err != nil { var pgErr *pgconn.PgError if errors.As(err, &pgErr) { @@ -825,6 +828,7 @@ VALUES ( return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot insert control: %w", err) } @@ -850,6 +854,7 @@ WHERE q = fmt.Sprintf(q, scope.SQLFragment()) _, err := conn.Exec(ctx, q, args) + return err } @@ -893,6 +898,7 @@ WHERE %s return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot update control: %w", err) } diff --git a/pkg/coredata/control_audit.go b/pkg/coredata/control_audit.go index 51a55eeaa..361995b1a 100644 --- a/pkg/coredata/control_audit.go +++ b/pkg/coredata/control_audit.go @@ -68,6 +68,7 @@ ON CONFLICT (control_id, audit_id) DO NOTHING; "created_at": ca.CreatedAt, } _, err := conn.Exec(ctx, q, args) + return err } @@ -96,5 +97,6 @@ WHERE q = fmt.Sprintf(q, scope.SQLFragment()) _, err := conn.Exec(ctx, q, args) + return err } diff --git a/pkg/coredata/control_document.go b/pkg/coredata/control_document.go index 9967ce399..958cd4648 100644 --- a/pkg/coredata/control_document.go +++ b/pkg/coredata/control_document.go @@ -69,8 +69,8 @@ VALUES ( "tenant_id": scope.GetTenantID(), "created_at": cp.CreatedAt, } - _, err := conn.Exec(ctx, q, args) + _, err := conn.Exec(ctx, q, args) if err != nil { var pgErr *pgconn.PgError if errors.As(err, &pgErr) { @@ -111,6 +111,7 @@ WHERE q = fmt.Sprintf(q, scope.SQLFragment()) _, err := conn.Exec(ctx, q, args) + return err } diff --git a/pkg/coredata/control_maturity_level.go b/pkg/coredata/control_maturity_level.go index e5de3949f..7caf66a1d 100644 --- a/pkg/coredata/control_maturity_level.go +++ b/pkg/coredata/control_maturity_level.go @@ -53,6 +53,7 @@ func (l ControlMaturityLevel) IsValid() bool { ControlMaturityLevelOptimizing: return true } + return false } @@ -69,7 +70,9 @@ func (l *ControlMaturityLevel) UnmarshalText(data []byte) error { if !val.IsValid() { return fmt.Errorf("invalid ControlMaturityLevel value: %q", string(data)) } + *l = val + return nil } @@ -78,6 +81,7 @@ func (l *ControlMaturityLevel) Scan(value any) error { if !ok { return fmt.Errorf("invalid scan source for ControlMaturityLevel, expected string got %T", value) } + return l.UnmarshalText([]byte(val)) } diff --git a/pkg/coredata/control_maturity_level_test.go b/pkg/coredata/control_maturity_level_test.go index 2344f051d..f3233ccb4 100644 --- a/pkg/coredata/control_maturity_level_test.go +++ b/pkg/coredata/control_maturity_level_test.go @@ -69,17 +69,20 @@ func TestControlMaturityLevelScan(t *testing.T) { t.Parallel() var got ControlMaturityLevel + err := got.Scan(tt.input) if tt.wantErr { if err == nil { t.Fatalf("Scan(%v) expected error", tt.input) } + return } if err != nil { t.Fatalf("Scan(%v) returned error: %v", tt.input, err) } + if got != tt.want { t.Fatalf("Scan(%v) = %q, want %q", tt.input, got, tt.want) } @@ -111,6 +114,7 @@ func TestControlMaturityLevelValue(t *testing.T) { if err != nil { t.Fatalf("Value() returned error: %v", err) } + if got != tt.want { t.Fatalf("Value() = %q, want %q", got, tt.want) } @@ -141,6 +145,7 @@ func TestControlMaturityLevelMarshalUnmarshalText(t *testing.T) { if err := roundtrip.UnmarshalText(data); err != nil { t.Fatalf("UnmarshalText(%q) returned error: %v", string(data), err) } + if roundtrip != level { t.Fatalf("roundtrip = %q, want %q", roundtrip, level) } diff --git a/pkg/coredata/control_mesure.go b/pkg/coredata/control_mesure.go index ad3550767..eebaf70cd 100644 --- a/pkg/coredata/control_mesure.go +++ b/pkg/coredata/control_mesure.go @@ -69,6 +69,7 @@ ON CONFLICT (control_id, measure_id) DO NOTHING; "created_at": cm.CreatedAt, } _, err := conn.Exec(ctx, q, args) + return err } @@ -97,6 +98,7 @@ WHERE q = fmt.Sprintf(q, scope.SQLFragment()) _, err := conn.Exec(ctx, q, args) + return err } @@ -169,5 +171,6 @@ WHERE } *cwrs = controlsWithRisk + return nil } diff --git a/pkg/coredata/control_obligation.go b/pkg/coredata/control_obligation.go index 8d3730810..afb1f9481 100644 --- a/pkg/coredata/control_obligation.go +++ b/pkg/coredata/control_obligation.go @@ -69,6 +69,7 @@ ON CONFLICT (control_id, obligation_id) DO NOTHING; "created_at": co.CreatedAt, } _, err := conn.Exec(ctx, q, args) + return err } @@ -97,6 +98,7 @@ WHERE q = fmt.Sprintf(q, scope.SQLFragment()) _, err := conn.Exec(ctx, q, args) + return err } diff --git a/pkg/coredata/cookie_banner.go b/pkg/coredata/cookie_banner.go index 7d37b80f1..808a786c5 100644 --- a/pkg/coredata/cookie_banner.go +++ b/pkg/coredata/cookie_banner.go @@ -433,6 +433,7 @@ INSERT INTO cookie_banners ( return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot insert cookie banner: %w", err) } @@ -482,6 +483,7 @@ WHERE return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot update cookie banner: %w", err) } @@ -595,6 +597,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect cookie banner: %w", err) } diff --git a/pkg/coredata/cookie_banner_order_field.go b/pkg/coredata/cookie_banner_order_field.go index 52dd47393..3ab8d6c38 100644 --- a/pkg/coredata/cookie_banner_order_field.go +++ b/pkg/coredata/cookie_banner_order_field.go @@ -27,6 +27,7 @@ func (p CookieBannerOrderField) Column() string { case CookieBannerOrderFieldCreatedAt: return "created_at" } + panic(fmt.Sprintf("unsupported order by: %s", p)) } @@ -35,6 +36,7 @@ func (p CookieBannerOrderField) IsValid() bool { case CookieBannerOrderFieldCreatedAt: return true } + return false } @@ -47,6 +49,7 @@ func (p *CookieBannerOrderField) UnmarshalText(text []byte) error { if !p.IsValid() { return fmt.Errorf("%s is not a valid CookieBannerOrderField", string(text)) } + return nil } diff --git a/pkg/coredata/cookie_banner_state.go b/pkg/coredata/cookie_banner_state.go index 85cf6ab64..c10fae942 100644 --- a/pkg/coredata/cookie_banner_state.go +++ b/pkg/coredata/cookie_banner_state.go @@ -39,6 +39,7 @@ func (s CookieBannerState) String() string { func (s *CookieBannerState) Scan(value any) error { var v string + switch val := value.(type) { case string: v = val @@ -56,6 +57,7 @@ func (s *CookieBannerState) Scan(value any) error { default: return fmt.Errorf("invalid CookieBannerState value: %q", v) } + return nil } diff --git a/pkg/coredata/cookie_banner_translation.go b/pkg/coredata/cookie_banner_translation.go index c1e14aef2..593197adb 100644 --- a/pkg/coredata/cookie_banner_translation.go +++ b/pkg/coredata/cookie_banner_translation.go @@ -95,6 +95,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect cookie banner translation: %w", err) } @@ -146,6 +147,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect cookie banner translation: %w", err) } @@ -243,6 +245,7 @@ INSERT INTO cookie_banner_translations ( return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot insert cookie banner translation: %w", err) } diff --git a/pkg/coredata/cookie_banner_version.go b/pkg/coredata/cookie_banner_version.go index 209d4a5c8..152b978cd 100644 --- a/pkg/coredata/cookie_banner_version.go +++ b/pkg/coredata/cookie_banner_version.go @@ -100,6 +100,7 @@ func (v *CookieBannerVersion) GetSnapshot() (CookieBannerVersionSnapshot, error) if err := json.Unmarshal(v.Snapshot, &snapshot); err != nil { return snapshot, fmt.Errorf("cannot unmarshal cookie banner version snapshot: %w", err) } + return snapshot, nil } @@ -108,7 +109,9 @@ func (v *CookieBannerVersion) SetSnapshot(snapshot CookieBannerVersionSnapshot) if err != nil { return fmt.Errorf("cannot marshal cookie banner version snapshot: %w", err) } + v.Snapshot = data + return nil } @@ -151,6 +154,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect cookie banner version: %w", err) } @@ -280,6 +284,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect cookie banner version: %w", err) } @@ -328,6 +333,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect cookie banner version: %w", err) } @@ -498,6 +504,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect cookie banner version: %w", err) } diff --git a/pkg/coredata/cookie_banner_version_order_field.go b/pkg/coredata/cookie_banner_version_order_field.go index 78ef0411f..258c1fbf1 100644 --- a/pkg/coredata/cookie_banner_version_order_field.go +++ b/pkg/coredata/cookie_banner_version_order_field.go @@ -27,6 +27,7 @@ func (p CookieBannerVersionOrderField) Column() string { case CookieBannerVersionOrderFieldCreatedAt: return "created_at" } + panic(fmt.Sprintf("unsupported order by: %s", p)) } @@ -35,6 +36,7 @@ func (p CookieBannerVersionOrderField) IsValid() bool { case CookieBannerVersionOrderFieldCreatedAt: return true } + return false } @@ -47,6 +49,7 @@ func (p *CookieBannerVersionOrderField) UnmarshalText(text []byte) error { if !p.IsValid() { return fmt.Errorf("%s is not a valid CookieBannerVersionOrderField", string(text)) } + return nil } diff --git a/pkg/coredata/cookie_banner_version_state.go b/pkg/coredata/cookie_banner_version_state.go index cb90e4e22..5ce8625b3 100644 --- a/pkg/coredata/cookie_banner_version_state.go +++ b/pkg/coredata/cookie_banner_version_state.go @@ -39,6 +39,7 @@ func (s CookieBannerVersionState) String() string { func (s *CookieBannerVersionState) Scan(value any) error { var v string + switch val := value.(type) { case string: v = val @@ -56,6 +57,7 @@ func (s *CookieBannerVersionState) Scan(value any) error { default: return fmt.Errorf("invalid CookieBannerVersionState value: %q", v) } + return nil } diff --git a/pkg/coredata/cookie_category.go b/pkg/coredata/cookie_category.go index a39f079d0..63908ba93 100644 --- a/pkg/coredata/cookie_category.go +++ b/pkg/coredata/cookie_category.go @@ -60,6 +60,7 @@ func (c CookieItems) MarshalJSON() ([]byte, error) { if c == nil { return []byte("[]"), nil } + return json.Marshal([]CookieItem(c)) } @@ -68,6 +69,7 @@ func (c *CookieItems) UnmarshalJSON(data []byte) error { *c = CookieItems{} return nil } + return json.Unmarshal(data, (*[]CookieItem)(c)) } @@ -138,6 +140,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect cookie category: %w", err) } @@ -527,6 +530,7 @@ INSERT INTO cookie_categories ( return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot insert cookie category: %w", err) } @@ -572,6 +576,7 @@ WHERE return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot update cookie category: %w", err) } @@ -740,6 +745,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect uncategorised cookie category: %w", err) } diff --git a/pkg/coredata/cookie_category_order_field.go b/pkg/coredata/cookie_category_order_field.go index eef02dad0..202d25ff5 100644 --- a/pkg/coredata/cookie_category_order_field.go +++ b/pkg/coredata/cookie_category_order_field.go @@ -27,6 +27,7 @@ func (p CookieCategoryOrderField) Column() string { case CookieCategoryOrderFieldRank: return "rank" } + panic(fmt.Sprintf("unsupported order by: %s", p)) } @@ -35,6 +36,7 @@ func (p CookieCategoryOrderField) IsValid() bool { case CookieCategoryOrderFieldRank: return true } + return false } @@ -47,6 +49,7 @@ func (p *CookieCategoryOrderField) UnmarshalText(text []byte) error { if !p.IsValid() { return fmt.Errorf("%s is not a valid CookieCategoryOrderField", string(text)) } + return nil } diff --git a/pkg/coredata/cookie_consent_action.go b/pkg/coredata/cookie_consent_action.go index 005b2eeff..3bc1d9ebe 100644 --- a/pkg/coredata/cookie_consent_action.go +++ b/pkg/coredata/cookie_consent_action.go @@ -44,6 +44,7 @@ func (a CookieConsentAction) String() string { func (a *CookieConsentAction) Scan(value any) error { var v string + switch val := value.(type) { case string: v = val diff --git a/pkg/coredata/cookie_consent_mode.go b/pkg/coredata/cookie_consent_mode.go index b6002d79d..dacc87a40 100644 --- a/pkg/coredata/cookie_consent_mode.go +++ b/pkg/coredata/cookie_consent_mode.go @@ -39,6 +39,7 @@ func (m CookieConsentMode) String() string { func (m *CookieConsentMode) Scan(value any) error { var v string + switch val := value.(type) { case string: v = val diff --git a/pkg/coredata/cookie_consent_record.go b/pkg/coredata/cookie_consent_record.go index 687e9c306..d67e2ad5e 100644 --- a/pkg/coredata/cookie_consent_record.go +++ b/pkg/coredata/cookie_consent_record.go @@ -333,6 +333,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect consent record: %w", err) } diff --git a/pkg/coredata/cookie_consent_record_order_field.go b/pkg/coredata/cookie_consent_record_order_field.go index 7fc2e2899..2ef299a8e 100644 --- a/pkg/coredata/cookie_consent_record_order_field.go +++ b/pkg/coredata/cookie_consent_record_order_field.go @@ -27,6 +27,7 @@ func (p CookieConsentRecordOrderField) Column() string { case CookieConsentRecordOrderFieldCreatedAt: return "created_at" } + panic(fmt.Sprintf("unsupported order by: %s", p)) } @@ -35,6 +36,7 @@ func (p CookieConsentRecordOrderField) IsValid() bool { case CookieConsentRecordOrderFieldCreatedAt: return true } + return false } @@ -47,6 +49,7 @@ func (p *CookieConsentRecordOrderField) UnmarshalText(text []byte) error { if !p.IsValid() { return fmt.Errorf("%s is not a valid CookieConsentRecordOrderField", string(text)) } + return nil } diff --git a/pkg/coredata/cookie_source.go b/pkg/coredata/cookie_source.go index fe800021e..4b437093c 100644 --- a/pkg/coredata/cookie_source.go +++ b/pkg/coredata/cookie_source.go @@ -41,6 +41,7 @@ func (s CookieSource) String() string { func (s *CookieSource) Scan(value any) error { var v string + switch val := value.(type) { case string: v = val @@ -60,6 +61,7 @@ func (s *CookieSource) Scan(value any) error { default: return fmt.Errorf("invalid CookieSource value: %q", v) } + return nil } diff --git a/pkg/coredata/country_code.go b/pkg/coredata/country_code.go index 7c04aa648..e329c6743 100644 --- a/pkg/coredata/country_code.go +++ b/pkg/coredata/country_code.go @@ -282,6 +282,7 @@ func (ct CountryCode) String() string { func (ct *CountryCode) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -791,6 +792,7 @@ func (ct *CountryCode) Scan(value any) error { default: return fmt.Errorf("invalid CountryCode value: %q", s) } + return nil } @@ -836,10 +838,12 @@ func (s *CountryCodes) scanFromString(str string) error { if err := ct.Scan(part); err != nil { return fmt.Errorf("invalid country code in array: %s", part) } + result[i] = ct } *s = result + return nil } diff --git a/pkg/coredata/custom_domain.go b/pkg/coredata/custom_domain.go index 12b63ca61..843642d3e 100644 --- a/pkg/coredata/custom_domain.go +++ b/pkg/coredata/custom_domain.go @@ -57,6 +57,7 @@ type ( func NewCustomDomain(tenantID gid.TenantID, domain string) *CustomDomain { now := time.Now() + return &CustomDomain{ ID: gid.New(tenantID, CustomDomainEntityType), SSLStatus: CustomDomainSSLStatusPending, @@ -75,6 +76,7 @@ func (cd *CustomDomain) AuthorizationAttributes(ctx context.Context, conn pg.Que if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query custom domain authorization attributes: %w", err) } @@ -119,6 +121,7 @@ func (cd *CustomDomain) EncryptPrivateKey(privateKeyPEM []byte, encryptionKey ci } cd.EncryptedSSLPrivateKey = encrypted + return nil } @@ -147,6 +150,7 @@ func (cd *CustomDomain) ParseCertificate(encryptionKey cipher.EncryptionKey) err } cd.SSLCertificate = &tlsCert + return nil } @@ -460,6 +464,7 @@ INSERT INTO custom_domains ( return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot insert custom domain: %w", err) } @@ -658,6 +663,7 @@ ORDER BY } *domains = result + return nil } @@ -714,6 +720,7 @@ WHERE } *domains = result + return nil } @@ -765,6 +772,7 @@ WHERE } *domains = result + return nil } @@ -826,5 +834,6 @@ WHERE } *domains = result + return nil } diff --git a/pkg/coredata/data_protection_impact_assessment.go b/pkg/coredata/data_protection_impact_assessment.go index 14964eae9..2104094ff 100644 --- a/pkg/coredata/data_protection_impact_assessment.go +++ b/pkg/coredata/data_protection_impact_assessment.go @@ -50,6 +50,7 @@ WHERE if errors.Is(err, pgx.ErrNoRows) { return nil, nil } + if err != nil { return nil, fmt.Errorf("cannot get DPIA list document ID: %w", err) } @@ -170,6 +171,7 @@ func (dpia *DataProtectionImpactAssessment) AuthorizationAttributes(ctx context. if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query data protection impact assessment authorization attributes: %w", err) } @@ -200,6 +202,7 @@ WHERE row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count data protection impact assessments: %w", err) diff --git a/pkg/coredata/data_protection_impact_assessment_order_field.go b/pkg/coredata/data_protection_impact_assessment_order_field.go index fc3304eed..b6506719c 100644 --- a/pkg/coredata/data_protection_impact_assessment_order_field.go +++ b/pkg/coredata/data_protection_impact_assessment_order_field.go @@ -41,5 +41,6 @@ func (p *DataProtectionImpactAssessmentOrderField) UnmarshalText(text []byte) er *p = DataProtectionImpactAssessmentOrderFieldCreatedAt return nil } + return fmt.Errorf("invalid DataProtectionImpactAssessmentOrderField value: %q", val) } diff --git a/pkg/coredata/data_protection_impact_assessment_residual_risk.go b/pkg/coredata/data_protection_impact_assessment_residual_risk.go index 64df56005..28ec0770a 100644 --- a/pkg/coredata/data_protection_impact_assessment_residual_risk.go +++ b/pkg/coredata/data_protection_impact_assessment_residual_risk.go @@ -41,6 +41,7 @@ func (p DataProtectionImpactAssessmentResidualRisk) String() string { func (p *DataProtectionImpactAssessmentResidualRisk) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -60,6 +61,7 @@ func (p *DataProtectionImpactAssessmentResidualRisk) Scan(value any) error { default: return fmt.Errorf("invalid DataProtectionImpactAssessmentResidualRisk value: %q", s) } + return nil } diff --git a/pkg/coredata/data_sensitivity.go b/pkg/coredata/data_sensitivity.go index 382b6a7aa..1ed703c3d 100644 --- a/pkg/coredata/data_sensitivity.go +++ b/pkg/coredata/data_sensitivity.go @@ -64,6 +64,7 @@ func (i *DataSensitivity) Scan(value any) error { default: return fmt.Errorf("unsupported type for DataSensitivity: %T", value) } + return nil } @@ -95,6 +96,7 @@ func (i *DataSensitivity) UnmarshalJSON(data []byte) error { default: return fmt.Errorf("invalid DataSensitivity value: %q", s) } + return nil } @@ -118,5 +120,6 @@ func (i *DataSensitivity) UnmarshalText(text []byte) error { default: return fmt.Errorf("invalid DataSensitivity value: %q", s) } + return nil } diff --git a/pkg/coredata/datum.go b/pkg/coredata/datum.go index 105362de1..1db12da29 100644 --- a/pkg/coredata/datum.go +++ b/pkg/coredata/datum.go @@ -63,6 +63,7 @@ func (d *Datum) AuthorizationAttributes(ctx context.Context, conn pg.Querier) (m if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query datum authorization attributes: %w", err) } @@ -178,6 +179,7 @@ WHERE row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count data: %w", err) @@ -419,6 +421,7 @@ WHERE if errors.Is(err, pgx.ErrNoRows) { return nil, nil } + if err != nil { return nil, fmt.Errorf("cannot get data document ID: %w", err) } diff --git a/pkg/coredata/datum_order_field.go b/pkg/coredata/datum_order_field.go index 7fc150f15..2e30ec6e6 100644 --- a/pkg/coredata/datum_order_field.go +++ b/pkg/coredata/datum_order_field.go @@ -47,5 +47,6 @@ func (p *DatumOrderField) UnmarshalText(text []byte) error { *p = DatumOrderField(val) return nil } + return fmt.Errorf("invalid DatumOrderField value: %q", val) } diff --git a/pkg/coredata/document.go b/pkg/coredata/document.go index e019e96b8..4e2aea6a3 100644 --- a/pkg/coredata/document.go +++ b/pkg/coredata/document.go @@ -78,6 +78,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query document authorization attributes: %w", err) } @@ -287,6 +288,7 @@ WHERE maps.Copy(args, filter.SQLArguments()) row := conn.QueryRow(ctx, q, args) + var count int if err := row.Scan(&count); err != nil { return 0, fmt.Errorf("cannot scan count: %w", err) @@ -540,6 +542,7 @@ VALUES ( "updated_at": p.UpdatedAt, } _, err := conn.Exec(ctx, q, args) + return err } @@ -558,6 +561,7 @@ UPDATE documents SET deleted_at = @deleted_at WHERE %s AND id = @document_id maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) + return err } @@ -577,6 +581,7 @@ DELETE FROM documents WHERE %s AND organization_id = @organization_id maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) + return err } @@ -649,6 +654,7 @@ WHERE cp.control_id = @control_id maps.Copy(args, filter.SQLArguments()) row := conn.QueryRow(ctx, q, args) + var count int if err := row.Scan(&count); err != nil { return 0, fmt.Errorf("cannot scan count: %w", err) @@ -749,6 +755,7 @@ WHERE rp.risk_id = @risk_id maps.Copy(args, filter.SQLArguments()) row := conn.QueryRow(ctx, q, args) + var count int if err := row.Scan(&count); err != nil { return 0, fmt.Errorf("cannot scan count: %w", err) @@ -849,6 +856,7 @@ WHERE md.measure_id = @measure_id maps.Copy(args, filter.SQLArguments()) row := conn.QueryRow(ctx, q, args) + var count int if err := row.Scan(&count); err != nil { return 0, fmt.Errorf("cannot scan count: %w", err) @@ -942,6 +950,7 @@ UPDATE documents SET deleted_at = @deleted_at WHERE %s AND id = ANY(@document_id maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) + return err } @@ -969,6 +978,7 @@ UPDATE documents SET status = 'ARCHIVED', archived_at = @archived_at, trust_cent if _, err := conn.Exec(ctx, q, args); err != nil { return fmt.Errorf("cannot bulk archive documents: %w", err) } + return nil } @@ -995,6 +1005,7 @@ UPDATE documents SET status = 'ACTIVE', archived_at = NULL WHERE %s AND id = ANY if _, err := conn.Exec(ctx, q, args); err != nil { return fmt.Errorf("cannot bulk unarchive documents: %w", err) } + return nil } @@ -1106,6 +1117,7 @@ LIMIT 1 if errors.Is(err, pgx.ErrNoRows) { return "", nil } + return "", fmt.Errorf("cannot collect approval state: %w", err) } diff --git a/pkg/coredata/document_classification.go b/pkg/coredata/document_classification.go index 72a7b179f..6d30eb565 100644 --- a/pkg/coredata/document_classification.go +++ b/pkg/coredata/document_classification.go @@ -48,6 +48,7 @@ func (dc DocumentClassification) String() string { case DocumentClassificationSecret: return "SECRET" } + panic(fmt.Errorf("invalid DocumentClassification value: %s", string(dc))) } @@ -58,6 +59,7 @@ func (dc *DocumentClassification) Scan(value any) error { } var sv string + switch v := value.(type) { case string: sv = v @@ -68,6 +70,7 @@ func (dc *DocumentClassification) Scan(value any) error { } *dc = DocumentClassification(sv) + return nil } diff --git a/pkg/coredata/document_default_approver.go b/pkg/coredata/document_default_approver.go index 1a7d8d54d..3b2c5b640 100644 --- a/pkg/coredata/document_default_approver.go +++ b/pkg/coredata/document_default_approver.go @@ -74,6 +74,7 @@ ORDER BY created_at ASC; } *das = result + return nil } @@ -139,5 +140,6 @@ WHEN NOT MATCHED BY SOURCE } *das = result + return nil } diff --git a/pkg/coredata/document_filter.go b/pkg/coredata/document_filter.go index 5f1457608..e680ae19f 100644 --- a/pkg/coredata/document_filter.go +++ b/pkg/coredata/document_filter.go @@ -41,6 +41,7 @@ func NewDocumentFilter(query *string) *DocumentFilter { func NewDocumentTrustCenterFilter() *DocumentFilter { published := true + return &DocumentFilter{ trustCenterVisibilities: []TrustCenterVisibility{ TrustCenterVisibilityPrivate, @@ -59,6 +60,7 @@ func (f *DocumentFilter) WithPublished(published *bool) *DocumentFilter { func (f *DocumentFilter) WithEmployeeIdentityID(identityID *gid.GID, modes ...EmployeeFilterMode) *DocumentFilter { f.employeeIdentityID = identityID f.employeeFilterModes = modes + return f } diff --git a/pkg/coredata/document_order_field.go b/pkg/coredata/document_order_field.go index c5323b94b..47d145f1c 100644 --- a/pkg/coredata/document_order_field.go +++ b/pkg/coredata/document_order_field.go @@ -38,6 +38,7 @@ func (p DocumentOrderField) Column() string { case DocumentOrderFieldDocumentType: return "document_type" } + panic(fmt.Sprintf("unsupported order by: %s", p)) } @@ -49,6 +50,7 @@ func (p DocumentOrderField) IsValid() bool { DocumentOrderFieldDocumentType: return true } + return false } @@ -65,5 +67,6 @@ func (p *DocumentOrderField) UnmarshalText(text []byte) error { if !p.IsValid() { return fmt.Errorf("%s is not a valid DocumentOrderField", string(text)) } + return nil } diff --git a/pkg/coredata/document_status.go b/pkg/coredata/document_status.go index 414c0ff05..285d6f559 100644 --- a/pkg/coredata/document_status.go +++ b/pkg/coredata/document_status.go @@ -31,6 +31,7 @@ func (s DocumentStatus) IsValid() bool { case DocumentStatusActive, DocumentStatusArchived: return true } + return false } @@ -41,6 +42,7 @@ func (s *DocumentStatus) UnmarshalText(text []byte) error { if !s.IsValid() { return fmt.Errorf("%s is not a valid DocumentStatus", string(text)) } + return nil } @@ -53,6 +55,7 @@ func (s *DocumentStatus) Scan(value any) error { if !ok { return fmt.Errorf("invalid scan source for DocumentStatus, expected string got %T", value) } + return s.UnmarshalText([]byte(val)) } diff --git a/pkg/coredata/document_version.go b/pkg/coredata/document_version.go index 989ca3c3e..94d9f7a5a 100644 --- a/pkg/coredata/document_version.go +++ b/pkg/coredata/document_version.go @@ -67,6 +67,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query document version authorization attributes: %w", err) } @@ -193,6 +194,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect document version: %w", err) } @@ -279,6 +281,7 @@ VALUES ( } } } + return fmt.Errorf("error creating document version: %w", err) } @@ -341,6 +344,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect document version: %w", err) } @@ -399,6 +403,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect document version: %w", err) } @@ -459,6 +464,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect document version: %w", err) } @@ -595,6 +601,7 @@ FOR UPDATE OF dv SKIP LOCKED; if errors.Is(err, pgx.ErrNoRows) { return ErrNoDocumentPDFJobAvailable } + return fmt.Errorf("cannot collect document version: %w", err) } @@ -651,6 +658,7 @@ WHERE maps.Copy(args, filter.SQLArguments()) row := conn.QueryRow(ctx, q, args) + var count int if err := row.Scan(&count); err != nil { return 0, fmt.Errorf("cannot scan count: %w", err) diff --git a/pkg/coredata/document_version_approval_decision.go b/pkg/coredata/document_version_approval_decision.go index 96674bdd4..e30cfe6da 100644 --- a/pkg/coredata/document_version_approval_decision.go +++ b/pkg/coredata/document_version_approval_decision.go @@ -62,6 +62,7 @@ func (d *DocumentVersionApprovalDecision) AuthorizationAttributes(ctx context.Co if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query document version approval decision authorization attributes: %w", err) } @@ -108,6 +109,7 @@ WHERE if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect document version approval decision: %w", err) } @@ -162,6 +164,7 @@ LIMIT 1 if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect document version approval decision: %w", err) } @@ -193,6 +196,7 @@ WHERE maps.Copy(args, scope.SQLArguments()) row := conn.QueryRow(ctx, q, args) + var count int if err := row.Scan(&count); err != nil { return 0, fmt.Errorf("cannot scan count: %w", err) @@ -307,6 +311,7 @@ INSERT INTO document_version_approval_decisions ( return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot insert document version approval decision: %w", err) } @@ -357,6 +362,7 @@ func (ds DocumentVersionApprovalDecisions) BulkInsert( }, pgx.CopyFromRows(rows), ) + return err } @@ -483,6 +489,7 @@ WHERE maps.Copy(args, filter.SQLArguments()) row := conn.QueryRow(ctx, q, args) + var count int if err := row.Scan(&count); err != nil { return 0, fmt.Errorf("cannot scan count: %w", err) diff --git a/pkg/coredata/document_version_approval_decision_filter.go b/pkg/coredata/document_version_approval_decision_filter.go index 3baa36c4a..38a37836e 100644 --- a/pkg/coredata/document_version_approval_decision_filter.go +++ b/pkg/coredata/document_version_approval_decision_filter.go @@ -28,6 +28,7 @@ func NewDocumentVersionApprovalDecisionFilter(states []DocumentVersionApprovalDe if len(states) == 0 { states = nil } + return &DocumentVersionApprovalDecisionFilter{ states: DocumentVersionApprovalDecisionStates(states), } diff --git a/pkg/coredata/document_version_approval_decision_order_field.go b/pkg/coredata/document_version_approval_decision_order_field.go index f5479d1bb..9cf688e67 100644 --- a/pkg/coredata/document_version_approval_decision_order_field.go +++ b/pkg/coredata/document_version_approval_decision_order_field.go @@ -29,6 +29,7 @@ func (e DocumentVersionApprovalDecisionOrderField) Column() string { case DocumentVersionApprovalDecisionOrderFieldCreatedAt: return "created_at" } + panic(fmt.Sprintf("unsupported order by: %s", e)) } @@ -37,6 +38,7 @@ func (e DocumentVersionApprovalDecisionOrderField) IsValid() bool { case DocumentVersionApprovalDecisionOrderFieldCreatedAt: return true } + return false } @@ -47,6 +49,7 @@ func (e *DocumentVersionApprovalDecisionOrderField) UnmarshalText(text []byte) e if !e.IsValid() { return fmt.Errorf("%s is not a valid DocumentVersionApprovalDecisionOrderField", string(text)) } + return nil } diff --git a/pkg/coredata/document_version_approval_decision_state.go b/pkg/coredata/document_version_approval_decision_state.go index 7a35475be..36e2405d9 100644 --- a/pkg/coredata/document_version_approval_decision_state.go +++ b/pkg/coredata/document_version_approval_decision_state.go @@ -94,12 +94,16 @@ func (states DocumentVersionApprovalDecisionStates) Value() (driver.Value, error var result strings.Builder result.WriteString("{") + for i, state := range states { if i > 0 { result.WriteString(",") } + fmt.Fprintf(&result, "%q", state.String()) } + result.WriteString("}") + return result.String(), nil } diff --git a/pkg/coredata/document_version_approval_quorum.go b/pkg/coredata/document_version_approval_quorum.go index 51ec51a9e..847798d16 100644 --- a/pkg/coredata/document_version_approval_quorum.go +++ b/pkg/coredata/document_version_approval_quorum.go @@ -58,6 +58,7 @@ func (q *DocumentVersionApprovalQuorum) AuthorizationAttributes(ctx context.Cont if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query approval quorum authorization attributes: %w", err) } @@ -100,6 +101,7 @@ WHERE if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect approval quorum: %w", err) } @@ -153,6 +155,7 @@ LIMIT 1 if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect last approval quorum: %w", err) } @@ -241,6 +244,7 @@ WHERE maps.Copy(args, scope.SQLArguments()) row := conn.QueryRow(ctx, query, args) + var count int if err := row.Scan(&count); err != nil { return 0, fmt.Errorf("cannot scan count: %w", err) @@ -292,6 +296,7 @@ INSERT INTO document_version_approval_quorums ( return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot insert approval quorum: %w", err) } diff --git a/pkg/coredata/document_version_approval_quorum_order_field.go b/pkg/coredata/document_version_approval_quorum_order_field.go index 8da894ddb..67f998004 100644 --- a/pkg/coredata/document_version_approval_quorum_order_field.go +++ b/pkg/coredata/document_version_approval_quorum_order_field.go @@ -29,6 +29,7 @@ func (e DocumentVersionApprovalQuorumOrderField) Column() string { case DocumentVersionApprovalQuorumOrderFieldCreatedAt: return "created_at" } + panic(fmt.Sprintf("unsupported order by: %s", e)) } @@ -37,6 +38,7 @@ func (e DocumentVersionApprovalQuorumOrderField) IsValid() bool { case DocumentVersionApprovalQuorumOrderFieldCreatedAt: return true } + return false } @@ -47,6 +49,7 @@ func (e *DocumentVersionApprovalQuorumOrderField) UnmarshalText(text []byte) err if !e.IsValid() { return fmt.Errorf("%s is not a valid DocumentVersionApprovalQuorumOrderField", string(text)) } + return nil } diff --git a/pkg/coredata/document_version_filter.go b/pkg/coredata/document_version_filter.go index 2bfac5811..3349a9a0d 100644 --- a/pkg/coredata/document_version_filter.go +++ b/pkg/coredata/document_version_filter.go @@ -46,6 +46,7 @@ func (f *DocumentVersionFilter) WithStatuses(statuses ...DocumentVersionStatus) func (f *DocumentVersionFilter) WithEmployeeIdentityID(identityID *gid.GID, modes ...EmployeeFilterMode) *DocumentVersionFilter { f.employeeIdentityID = identityID f.employeeFilterModes = modes + return f } diff --git a/pkg/coredata/document_version_signature.go b/pkg/coredata/document_version_signature.go index 90efe4d8d..2ae903f7d 100644 --- a/pkg/coredata/document_version_signature.go +++ b/pkg/coredata/document_version_signature.go @@ -72,6 +72,7 @@ func (dvs *DocumentVersionSignature) AuthorizationAttributes(ctx context.Context if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query document version signature authorization attributes: %w", err) } @@ -221,6 +222,7 @@ INSERT INTO document_version_signatures ( return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot insert document version signature: %w", err) } @@ -506,6 +508,7 @@ WHERE maps.Copy(args, filter.SQLArguments()) row := conn.QueryRow(ctx, q, args) + var count int if err := row.Scan(&count); err != nil { return 0, fmt.Errorf("cannot scan count: %w", err) diff --git a/pkg/coredata/document_version_signature_state.go b/pkg/coredata/document_version_signature_state.go index 35fee0233..d5bc8d598 100644 --- a/pkg/coredata/document_version_signature_state.go +++ b/pkg/coredata/document_version_signature_state.go @@ -84,12 +84,16 @@ func (states DocumentVersionSignatureStates) Value() (driver.Value, error) { var result strings.Builder result.WriteString("{") + for i, state := range states { if i > 0 { result.WriteString(",") } + fmt.Fprintf(&result, "%q", state.String()) } + result.WriteString("}") + return result.String(), nil } diff --git a/pkg/coredata/document_write_mode.go b/pkg/coredata/document_write_mode.go index e9f5e1331..c3ff0d3c4 100644 --- a/pkg/coredata/document_write_mode.go +++ b/pkg/coredata/document_write_mode.go @@ -30,6 +30,7 @@ func (e DocumentWriteMode) IsValid() bool { case DocumentWriteModeAuthored, DocumentWriteModeGenerated: return true } + return false } @@ -40,6 +41,7 @@ func (e *DocumentWriteMode) UnmarshalText(text []byte) error { if !e.IsValid() { return fmt.Errorf("%s is not a valid DocumentWriteMode", string(text)) } + return nil } diff --git a/pkg/coredata/electronic_signature.go b/pkg/coredata/electronic_signature.go index 25dd99e36..0294e9313 100644 --- a/pkg/coredata/electronic_signature.go +++ b/pkg/coredata/electronic_signature.go @@ -211,10 +211,12 @@ LIMIT 1 if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect electronic signature: %w", err) } *es = sig + return nil } @@ -247,10 +249,12 @@ FOR UPDATE SKIP LOCKED if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect electronic signature: %w", err) } *es = sig + return nil } @@ -286,10 +290,12 @@ FOR UPDATE SKIP LOCKED if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect electronic signature: %w", err) } *es = sig + return nil } @@ -304,6 +310,7 @@ SET status = 'ACCEPTED', processing_started_at = NULL, updated_at = NOW() WHERE status = 'PROCESSING' AND processing_started_at < NOW() - $1::interval ` + _, err := conn.Exec(ctx, q, staleAfter) if err != nil { return fmt.Errorf("cannot reset stale processing signatures: %w", err) @@ -344,12 +351,14 @@ func (es *ElectronicSignature) computeSealV1() (string, error) { if f == "" { return "", fmt.Errorf("seal field %d must not be empty", i) } + if strings.Contains(f, "\n") { return "", fmt.Errorf("seal field %d must not contain newline", i) } } input := strings.Join(fields, "\n") + return hash.SHA256HexString(input), nil } @@ -367,6 +376,7 @@ WHERE status = 'COMPLETED' AND certificate_processing_started_at < NOW() - $1::interval AND attempt_count < max_attempts ` + _, err := conn.Exec(ctx, q, staleAfter) if err != nil { return fmt.Errorf("cannot reset stale certificate processing: %w", err) diff --git a/pkg/coredata/electronic_signature_document_type.go b/pkg/coredata/electronic_signature_document_type.go index 7901891c6..7621ba0be 100644 --- a/pkg/coredata/electronic_signature_document_type.go +++ b/pkg/coredata/electronic_signature_document_type.go @@ -174,6 +174,7 @@ func (dt ElectronicSignatureDocumentType) DisplayName() string { func (dt ElectronicSignatureDocumentType) ConsentText() (string, error) { var docAgreement string + switch dt { case ElectronicSignatureDocumentTypeNDA: docAgreement = "I agree to the terms of this Non-Disclosure Agreement." diff --git a/pkg/coredata/electronic_signature_event_type.go b/pkg/coredata/electronic_signature_event_type.go index 233918a19..1a5bd55da 100644 --- a/pkg/coredata/electronic_signature_event_type.go +++ b/pkg/coredata/electronic_signature_event_type.go @@ -74,6 +74,7 @@ func (t ElectronicSignatureEventType) String() string { func (t *ElectronicSignatureEventType) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v diff --git a/pkg/coredata/email.go b/pkg/coredata/email.go index 5e60d41eb..e33aacce3 100644 --- a/pkg/coredata/email.go +++ b/pkg/coredata/email.go @@ -162,6 +162,7 @@ VALUES ( } _, err := conn.Exec(ctx, q, args) + return err } @@ -197,6 +198,7 @@ func (emails Emails) BulkInsert( []string{"id", "recipient_email", "recipient_name", "sender_name", "reply_to", "unsubscribe_url", "mailing_list_update_id", "subject", "text_body", "html_body", "created_at", "updated_at"}, pgx.CopyFromRows(rows), ) + return err } @@ -264,6 +266,7 @@ WHERE id = @id } _, err := conn.Exec(ctx, q, args) + return err } @@ -278,6 +281,7 @@ SET status = 'PENDING', processing_started_at = NULL, updated_at = NOW() WHERE status = 'PROCESSING' AND processing_started_at < NOW() - $1::interval ` + _, err := conn.Exec(ctx, q, staleAfter) if err != nil { return fmt.Errorf("cannot reset stale processing emails: %w", err) diff --git a/pkg/coredata/evidence.go b/pkg/coredata/evidence.go index 205c45feb..7cb7eb92f 100644 --- a/pkg/coredata/evidence.go +++ b/pkg/coredata/evidence.go @@ -67,6 +67,7 @@ func (e *Evidence) AuthorizationAttributes(ctx context.Context, conn pg.Querier) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query evidence authorization attributes: %w", err) } @@ -136,6 +137,7 @@ WHERE evidences.state = 'REQUESTED'; "description_processing_started_at": e.DescriptionProcessingStartedAt, } _, err := conn.Exec(ctx, q, args) + return err } @@ -199,8 +201,8 @@ VALUES ( "description_status": e.DescriptionStatus, "description_processing_started_at": e.DescriptionProcessingStartedAt, } - _, err := conn.Exec(ctx, q, args) + _, err := conn.Exec(ctx, q, args) if err != nil { var pgErr *pgconn.PgError if errors.As(err, &pgErr) { @@ -208,6 +210,7 @@ VALUES ( return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot insert evidence: %w", err) } @@ -288,6 +291,7 @@ WHERE row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot collect evidence: %w", err) @@ -372,6 +376,7 @@ WHERE row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot collect evidence: %w", err) @@ -470,6 +475,7 @@ WHERE maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) + return err } @@ -540,6 +546,7 @@ FOR UPDATE SKIP LOCKED; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect evidence: %w", err) } @@ -564,5 +571,6 @@ WHERE ` _, err := conn.Exec(ctx, q, time.Now().Add(-staleAfter)) + return err } diff --git a/pkg/coredata/export_job.go b/pkg/coredata/export_job.go index 72231593c..33c3e7108 100644 --- a/pkg/coredata/export_job.go +++ b/pkg/coredata/export_job.go @@ -71,6 +71,7 @@ func (ej *ExportJob) AuthorizationAttributes(ctx context.Context, conn pg.Querie if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query export job authorization attributes: %w", err) } @@ -116,6 +117,7 @@ INSERT INTO export_jobs ( "created_at": ej.CreatedAt, } _, err := conn.Exec(ctx, q, args) + return err } @@ -148,6 +150,7 @@ WHERE } maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) + return err } @@ -192,6 +195,7 @@ WHERE } *ej = ej2 + return nil } @@ -225,6 +229,7 @@ FOR UPDATE SKIP LOCKED args := pgx.StrictNamedArgs{ "status": ExportJobStatusPending, } + rows, err := conn.Query(ctx, q, args) if err != nil { return err @@ -235,10 +240,12 @@ FOR UPDATE SKIP LOCKED if errors.Is(err, pgx.ErrNoRows) { return ErrNoExportJobAvailable } + return fmt.Errorf("cannot collect export job: %w", err) } *ej = ej2 + return nil } @@ -273,6 +280,7 @@ func (ej *ExportJob) GetDocumentIDs() ([]gid.GID, error) { if err != nil { return nil, err } + return args.DocumentIDs, nil } @@ -281,5 +289,6 @@ func (ej *ExportJob) GetFrameworkID() (gid.GID, error) { if err != nil { return gid.GID{}, err } + return args.FrameworkID, nil } diff --git a/pkg/coredata/export_job_status.go b/pkg/coredata/export_job_status.go index 392173a17..601c48270 100644 --- a/pkg/coredata/export_job_status.go +++ b/pkg/coredata/export_job_status.go @@ -36,6 +36,7 @@ func (ejs ExportJobStatus) String() string { func (ejs *ExportJobStatus) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -57,6 +58,7 @@ func (ejs *ExportJobStatus) Scan(value any) error { default: return fmt.Errorf("invalid ExportJobStatus value: %q", s) } + return nil } diff --git a/pkg/coredata/export_job_type.go b/pkg/coredata/export_job_type.go index c8c99405f..6c40e691b 100644 --- a/pkg/coredata/export_job_type.go +++ b/pkg/coredata/export_job_type.go @@ -34,6 +34,7 @@ func (ejt ExportJobType) String() string { func (ejt *ExportJobType) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -51,6 +52,7 @@ func (ejt *ExportJobType) Scan(value any) error { default: return fmt.Errorf("invalid ExportJobType value: %q", s) } + return nil } diff --git a/pkg/coredata/file.go b/pkg/coredata/file.go index abebf3fbe..97882c223 100644 --- a/pkg/coredata/file.go +++ b/pkg/coredata/file.go @@ -73,6 +73,7 @@ func (f *File) AuthorizationAttributes(ctx context.Context, conn pg.Querier) (ma if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query file authorization attributes: %w", err) } @@ -228,8 +229,8 @@ VALUES ( "updated_at": f.UpdatedAt, "deleted_at": f.DeletedAt, } - _, err := conn.Exec(ctx, q, args) + _, err := conn.Exec(ctx, q, args) if err != nil { var pgErr *pgconn.PgError if errors.As(err, &pgErr) { @@ -237,6 +238,7 @@ VALUES ( return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot insert file: %w", err) } diff --git a/pkg/coredata/file_visibility.go b/pkg/coredata/file_visibility.go index aba204f5e..d31abad39 100644 --- a/pkg/coredata/file_visibility.go +++ b/pkg/coredata/file_visibility.go @@ -32,6 +32,7 @@ func (fv FileVisibility) String() string { func (fv *FileVisibility) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -49,6 +50,7 @@ func (fv *FileVisibility) Scan(value any) error { default: return fmt.Errorf("invalid FileVisibility value: %q", s) } + return nil } diff --git a/pkg/coredata/finding.go b/pkg/coredata/finding.go index df28ebbb7..8d33608a8 100644 --- a/pkg/coredata/finding.go +++ b/pkg/coredata/finding.go @@ -80,6 +80,7 @@ func (f *Finding) AuthorizationAttributes(ctx context.Context, conn pg.Querier) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query finding authorization attributes: %w", err) } @@ -166,6 +167,7 @@ WHERE row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count findings: %w", err) @@ -524,6 +526,7 @@ WHERE row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count findings: %w", err) @@ -608,6 +611,7 @@ WHERE if errors.Is(err, pgx.ErrNoRows) { return nil, nil } + if err != nil { return nil, fmt.Errorf("cannot get finding list document ID: %w", err) } diff --git a/pkg/coredata/finding_audit.go b/pkg/coredata/finding_audit.go index f3a540d4d..15a14c7f9 100644 --- a/pkg/coredata/finding_audit.go +++ b/pkg/coredata/finding_audit.go @@ -71,6 +71,7 @@ ON CONFLICT (finding_id, audit_id) DO NOTHING; "tenant_id": scope.GetTenantID(), "created_at": fa.CreatedAt, } + _, err := conn.Exec(ctx, q, args) if err != nil { return fmt.Errorf("cannot upsert finding audit: %w", err) diff --git a/pkg/coredata/finding_kind.go b/pkg/coredata/finding_kind.go index 87e3983c4..43118f5c2 100644 --- a/pkg/coredata/finding_kind.go +++ b/pkg/coredata/finding_kind.go @@ -43,6 +43,7 @@ func (fk FindingKind) String() string { func (fk *FindingKind) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -64,6 +65,7 @@ func (fk *FindingKind) Scan(value any) error { default: return fmt.Errorf("invalid FindingKind value: %q", s) } + return nil } diff --git a/pkg/coredata/finding_order_field.go b/pkg/coredata/finding_order_field.go index 8f72753a6..38f40ab14 100644 --- a/pkg/coredata/finding_order_field.go +++ b/pkg/coredata/finding_order_field.go @@ -55,5 +55,6 @@ func (p *FindingOrderField) UnmarshalText(text []byte) error { *p = FindingOrderField(val) return nil } + return fmt.Errorf("invalid FindingOrderField value: %q", val) } diff --git a/pkg/coredata/finding_priority.go b/pkg/coredata/finding_priority.go index a62e36094..21cae456f 100644 --- a/pkg/coredata/finding_priority.go +++ b/pkg/coredata/finding_priority.go @@ -41,6 +41,7 @@ func (fp FindingPriority) String() string { func (fp *FindingPriority) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -60,6 +61,7 @@ func (fp *FindingPriority) Scan(value any) error { default: return fmt.Errorf("invalid FindingPriority value: %q", s) } + return nil } diff --git a/pkg/coredata/finding_status.go b/pkg/coredata/finding_status.go index 78913d862..b47efc373 100644 --- a/pkg/coredata/finding_status.go +++ b/pkg/coredata/finding_status.go @@ -47,6 +47,7 @@ func (fs FindingStatus) String() string { func (fs *FindingStatus) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -72,6 +73,7 @@ func (fs *FindingStatus) Scan(value any) error { default: return fmt.Errorf("invalid FindingStatus value: %q", s) } + return nil } diff --git a/pkg/coredata/framework.go b/pkg/coredata/framework.go index 1dc2aefcb..ada707c04 100644 --- a/pkg/coredata/framework.go +++ b/pkg/coredata/framework.go @@ -62,6 +62,7 @@ func (f *Framework) AuthorizationAttributes(ctx context.Context, conn pg.Querier if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query framework authorization attributes: %w", err) } @@ -174,6 +175,7 @@ LIMIT 1; args := pgx.StrictNamedArgs{"reference_id": referenceID} maps.Copy(args, scope.SQLArguments()) + rows, err := conn.Query(ctx, q, args) if err != nil { return fmt.Errorf("cannot query frameworks: %w", err) @@ -222,6 +224,7 @@ LIMIT 1; args := pgx.StrictNamedArgs{"framework_id": frameworkID} maps.Copy(args, scope.SQLArguments()) + rows, err := conn.Query(ctx, q, args) if err != nil { return fmt.Errorf("cannot query frameworks: %w", err) @@ -330,8 +333,8 @@ VALUES ( "created_at": f.CreatedAt, "updated_at": f.UpdatedAt, } - _, err := conn.Exec(ctx, q, args) + _, err := conn.Exec(ctx, q, args) if err != nil { var pgErr *pgconn.PgError if errors.As(err, &pgErr) { @@ -366,6 +369,7 @@ WHERE q = fmt.Sprintf(q, scope.SQLFragment()) _, err := conn.Exec(ctx, q, args) + return err } @@ -396,5 +400,6 @@ WHERE maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) + return err } diff --git a/pkg/coredata/identity.go b/pkg/coredata/identity.go index 155dc5762..c8247d846 100644 --- a/pkg/coredata/identity.go +++ b/pkg/coredata/identity.go @@ -162,6 +162,7 @@ WHERE if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query identity iam attributes: %w", err) } @@ -202,7 +203,6 @@ VALUES ( } _, err := conn.Exec(ctx, q, args) - if err != nil { var pgErr *pgconn.PgError if errors.As(err, &pgErr) { @@ -314,6 +314,7 @@ WHERE args := pgx.StrictNamedArgs{"identity_id": i.ID} var count int + err := conn.QueryRow(ctx, q, args).Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count identity memberships: %w", err) diff --git a/pkg/coredata/invitation.go b/pkg/coredata/invitation.go index e6e3663c1..06cc4508b 100644 --- a/pkg/coredata/invitation.go +++ b/pkg/coredata/invitation.go @@ -136,6 +136,7 @@ WHERE } *i = invitation + return nil } @@ -152,12 +153,16 @@ WHERE LIMIT 1; ` - var email string - var organizationID gid.GID + var ( + email string + organizationID gid.GID + ) + if err := conn.QueryRow(ctx, q, i.ID).Scan(&email, &organizationID); err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query invitation iam attributes: %w", err) } @@ -276,6 +281,7 @@ WHERE } *i = invitations + return nil } diff --git a/pkg/coredata/invitation_order_field.go b/pkg/coredata/invitation_order_field.go index 37bbfc3d5..15cefe4a8 100644 --- a/pkg/coredata/invitation_order_field.go +++ b/pkg/coredata/invitation_order_field.go @@ -38,6 +38,7 @@ func (e InvitationOrderField) IsValid() bool { case InvitationOrderFieldCreatedAt: return true } + return false } @@ -50,6 +51,7 @@ func (e *InvitationOrderField) UnmarshalText(text []byte) error { if !e.IsValid() { return fmt.Errorf("%s is not a valid InvitationOrderField", string(text)) } + return nil } diff --git a/pkg/coredata/invitation_status.go b/pkg/coredata/invitation_status.go index a1f58ec34..8a2f61d00 100644 --- a/pkg/coredata/invitation_status.go +++ b/pkg/coredata/invitation_status.go @@ -37,6 +37,7 @@ func (tcv InvitationStatus) String() string { func (tcv *InvitationStatus) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -56,6 +57,7 @@ func (tcv *InvitationStatus) Scan(value any) error { default: return fmt.Errorf("invalid InvitationStatus value: %q", s) } + return nil } @@ -70,12 +72,16 @@ func (statuses InvitationStatuses) Value() (driver.Value, error) { var result strings.Builder result.WriteString("{") + for i, status := range statuses { if i > 0 { result.WriteString(",") } + fmt.Fprintf(&result, "%q", status.String()) } + result.WriteString("}") + return result.String(), nil } diff --git a/pkg/coredata/ip_country_block.go b/pkg/coredata/ip_country_block.go index 0c2d93c47..dd459e014 100644 --- a/pkg/coredata/ip_country_block.go +++ b/pkg/coredata/ip_country_block.go @@ -48,6 +48,7 @@ LIMIT 1; if err == pgx.ErrNoRows { return "", nil } + return "", fmt.Errorf("cannot collect ip country block row: %w", err) } diff --git a/pkg/coredata/mailing_list.go b/pkg/coredata/mailing_list.go index d59743576..544f0eb76 100644 --- a/pkg/coredata/mailing_list.go +++ b/pkg/coredata/mailing_list.go @@ -43,6 +43,7 @@ func (ml *MailingList) AuthorizationAttributes(ctx context.Context, conn pg.Quer if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query mailing list authorization attributes: %w", err) } @@ -85,6 +86,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect mailing list: %w", err) } diff --git a/pkg/coredata/mailing_list_subscriber.go b/pkg/coredata/mailing_list_subscriber.go index 65c27fb07..b078d70bb 100644 --- a/pkg/coredata/mailing_list_subscriber.go +++ b/pkg/coredata/mailing_list_subscriber.go @@ -52,6 +52,7 @@ func (cns *MailingListSubscriber) AuthorizationAttributes(ctx context.Context, c if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query mailing list subscriber authorization attributes: %w", err) } @@ -313,6 +314,7 @@ WHERE row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count mailing list subscribers: %w", err) diff --git a/pkg/coredata/mailing_list_subscriber_status.go b/pkg/coredata/mailing_list_subscriber_status.go index de5c596e0..928e6bdae 100644 --- a/pkg/coredata/mailing_list_subscriber_status.go +++ b/pkg/coredata/mailing_list_subscriber_status.go @@ -32,6 +32,7 @@ func (s MailingListSubscriberStatus) String() string { func (s *MailingListSubscriberStatus) Scan(value any) error { var str string + switch v := value.(type) { case string: str = v diff --git a/pkg/coredata/mailing_list_update.go b/pkg/coredata/mailing_list_update.go index 36ce5e6d2..ab4b51934 100644 --- a/pkg/coredata/mailing_list_update.go +++ b/pkg/coredata/mailing_list_update.go @@ -61,6 +61,7 @@ func (mlu *MailingListUpdate) AuthorizationAttributes(ctx context.Context, conn if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query mailing list update authorization attributes: %w", err) } @@ -104,6 +105,7 @@ INSERT INTO mailing_list_updates ( maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) + return err } @@ -134,9 +136,11 @@ WHERE if err != nil { return fmt.Errorf("cannot update mailing list update: %w", err) } + if tag.RowsAffected() == 0 { return ErrResourceNotFound } + return nil } @@ -158,9 +162,11 @@ WHERE if err != nil { return fmt.Errorf("cannot delete mailing list update: %w", err) } + if tag.RowsAffected() == 0 { return ErrResourceNotFound } + return nil } @@ -198,10 +204,12 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect mailing list update: %w", err) } *mlu = result + return nil } @@ -249,6 +257,7 @@ WHERE } *mlul = results + return nil } @@ -295,6 +304,7 @@ WHERE } *mlul = results + return nil } @@ -358,10 +368,12 @@ FOR UPDATE SKIP LOCKED if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect enqueued mailing list update: %w", err) } *mlu = result + return nil } @@ -376,6 +388,7 @@ SET status = 'ENQUEUED', updated_at = NOW() WHERE status = 'PROCESSING' AND updated_at < NOW() - @stale_after::interval ` + _, err := conn.Exec(ctx, q, pgx.StrictNamedArgs{"stale_after": staleAfter}) if err != nil { return fmt.Errorf("cannot reset stale processing mailing list updates: %w", err) diff --git a/pkg/coredata/mailing_list_update_status.go b/pkg/coredata/mailing_list_update_status.go index aad7b0acf..90799e311 100644 --- a/pkg/coredata/mailing_list_update_status.go +++ b/pkg/coredata/mailing_list_update_status.go @@ -34,6 +34,7 @@ func (s MailingListUpdateStatus) String() string { func (s *MailingListUpdateStatus) Scan(value any) error { var str string + switch v := value.(type) { case string: str = v diff --git a/pkg/coredata/measure.go b/pkg/coredata/measure.go index 217149dce..32d944715 100644 --- a/pkg/coredata/measure.go +++ b/pkg/coredata/measure.go @@ -65,6 +65,7 @@ func (m *Measure) AuthorizationAttributes(ctx context.Context, conn pg.Querier) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query measure authorization attributes: %w", err) } @@ -626,8 +627,8 @@ VALUES ( "updated_at": m.UpdatedAt, "state": m.State, } - _, err := conn.Exec(ctx, q, args) + _, err := conn.Exec(ctx, q, args) if err != nil { var pgErr *pgconn.PgError if errors.As(err, &pgErr) { @@ -635,6 +636,7 @@ VALUES ( return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot insert measure: %w", err) } @@ -671,6 +673,7 @@ WHERE %s maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) + return err } @@ -691,5 +694,6 @@ WHERE %s maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) + return err } diff --git a/pkg/coredata/measure_document.go b/pkg/coredata/measure_document.go index f0d47eac3..172bcc404 100644 --- a/pkg/coredata/measure_document.go +++ b/pkg/coredata/measure_document.go @@ -69,8 +69,8 @@ VALUES ( "tenant_id": scope.GetTenantID(), "created_at": md.CreatedAt, } - _, err := conn.Exec(ctx, q, args) + _, err := conn.Exec(ctx, q, args) if err != nil { var pgErr *pgconn.PgError if errors.As(err, &pgErr) { @@ -111,6 +111,7 @@ WHERE q = fmt.Sprintf(q, scope.SQLFragment()) _, err := conn.Exec(ctx, q, args) + return err } diff --git a/pkg/coredata/member_role.go b/pkg/coredata/member_role.go index cf68fd26f..fab1439d3 100644 --- a/pkg/coredata/member_role.go +++ b/pkg/coredata/member_role.go @@ -35,6 +35,7 @@ func (r MembershipRole) String() string { func (r *MembershipRole) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -58,6 +59,7 @@ func (r *MembershipRole) Scan(value any) error { default: return fmt.Errorf("invalid MembershipRole value: %q", s) } + return nil } diff --git a/pkg/coredata/membership.go b/pkg/coredata/membership.go index 51bc488db..cd2e6f09e 100644 --- a/pkg/coredata/membership.go +++ b/pkg/coredata/membership.go @@ -99,6 +99,7 @@ WHERE } *m = membership + return nil } @@ -195,6 +196,7 @@ WHERE } *m = membership + return nil } @@ -211,9 +213,12 @@ WHERE LIMIT 1; ` - var identityID gid.GID - var organizationID gid.GID - var role MembershipRole + var ( + identityID gid.GID + organizationID gid.GID + role MembershipRole + ) + if err := conn.QueryRow(ctx, q, m.ID).Scan( &identityID, &organizationID, @@ -222,6 +227,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query membership iam attributes: %w", err) } @@ -278,6 +284,7 @@ WHERE } *m = membership + return nil } @@ -390,5 +397,6 @@ LIMIT 1 } *m = *membership + return nil } diff --git a/pkg/coredata/membership_order_field.go b/pkg/coredata/membership_order_field.go index 52a5f553a..c2220cdc5 100644 --- a/pkg/coredata/membership_order_field.go +++ b/pkg/coredata/membership_order_field.go @@ -39,6 +39,7 @@ func (p MembershipOrderField) Column() string { case MembershipOrderFieldCreatedAt: return "created_at" } + return string(p) } diff --git a/pkg/coredata/membership_profile.go b/pkg/coredata/membership_profile.go index 972e32a15..a61d7eb29 100644 --- a/pkg/coredata/membership_profile.go +++ b/pkg/coredata/membership_profile.go @@ -90,12 +90,16 @@ func (p MembershipProfile) CursorKey(orderBy MembershipProfileOrderField) page.C func (p *MembershipProfile) AuthorizationAttributes(ctx context.Context, conn pg.Querier) (map[string]string, error) { q := `SELECT organization_id, identity_id FROM iam_membership_profiles WHERE id = $1 LIMIT 1;` - var organizationID gid.GID - var identityID gid.GID + var ( + organizationID gid.GID + identityID gid.GID + ) + if err := conn.QueryRow(ctx, q, p.ID).Scan(&organizationID, &identityID); err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query profile authorization attributes: %w", err) } @@ -903,6 +907,7 @@ WHERE maps.Copy(args, scope.SQLArguments()) var count int + err := conn.QueryRow(ctx, q, args).Scan(&count) if err != nil { return 0, fmt.Errorf("cannot query document version approver profiles count: %w", err) @@ -1012,6 +1017,7 @@ WHERE row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot collect count: %w", err) @@ -1048,6 +1054,7 @@ WHERE row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot collect count: %w", err) @@ -1087,6 +1094,7 @@ WHERE row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot collect count: %w", err) diff --git a/pkg/coredata/mfa_status.go b/pkg/coredata/mfa_status.go index 4e71dc7c0..1265ff826 100644 --- a/pkg/coredata/mfa_status.go +++ b/pkg/coredata/mfa_status.go @@ -41,6 +41,7 @@ func (m MFAStatus) String() string { func (m *MFAStatus) Scan(value any) error { var str string + switch v := value.(type) { case string: str = v @@ -60,6 +61,7 @@ func (m *MFAStatus) Scan(value any) error { default: return fmt.Errorf("cannot parse MFAStatus: invalid value %q", str) } + return nil } diff --git a/pkg/coredata/oauth2_access_token.go b/pkg/coredata/oauth2_access_token.go index 10b7ae5ff..45be58e06 100644 --- a/pkg/coredata/oauth2_access_token.go +++ b/pkg/coredata/oauth2_access_token.go @@ -112,6 +112,7 @@ LIMIT 1; } *t = token + return nil } @@ -158,6 +159,7 @@ LIMIT 1; } *t = token + return nil } diff --git a/pkg/coredata/oauth2_authorization_code.go b/pkg/coredata/oauth2_authorization_code.go index 018e5955f..34ac936b1 100644 --- a/pkg/coredata/oauth2_authorization_code.go +++ b/pkg/coredata/oauth2_authorization_code.go @@ -152,6 +152,7 @@ FOR UPDATE; } *c = code + return nil } diff --git a/pkg/coredata/oauth2_claim.go b/pkg/coredata/oauth2_claim.go index cfef14eff..1e8058ddd 100644 --- a/pkg/coredata/oauth2_claim.go +++ b/pkg/coredata/oauth2_claim.go @@ -58,6 +58,7 @@ func (c *OAuth2Claim) UnmarshalText(text []byte) error { if !c.IsValid() { return fmt.Errorf("%s is not a valid OAuth2Claim", string(text)) } + return nil } diff --git a/pkg/coredata/oauth2_client.go b/pkg/coredata/oauth2_client.go index 3fd3f78d3..232eb487e 100644 --- a/pkg/coredata/oauth2_client.go +++ b/pkg/coredata/oauth2_client.go @@ -88,6 +88,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query oauth2 client authorization attributes: %w", err) } @@ -150,6 +151,7 @@ LIMIT 1; } *c = client + return nil } @@ -206,6 +208,7 @@ WHERE } *c = clients + return nil } diff --git a/pkg/coredata/oauth2_client_order_field.go b/pkg/coredata/oauth2_client_order_field.go index 27b991194..9ea87c1a6 100644 --- a/pkg/coredata/oauth2_client_order_field.go +++ b/pkg/coredata/oauth2_client_order_field.go @@ -36,6 +36,7 @@ func (f OAuth2ClientOrderField) IsValid() bool { case OAuth2ClientOrderFieldCreatedAt: return true } + return false } @@ -48,6 +49,7 @@ func (f *OAuth2ClientOrderField) UnmarshalText(text []byte) error { if !f.IsValid() { return fmt.Errorf("%s is not a valid OAuth2ClientOrderField", string(text)) } + return nil } diff --git a/pkg/coredata/oauth2_consent.go b/pkg/coredata/oauth2_consent.go index 5e6a0aac3..4a4cac90d 100644 --- a/pkg/coredata/oauth2_consent.go +++ b/pkg/coredata/oauth2_consent.go @@ -129,6 +129,7 @@ LIMIT 1; } *c = consent + return nil } @@ -187,6 +188,7 @@ LIMIT 1; } *c = consent + return nil } @@ -246,6 +248,7 @@ FOR UPDATE; } *c = consent + return nil } @@ -306,6 +309,7 @@ LIMIT 1; } *c = consent + return nil } @@ -465,6 +469,7 @@ WHERE } *c = consents + return nil } @@ -484,6 +489,7 @@ WHERE ` var count int + err := conn.QueryRow( ctx, q, diff --git a/pkg/coredata/oauth2_device_code.go b/pkg/coredata/oauth2_device_code.go index 6afcaeb8d..9a5afcf92 100644 --- a/pkg/coredata/oauth2_device_code.go +++ b/pkg/coredata/oauth2_device_code.go @@ -151,6 +151,7 @@ FOR UPDATE; } *d = code + return nil } @@ -194,6 +195,7 @@ FOR UPDATE; } *d = code + return nil } @@ -246,6 +248,7 @@ FOR UPDATE; } *d = code + return nil } diff --git a/pkg/coredata/oauth2_device_code_test.go b/pkg/coredata/oauth2_device_code_test.go index 11584fa74..104a222b4 100644 --- a/pkg/coredata/oauth2_device_code_test.go +++ b/pkg/coredata/oauth2_device_code_test.go @@ -40,6 +40,7 @@ func TestOAuth2UserCode_Format(t *testing.T) { t.Parallel() code := coredata.OAuth2UserCode("ABC") + assert.Panics(t, func() { code.Format() }) }, ) @@ -50,6 +51,7 @@ func TestOAuth2UserCode_Format(t *testing.T) { t.Parallel() code := coredata.OAuth2UserCode("ABCDEFGHIJ") + assert.Panics(t, func() { code.Format() }) }, ) @@ -60,6 +62,7 @@ func TestOAuth2UserCode_Format(t *testing.T) { t.Parallel() code := coredata.OAuth2UserCode("") + assert.Panics(t, func() { code.Format() }) }, ) diff --git a/pkg/coredata/oauth2_refresh_token.go b/pkg/coredata/oauth2_refresh_token.go index 26b656d77..af2205112 100644 --- a/pkg/coredata/oauth2_refresh_token.go +++ b/pkg/coredata/oauth2_refresh_token.go @@ -125,6 +125,7 @@ LIMIT 1; } *t = token + return nil } @@ -175,6 +176,7 @@ LIMIT 1; } *t = token + return nil } @@ -225,6 +227,7 @@ FOR UPDATE; } *t = token + return nil } diff --git a/pkg/coredata/oauth2_scope.go b/pkg/coredata/oauth2_scope.go index 3afba2def..c602b786d 100644 --- a/pkg/coredata/oauth2_scope.go +++ b/pkg/coredata/oauth2_scope.go @@ -96,6 +96,7 @@ func (s OAuth2Scopes) OrDefault(defaultScopes OAuth2Scopes) OAuth2Scopes { if len(s) == 0 { return defaultScopes } + return s } @@ -107,6 +108,7 @@ func (s *OAuth2Scopes) UnmarshalText(text []byte) error { } fields := strings.Fields(str) + scopes := make(OAuth2Scopes, len(fields)) for i, f := range fields { if err := scopes[i].UnmarshalText([]byte(f)); err != nil { @@ -115,5 +117,6 @@ func (s *OAuth2Scopes) UnmarshalText(text []byte) error { } *s = scopes + return nil } diff --git a/pkg/coredata/oauth2_scope_test.go b/pkg/coredata/oauth2_scope_test.go index 0ab594ce4..38c457d7f 100644 --- a/pkg/coredata/oauth2_scope_test.go +++ b/pkg/coredata/oauth2_scope_test.go @@ -52,6 +52,7 @@ func TestOAuth2Scope_UnmarshalText(t *testing.T) { t.Parallel() var scope coredata.OAuth2Scope + err := scope.UnmarshalText([]byte("offline_access")) assert.NoError(t, err) assert.Equal(t, coredata.OAuth2ScopeOfflineAccess, scope) @@ -64,6 +65,7 @@ func TestOAuth2Scope_UnmarshalText(t *testing.T) { t.Parallel() var scope coredata.OAuth2Scope + err := scope.UnmarshalText([]byte("admin")) assert.Error(t, err) }, @@ -114,6 +116,7 @@ func TestOAuth2Scopes_OrDefault(t *testing.T) { t.Parallel() var scopes coredata.OAuth2Scopes + result := scopes.OrDefault(defaultScopes) assert.Equal(t, defaultScopes, result) }, diff --git a/pkg/coredata/obligation.go b/pkg/coredata/obligation.go index d01dbb35b..b0657d675 100644 --- a/pkg/coredata/obligation.go +++ b/pkg/coredata/obligation.go @@ -71,6 +71,7 @@ func (o *Obligation) AuthorizationAttributes(ctx context.Context, conn pg.Querie if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query obligation authorization attributes: %w", err) } @@ -151,6 +152,7 @@ WHERE row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count obligations: %w", err) @@ -193,6 +195,7 @@ WHERE %s row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count obligations: %w", err) @@ -361,6 +364,7 @@ WHERE %s row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count obligations: %w", err) @@ -656,6 +660,7 @@ WHERE if errors.Is(err, pgx.ErrNoRows) { return nil, nil } + if err != nil { return nil, fmt.Errorf("cannot get obligation list document ID: %w", err) } diff --git a/pkg/coredata/obligation_order_field.go b/pkg/coredata/obligation_order_field.go index 85a1b4553..00fc7d25c 100644 --- a/pkg/coredata/obligation_order_field.go +++ b/pkg/coredata/obligation_order_field.go @@ -49,5 +49,6 @@ func (p *ObligationOrderField) UnmarshalText(text []byte) error { *p = ObligationOrderField(val) return nil } + return fmt.Errorf("invalid ObligationOrderField value: %q", val) } diff --git a/pkg/coredata/obligation_status.go b/pkg/coredata/obligation_status.go index 48b228423..df7a81bba 100644 --- a/pkg/coredata/obligation_status.go +++ b/pkg/coredata/obligation_status.go @@ -41,6 +41,7 @@ func (os ObligationStatus) String() string { func (os *ObligationStatus) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -60,6 +61,7 @@ func (os *ObligationStatus) Scan(value any) error { default: return fmt.Errorf("invalid ObligationStatus value: %q", s) } + return nil } diff --git a/pkg/coredata/obligation_type.go b/pkg/coredata/obligation_type.go index 2acd27a5e..cc935c444 100644 --- a/pkg/coredata/obligation_type.go +++ b/pkg/coredata/obligation_type.go @@ -39,6 +39,7 @@ func (ot ObligationType) String() string { func (ot *ObligationType) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -56,6 +57,7 @@ func (ot *ObligationType) Scan(value any) error { default: return fmt.Errorf("invalid ObligationType value: %q", s) } + return nil } diff --git a/pkg/coredata/oidc_provider.go b/pkg/coredata/oidc_provider.go index d00f4d1b6..66dc08ac5 100644 --- a/pkg/coredata/oidc_provider.go +++ b/pkg/coredata/oidc_provider.go @@ -28,6 +28,7 @@ func (p OIDCProvider) IsValid() bool { case OIDCProviderGoogle, OIDCProviderMicrosoft: return true } + return false } @@ -38,6 +39,7 @@ func (p *OIDCProvider) UnmarshalText(text []byte) error { if !p.IsValid() { return fmt.Errorf("%s is not a valid OIDCProvider", string(text)) } + return nil } diff --git a/pkg/coredata/oidc_state.go b/pkg/coredata/oidc_state.go index 38c472e26..590dd8f57 100644 --- a/pkg/coredata/oidc_state.go +++ b/pkg/coredata/oidc_state.go @@ -76,10 +76,12 @@ FOR UPDATE if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect oidc_state: %w", err) } *s = state + return nil } diff --git a/pkg/coredata/organization.go b/pkg/coredata/organization.go index 70c97a0fc..4daf09078 100644 --- a/pkg/coredata/organization.go +++ b/pkg/coredata/organization.go @@ -54,6 +54,7 @@ func (o *Organization) AuthorizationAttributes(ctx context.Context, conn pg.Quer if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query organization authorization attributes: %w", err) } diff --git a/pkg/coredata/personal_api_key.go b/pkg/coredata/personal_api_key.go index b10a8ff6f..c37e99437 100644 --- a/pkg/coredata/personal_api_key.go +++ b/pkg/coredata/personal_api_key.go @@ -101,6 +101,7 @@ func (a *PersonalAPIKey) AuthorizationAttributes(ctx context.Context, conn pg.Qu if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query personal api key iam attributes: %w", err) } @@ -159,6 +160,7 @@ ORDER BY created_at DESC; args := pgx.StrictNamedArgs{"identity_id": identityID} row := conn.QueryRow(ctx, q, args) + var count int if err := row.Scan(&count); err != nil { return 0, fmt.Errorf("cannot scan count: %w", err) diff --git a/pkg/coredata/pg_checkpointer_test.go b/pkg/coredata/pg_checkpointer_test.go index 0337741d1..2dd962698 100644 --- a/pkg/coredata/pg_checkpointer_test.go +++ b/pkg/coredata/pg_checkpointer_test.go @@ -37,6 +37,7 @@ func TestPGCheckpointer(t *testing.T) { "load returns nil when no checkpoint exists", func(t *testing.T) { t.Parallel() + ctx := context.Background() run := agentruntest.InsertPendingRun( t, @@ -55,6 +56,7 @@ func TestPGCheckpointer(t *testing.T) { "save and load round-trip", func(t *testing.T) { t.Parallel() + ctx := context.Background() run := agentruntest.InsertPendingRun( t, @@ -96,6 +98,7 @@ func TestPGCheckpointer(t *testing.T) { "save overwrites previous checkpoint", func(t *testing.T) { t.Parallel() + ctx := context.Background() run := agentruntest.InsertPendingRun( t, @@ -138,6 +141,7 @@ func TestPGCheckpointer(t *testing.T) { "save and load preserves approval state", func(t *testing.T) { t.Parallel() + ctx := context.Background() run := agentruntest.InsertPendingRun( t, @@ -196,6 +200,7 @@ func TestPGCheckpointer(t *testing.T) { "save to nonexistent run returns error", func(t *testing.T) { t.Parallel() + ctx := context.Background() run := agentruntest.InsertPendingRun( t, diff --git a/pkg/coredata/processing_activities.go b/pkg/coredata/processing_activities.go index 8b0e5c419..2f3c74604 100644 --- a/pkg/coredata/processing_activities.go +++ b/pkg/coredata/processing_activities.go @@ -49,6 +49,7 @@ WHERE if errors.Is(err, pgx.ErrNoRows) { return nil, nil } + if err != nil { return nil, fmt.Errorf("cannot get processing activity list document ID: %w", err) } @@ -183,6 +184,7 @@ func (p *ProcessingActivity) AuthorizationAttributes(ctx context.Context, conn p if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query processing activity authorization attributes: %w", err) } @@ -272,6 +274,7 @@ WHERE row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count processing activities: %w", err) diff --git a/pkg/coredata/processing_activity_data_protection_impact_assessment.go b/pkg/coredata/processing_activity_data_protection_impact_assessment.go index 36d681e57..14e8c680a 100644 --- a/pkg/coredata/processing_activity_data_protection_impact_assessment.go +++ b/pkg/coredata/processing_activity_data_protection_impact_assessment.go @@ -39,6 +39,7 @@ func (p ProcessingActivityDataProtectionImpactAssessment) String() string { func (p *ProcessingActivityDataProtectionImpactAssessment) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -56,6 +57,7 @@ func (p *ProcessingActivityDataProtectionImpactAssessment) Scan(value any) error default: return fmt.Errorf("invalid ProcessingActivityDataProtectionImpactAssessment value: %q", s) } + return nil } diff --git a/pkg/coredata/processing_activity_lawful_basis.go b/pkg/coredata/processing_activity_lawful_basis.go index 4583c43e2..d57eb8525 100644 --- a/pkg/coredata/processing_activity_lawful_basis.go +++ b/pkg/coredata/processing_activity_lawful_basis.go @@ -47,6 +47,7 @@ func (p ProcessingActivityLawfulBasis) String() string { func (p *ProcessingActivityLawfulBasis) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -72,6 +73,7 @@ func (p *ProcessingActivityLawfulBasis) Scan(value any) error { default: return fmt.Errorf("invalid ProcessingActivityLawfulBasis value: %q", s) } + return nil } diff --git a/pkg/coredata/processing_activity_order_field.go b/pkg/coredata/processing_activity_order_field.go index ed15fb7f0..b81c85d6f 100644 --- a/pkg/coredata/processing_activity_order_field.go +++ b/pkg/coredata/processing_activity_order_field.go @@ -45,5 +45,6 @@ func (p *ProcessingActivityOrderField) UnmarshalText(text []byte) error { *p = ProcessingActivityOrderField(val) return nil } + return fmt.Errorf("invalid ProcessingActivityOrderField value: %q", val) } diff --git a/pkg/coredata/processing_activity_role.go b/pkg/coredata/processing_activity_role.go index 36956caee..3c46db2c9 100644 --- a/pkg/coredata/processing_activity_role.go +++ b/pkg/coredata/processing_activity_role.go @@ -39,6 +39,7 @@ func (p ProcessingActivityRole) String() string { func (p *ProcessingActivityRole) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -56,6 +57,7 @@ func (p *ProcessingActivityRole) Scan(value any) error { default: return fmt.Errorf("invalid ProcessingActivityRole value: %q", s) } + return nil } diff --git a/pkg/coredata/processing_activity_special_or_criminal_data.go b/pkg/coredata/processing_activity_special_or_criminal_data.go index af18198cd..19ff0977b 100644 --- a/pkg/coredata/processing_activity_special_or_criminal_data.go +++ b/pkg/coredata/processing_activity_special_or_criminal_data.go @@ -41,6 +41,7 @@ func (p ProcessingActivitySpecialOrCriminalDatum) String() string { func (p *ProcessingActivitySpecialOrCriminalDatum) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -60,6 +61,7 @@ func (p *ProcessingActivitySpecialOrCriminalDatum) Scan(value any) error { default: return fmt.Errorf("invalid ProcessingActivitySpecialOrCriminalDatum value: %q", s) } + return nil } diff --git a/pkg/coredata/processing_activity_transfer_impact_assessment.go b/pkg/coredata/processing_activity_transfer_impact_assessment.go index fb76c5a8a..c8fcf6154 100644 --- a/pkg/coredata/processing_activity_transfer_impact_assessment.go +++ b/pkg/coredata/processing_activity_transfer_impact_assessment.go @@ -39,6 +39,7 @@ func (p ProcessingActivityTransferImpactAssessment) String() string { func (p *ProcessingActivityTransferImpactAssessment) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -56,6 +57,7 @@ func (p *ProcessingActivityTransferImpactAssessment) Scan(value any) error { default: return fmt.Errorf("invalid ProcessingActivityTransferImpactAssessment value: %q", s) } + return nil } diff --git a/pkg/coredata/processing_activity_transfer_safeguards.go b/pkg/coredata/processing_activity_transfer_safeguards.go index 49553e380..768c4fc3c 100644 --- a/pkg/coredata/processing_activity_transfer_safeguards.go +++ b/pkg/coredata/processing_activity_transfer_safeguards.go @@ -47,6 +47,7 @@ func (p ProcessingActivityTransferSafeguard) String() string { func (p *ProcessingActivityTransferSafeguard) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -72,6 +73,7 @@ func (p *ProcessingActivityTransferSafeguard) Scan(value any) error { default: return fmt.Errorf("invalid ProcessingActivityTransferSafeguard value: %q", s) } + return nil } diff --git a/pkg/coredata/profile_source.go b/pkg/coredata/profile_source.go index 92d5b4045..8d2cde77f 100644 --- a/pkg/coredata/profile_source.go +++ b/pkg/coredata/profile_source.go @@ -33,6 +33,7 @@ func (s ProfileSource) String() string { func (s *ProfileSource) Scan(value any) error { var str string + switch v := value.(type) { case string: str = v @@ -52,6 +53,7 @@ func (s *ProfileSource) Scan(value any) error { default: return fmt.Errorf("invalid ProfileSource value: %q", str) } + return nil } diff --git a/pkg/coredata/profile_state.go b/pkg/coredata/profile_state.go index 6b47231ec..2b08480d9 100644 --- a/pkg/coredata/profile_state.go +++ b/pkg/coredata/profile_state.go @@ -32,6 +32,7 @@ func (s ProfileState) String() string { func (s *ProfileState) Scan(value any) error { var str string + switch v := value.(type) { case string: str = v @@ -49,6 +50,7 @@ func (s *ProfileState) Scan(value any) error { default: return fmt.Errorf("invalid ProfileState value: %q", str) } + return nil } diff --git a/pkg/coredata/regulation.go b/pkg/coredata/regulation.go index 5fa483d9d..9a6a57649 100644 --- a/pkg/coredata/regulation.go +++ b/pkg/coredata/regulation.go @@ -102,6 +102,7 @@ func (r Regulation) String() string { func (r *Regulation) Scan(value any) error { var v string + switch val := value.(type) { case string: v = val @@ -117,6 +118,7 @@ func (r *Regulation) Scan(value any) error { } *r = parsed + return nil } @@ -148,5 +150,6 @@ func (r *Regulation) UnmarshalJSON(data []byte) error { } *r = parsed + return nil } diff --git a/pkg/coredata/report.go b/pkg/coredata/report.go index 74f70a90c..e0c213612 100644 --- a/pkg/coredata/report.go +++ b/pkg/coredata/report.go @@ -50,6 +50,7 @@ func (r *Report) AuthorizationAttributes(ctx context.Context, conn pg.Querier) ( if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query report authorization attributes: %w", err) } diff --git a/pkg/coredata/rights_request_state.go b/pkg/coredata/rights_request_state.go index b096e278f..c0ac0748b 100644 --- a/pkg/coredata/rights_request_state.go +++ b/pkg/coredata/rights_request_state.go @@ -41,6 +41,7 @@ func (rrs RightsRequestState) String() string { func (rrs *RightsRequestState) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -60,6 +61,7 @@ func (rrs *RightsRequestState) Scan(value any) error { default: return fmt.Errorf("invalid RightsRequestState value: %q", s) } + return nil } diff --git a/pkg/coredata/rights_request_type.go b/pkg/coredata/rights_request_type.go index eff392be0..fd5b17272 100644 --- a/pkg/coredata/rights_request_type.go +++ b/pkg/coredata/rights_request_type.go @@ -41,6 +41,7 @@ func (rrt RightsRequestType) String() string { func (rrt *RightsRequestType) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -60,6 +61,7 @@ func (rrt *RightsRequestType) Scan(value any) error { default: return fmt.Errorf("invalid RightsRequestType value: %q", s) } + return nil } diff --git a/pkg/coredata/rights_requests.go b/pkg/coredata/rights_requests.go index 252469dcd..13d65399f 100644 --- a/pkg/coredata/rights_requests.go +++ b/pkg/coredata/rights_requests.go @@ -69,6 +69,7 @@ func (rr *RightsRequest) AuthorizationAttributes(ctx context.Context, conn pg.Qu if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query rights request authorization attributes: %w", err) } @@ -150,6 +151,7 @@ WHERE row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count rights requests: %w", err) diff --git a/pkg/coredata/risk.go b/pkg/coredata/risk.go index baec736db..11cc4b371 100644 --- a/pkg/coredata/risk.go +++ b/pkg/coredata/risk.go @@ -49,6 +49,7 @@ WHERE if errors.Is(err, pgx.ErrNoRows) { return nil, nil } + if err != nil { return nil, fmt.Errorf("cannot get risk list document ID: %w", err) } @@ -189,6 +190,7 @@ func (r *Risk) AuthorizationAttributes(ctx context.Context, conn pg.Querier) (ma if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query risk authorization attributes: %w", err) } @@ -620,6 +622,7 @@ VALUES (@id, @tenant_id, @organization_id, @name, @description, @category, @owne } _, err := conn.Exec(ctx, q, args) + return err } @@ -690,6 +693,7 @@ DELETE FROM risks WHERE %s AND id = @id maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) + return err } diff --git a/pkg/coredata/risk_document.go b/pkg/coredata/risk_document.go index 55a4e7f25..e6592c203 100644 --- a/pkg/coredata/risk_document.go +++ b/pkg/coredata/risk_document.go @@ -68,6 +68,7 @@ VALUES ( "created_at": rp.CreatedAt, } _, err := conn.Exec(ctx, q, args) + return err } @@ -97,6 +98,7 @@ WHERE maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) + return err } diff --git a/pkg/coredata/risk_mesure.go b/pkg/coredata/risk_mesure.go index b757fb6c2..f57a58938 100644 --- a/pkg/coredata/risk_mesure.go +++ b/pkg/coredata/risk_mesure.go @@ -68,6 +68,7 @@ VALUES ( "created_at": rm.CreatedAt, } _, err := conn.Exec(ctx, q, args) + return err } @@ -97,5 +98,6 @@ WHERE maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) + return err } diff --git a/pkg/coredata/saml_configuration.go b/pkg/coredata/saml_configuration.go index 399f8d416..e8eb43510 100644 --- a/pkg/coredata/saml_configuration.go +++ b/pkg/coredata/saml_configuration.go @@ -71,6 +71,7 @@ func (s *SAMLConfiguration) AuthorizationAttributes(ctx context.Context, conn pg if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query saml configuration authorization attributes: %w", err) } @@ -505,6 +506,7 @@ WHERE } var count int + err = rows.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot collect count: %w", err) @@ -572,6 +574,7 @@ WHERE ` row := conn.QueryRow(ctx, q, pgx.StrictNamedArgs{"email_domain": emailDomain}) + var count int if err := row.Scan(&count); err != nil { return 0, fmt.Errorf("cannot count SAML configurations: %w", err) diff --git a/pkg/coredata/saml_enforcement_policy.go b/pkg/coredata/saml_enforcement_policy.go index e4d454764..b5cd17ed2 100644 --- a/pkg/coredata/saml_enforcement_policy.go +++ b/pkg/coredata/saml_enforcement_policy.go @@ -33,6 +33,7 @@ func (sep SAMLEnforcementPolicy) String() string { func (sep *SAMLEnforcementPolicy) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -52,6 +53,7 @@ func (sep *SAMLEnforcementPolicy) Scan(value any) error { default: return fmt.Errorf("invalid SAMLEnforcementPolicy value: %q", s) } + return nil } diff --git a/pkg/coredata/saml_request.go b/pkg/coredata/saml_request.go index 477987887..c8d40ae2b 100644 --- a/pkg/coredata/saml_request.go +++ b/pkg/coredata/saml_request.go @@ -79,7 +79,9 @@ WHERE organization_id = @organization_id AND expires_at > @now requestIDs, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (string, error) { var id string + err := row.Scan(&id) + return id, err }) if err != nil { diff --git a/pkg/coredata/scim_bridge.go b/pkg/coredata/scim_bridge.go index bcfcb6775..a2eb442f3 100644 --- a/pkg/coredata/scim_bridge.go +++ b/pkg/coredata/scim_bridge.go @@ -68,6 +68,7 @@ func (s *SCIMBridge) AuthorizationAttributes(ctx context.Context, conn pg.Querie if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query scim bridge authorization attributes: %w", err) } @@ -409,10 +410,12 @@ FOR UPDATE SKIP LOCKED if errors.Is(err, pgx.ErrNoRows) { return ErrNoSCIMBridgeAvailable } + return fmt.Errorf("cannot collect scim_bridge: %w", err) } *s = bridge + return nil } diff --git a/pkg/coredata/scim_bridge_state.go b/pkg/coredata/scim_bridge_state.go index eadc88026..3608f396e 100644 --- a/pkg/coredata/scim_bridge_state.go +++ b/pkg/coredata/scim_bridge_state.go @@ -35,6 +35,7 @@ func (s SCIMBridgeState) String() string { func (s *SCIMBridgeState) Scan(value any) error { var str string + switch v := value.(type) { case string: str = v @@ -58,6 +59,7 @@ func (s *SCIMBridgeState) Scan(value any) error { default: return fmt.Errorf("invalid SCIMBridgeState value: %q", str) } + return nil } diff --git a/pkg/coredata/scim_bridge_type.go b/pkg/coredata/scim_bridge_type.go index bfc099cfe..11d17530a 100644 --- a/pkg/coredata/scim_bridge_type.go +++ b/pkg/coredata/scim_bridge_type.go @@ -32,6 +32,7 @@ func (t SCIMBridgeType) String() string { func (t *SCIMBridgeType) Scan(value any) error { var str string + switch v := value.(type) { case string: str = v @@ -49,6 +50,7 @@ func (t *SCIMBridgeType) Scan(value any) error { default: return fmt.Errorf("invalid SCIMBridgeType value: %q", str) } + return nil } diff --git a/pkg/coredata/scim_configuration.go b/pkg/coredata/scim_configuration.go index 82a3a69cb..039b0d4eb 100644 --- a/pkg/coredata/scim_configuration.go +++ b/pkg/coredata/scim_configuration.go @@ -58,6 +58,7 @@ func (s *SCIMConfiguration) AuthorizationAttributes(ctx context.Context, conn pg if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query scim configuration authorization attributes: %w", err) } diff --git a/pkg/coredata/scim_event.go b/pkg/coredata/scim_event.go index a5e8fa32e..f80804af9 100644 --- a/pkg/coredata/scim_event.go +++ b/pkg/coredata/scim_event.go @@ -64,6 +64,7 @@ func (s *SCIMEvent) AuthorizationAttributes(ctx context.Context, conn pg.Querier if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query scim event authorization attributes: %w", err) } @@ -255,6 +256,7 @@ WHERE maps.Copy(args, scope.SQLArguments()) row := conn.QueryRow(ctx, q, args) + var count int if err := row.Scan(&count); err != nil { return 0, fmt.Errorf("cannot count scim_events: %w", err) @@ -335,6 +337,7 @@ WHERE maps.Copy(args, scope.SQLArguments()) row := conn.QueryRow(ctx, q, args) + var count int if err := row.Scan(&count); err != nil { return 0, fmt.Errorf("cannot count scim_events: %w", err) diff --git a/pkg/coredata/search_engine_indexing.go b/pkg/coredata/search_engine_indexing.go index 2380a05c8..85e341af8 100644 --- a/pkg/coredata/search_engine_indexing.go +++ b/pkg/coredata/search_engine_indexing.go @@ -35,6 +35,7 @@ func (s SearchEngineIndexing) IsValid() bool { case SearchEngineIndexingIndexable, SearchEngineIndexingNotIndexable: return true } + return false } @@ -43,6 +44,7 @@ func (s *SearchEngineIndexing) UnmarshalText(text []byte) error { if !s.IsValid() { return fmt.Errorf("%s is not a valid SearchEngineIndexing", string(text)) } + return nil } @@ -52,6 +54,7 @@ func (s SearchEngineIndexing) MarshalText() ([]byte, error) { func (s *SearchEngineIndexing) Scan(value any) error { var str string + switch v := value.(type) { case string: str = v @@ -69,6 +72,7 @@ func (s *SearchEngineIndexing) Scan(value any) error { default: return fmt.Errorf("invalid SearchEngineIndexing value: %q", str) } + return nil } diff --git a/pkg/coredata/session.go b/pkg/coredata/session.go index 9f44a051f..275476a66 100644 --- a/pkg/coredata/session.go +++ b/pkg/coredata/session.go @@ -62,6 +62,7 @@ const ( func NewRootSession(identityID gid.GID, method AuthMethod, duration time.Duration) *Session { now := time.Now() + return &Session{ ID: gid.New(gid.NilTenant, SessionEntityType), IdentityID: identityID, @@ -137,6 +138,7 @@ LIMIT 1; return fmt.Errorf("cannot collect session: %w", err) } + *s = session return nil @@ -160,6 +162,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query session iam attributes: %w", err) } @@ -209,6 +212,7 @@ VALUES ( } _, err := conn.Exec(ctx, q, args) + return err } diff --git a/pkg/coredata/slack_message.go b/pkg/coredata/slack_message.go index df3fa4526..a84a32892 100644 --- a/pkg/coredata/slack_message.go +++ b/pkg/coredata/slack_message.go @@ -65,6 +65,7 @@ func (sm *SlackMessage) AuthorizationAttributes(ctx context.Context, conn pg.Que if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query slack message authorization attributes: %w", err) } @@ -79,6 +80,7 @@ func NewSlackMessage( ) *SlackMessage { now := time.Now() id := gid.New(scope.GetTenantID(), SlackMessageEntityType) + return &SlackMessage{ ID: id, OrganizationID: organizationID, @@ -295,6 +297,7 @@ LIMIT 1 if errors.Is(err, pgx.ErrNoRows) { return ErrSlackMessageNotFound{} } + return fmt.Errorf("cannot collect slack message: %w", err) } @@ -398,6 +401,7 @@ LIMIT 1 if errors.Is(err, pgx.ErrNoRows) { return ErrSlackMessageNotFound{} } + return fmt.Errorf("cannot collect slack message: %w", err) } @@ -438,6 +442,7 @@ LIMIT 1 if errors.Is(err, pgx.ErrNoRows) { return ErrSlackMessageNotFound{} } + return err } @@ -487,6 +492,7 @@ LIMIT 1 if errors.Is(err, pgx.ErrNoRows) { return ErrSlackMessageNotFound{} } + return err } diff --git a/pkg/coredata/slack_message_type.go b/pkg/coredata/slack_message_type.go index fa75386db..2bea3b35f 100644 --- a/pkg/coredata/slack_message_type.go +++ b/pkg/coredata/slack_message_type.go @@ -32,6 +32,7 @@ func (smt SlackMessageType) String() string { func (smt *SlackMessageType) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -49,6 +50,7 @@ func (smt *SlackMessageType) Scan(value any) error { default: return fmt.Errorf("invalid SlackMessageType value: %q", s) } + return nil } diff --git a/pkg/coredata/statement_of_applicability.go b/pkg/coredata/statement_of_applicability.go index d017fbcd1..f2cc08133 100644 --- a/pkg/coredata/statement_of_applicability.go +++ b/pkg/coredata/statement_of_applicability.go @@ -60,6 +60,7 @@ func (s *StatementOfApplicability) AuthorizationAttributes(ctx context.Context, if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query statement of applicability authorization attributes: %w", err) } @@ -108,6 +109,7 @@ LIMIT 1; } *s = statementOfApplicability + return nil } @@ -150,6 +152,7 @@ WHERE } *s = statementsOfApplicability + return nil } @@ -176,6 +179,7 @@ WHERE maps.Copy(args, scope.SQLArguments()) row := conn.QueryRow(ctx, q, args) + var count int if err := row.Scan(&count); err != nil { return 0, fmt.Errorf("cannot count statements_of_applicability: %w", err) @@ -220,8 +224,8 @@ VALUES ( "created_at": s.CreatedAt, "updated_at": s.UpdatedAt, } - _, err := conn.Exec(ctx, q, args) + _, err := conn.Exec(ctx, q, args) if err != nil { var pgErr *pgconn.PgError if errors.As(err, &pgErr) { @@ -229,6 +233,7 @@ VALUES ( return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot insert statement_of_applicability: %w", err) } @@ -269,6 +274,7 @@ WHERE return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot update statement_of_applicability: %w", err) } diff --git a/pkg/coredata/statement_of_applicability_order_field.go b/pkg/coredata/statement_of_applicability_order_field.go index 95fd39097..a2b3625af 100644 --- a/pkg/coredata/statement_of_applicability_order_field.go +++ b/pkg/coredata/statement_of_applicability_order_field.go @@ -34,6 +34,7 @@ func (s StatementOfApplicabilityOrderField) Column() string { case StatementOfApplicabilityOrderFieldCreatedAt: return "created_at" } + panic(fmt.Sprintf("unsupported order by: %s", s)) } @@ -46,6 +47,7 @@ func (s StatementOfApplicabilityOrderField) IsValid() bool { case StatementOfApplicabilityOrderFieldName, StatementOfApplicabilityOrderFieldCreatedAt: return true } + return false } @@ -58,5 +60,6 @@ func (s *StatementOfApplicabilityOrderField) UnmarshalText(text []byte) error { if !s.IsValid() { return fmt.Errorf("%s is not a valid StatementOfApplicabilityOrderField", string(text)) } + return nil } diff --git a/pkg/coredata/task.go b/pkg/coredata/task.go index e54236688..5bb06a179 100644 --- a/pkg/coredata/task.go +++ b/pkg/coredata/task.go @@ -72,6 +72,7 @@ func (t *Task) AuthorizationAttributes(ctx context.Context, conn pg.Querier) (ma if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query task authorization attributes: %w", err) } @@ -257,6 +258,7 @@ RETURNING rank, priority_rank; return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot insert task: %w", err) } @@ -348,6 +350,7 @@ RETURNING "created_at": t.CreatedAt, "updated_at": t.UpdatedAt, } + rows, err := conn.Query(ctx, q, args) if err != nil { return fmt.Errorf("cannot upsert task: %w", err) @@ -387,6 +390,7 @@ func (t *Tasks) CountByOrganizationID( row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot collect tasks: %w", err) @@ -471,6 +475,7 @@ WHERE row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot collect tasks: %w", err) @@ -569,6 +574,7 @@ WHERE %s maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) + return err } @@ -608,6 +614,7 @@ WHERE } t.Rank = rank + return nil } diff --git a/pkg/coredata/task_order_field.go b/pkg/coredata/task_order_field.go index c82e7816f..11fb663b1 100644 --- a/pkg/coredata/task_order_field.go +++ b/pkg/coredata/task_order_field.go @@ -32,6 +32,7 @@ func (p TaskOrderField) Column() string { case TaskOrderFieldCreatedAt: return "created_at" } + panic(fmt.Sprintf("unsupported order by: %s", p)) } @@ -40,6 +41,7 @@ func (p TaskOrderField) IsValid() bool { case TaskOrderFieldPriorityRank, TaskOrderFieldCreatedAt: return true } + return false } @@ -56,5 +58,6 @@ func (p *TaskOrderField) UnmarshalText(text []byte) error { if !p.IsValid() { return fmt.Errorf("%s is not a valid TaskOrderField", string(text)) } + return nil } diff --git a/pkg/coredata/task_priority.go b/pkg/coredata/task_priority.go index ae0ff11e7..7b44ec280 100644 --- a/pkg/coredata/task_priority.go +++ b/pkg/coredata/task_priority.go @@ -43,6 +43,7 @@ func (tp TaskPriority) String() string { func (tp *TaskPriority) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -64,6 +65,7 @@ func (tp *TaskPriority) Scan(value any) error { default: return fmt.Errorf("invalid TaskPriority value: %q", s) } + return nil } diff --git a/pkg/coredata/third_party.go b/pkg/coredata/third_party.go index e9685364b..a2c2d3d12 100644 --- a/pkg/coredata/third_party.go +++ b/pkg/coredata/third_party.go @@ -49,6 +49,7 @@ WHERE if errors.Is(err, pgx.ErrNoRows) { return nil, nil } + if err != nil { return nil, fmt.Errorf("cannot get thirdParty list document ID: %w", err) } @@ -188,6 +189,7 @@ func (v *ThirdParty) AuthorizationAttributes(ctx context.Context, conn pg.Querie if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query thirdParty authorization attributes: %w", err) } @@ -416,6 +418,7 @@ VALUES ( "updated_at": v.UpdatedAt, } _, err := conn.Exec(ctx, q, args) + return err } @@ -434,6 +437,7 @@ DELETE FROM third_parties WHERE %s AND id = @third_party_id maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) + return err } @@ -464,6 +468,7 @@ WHERE row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count thirdParties: %w", err) @@ -661,6 +666,7 @@ WHERE %s maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) + return err } @@ -729,6 +735,7 @@ WHERE %s row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count thirdParties: %w", err) @@ -864,6 +871,7 @@ WHERE %s row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count thirdParties: %w", err) @@ -1210,12 +1218,17 @@ ORDER BY defer rows.Close() thirdPartyMap := make(map[gid.GID][]string) + for rows.Next() { - var processingActivityID gid.GID - var thirdPartyName string + var ( + processingActivityID gid.GID + thirdPartyName string + ) + if err := rows.Scan(&processingActivityID, &thirdPartyName); err != nil { return nil, fmt.Errorf("cannot scan thirdParty: %w", err) } + thirdPartyMap[processingActivityID] = append(thirdPartyMap[processingActivityID], thirdPartyName) } @@ -1381,6 +1394,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect vendor by common third party: %w", err) } diff --git a/pkg/coredata/third_party_business_associate_agreement.go b/pkg/coredata/third_party_business_associate_agreement.go index fa6363af4..4c7e75999 100644 --- a/pkg/coredata/third_party_business_associate_agreement.go +++ b/pkg/coredata/third_party_business_associate_agreement.go @@ -61,6 +61,7 @@ func (vbaa *ThirdPartyBusinessAssociateAgreement) AuthorizationAttributes(ctx co if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query thirdParty business associate agreement authorization attributes: %w", err) } @@ -297,6 +298,7 @@ ON CONFLICT (organization_id, third_party_id) DO UPDATE SET if err != nil { return fmt.Errorf("cannot upsert thirdParty business associate agreement: %w", err) } + return nil } @@ -320,6 +322,7 @@ WHERE maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) + return err } @@ -344,5 +347,6 @@ WHERE maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) + return err } diff --git a/pkg/coredata/third_party_category.go b/pkg/coredata/third_party_category.go index cfdf2c08b..c75bda277 100644 --- a/pkg/coredata/third_party_category.go +++ b/pkg/coredata/third_party_category.go @@ -132,6 +132,7 @@ func (i *ThirdPartyCategory) Scan(value any) error { default: return fmt.Errorf("unsupported type for ThirdPartyCategory: %T", value) } + return nil } @@ -197,6 +198,7 @@ func (i *ThirdPartyCategory) UnmarshalJSON(data []byte) error { default: return fmt.Errorf("invalid ThirdPartyCategory value: %q", s) } + return nil } @@ -251,5 +253,6 @@ func (i *ThirdPartyCategory) UnmarshalText(text []byte) error { default: return fmt.Errorf("invalid ThirdPartyCategory value: %q", s) } + return nil } diff --git a/pkg/coredata/third_party_compliance_report.go b/pkg/coredata/third_party_compliance_report.go index b2307a5e2..9331d7b44 100644 --- a/pkg/coredata/third_party_compliance_report.go +++ b/pkg/coredata/third_party_compliance_report.go @@ -62,6 +62,7 @@ func (v *ThirdPartyComplianceReport) AuthorizationAttributes(ctx context.Context if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query thirdParty compliance report authorization attributes: %w", err) } @@ -262,6 +263,7 @@ VALUES ( } _, err := conn.Exec(ctx, q, args) + return err } @@ -286,8 +288,8 @@ RETURNING report_file_id maps.Copy(args, scope.SQLArguments()) var vcrFileId *gid.GID - err := conn.QueryRow(ctx, q, args).Scan(&vcrFileId) + err := conn.QueryRow(ctx, q, args).Scan(&vcrFileId) if err != nil { return fmt.Errorf("cannot delete thirdParty compliance report: %w", err) } @@ -298,5 +300,6 @@ RETURNING report_file_id return fmt.Errorf("cannot soft delete thirdParty compliance file: %w", err) } } + return nil } diff --git a/pkg/coredata/third_party_contact.go b/pkg/coredata/third_party_contact.go index 1ecec4aa5..7d53665e3 100644 --- a/pkg/coredata/third_party_contact.go +++ b/pkg/coredata/third_party_contact.go @@ -65,6 +65,7 @@ func (vc *ThirdPartyContact) AuthorizationAttributes(ctx context.Context, conn p if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query thirdParty contact authorization attributes: %w", err) } diff --git a/pkg/coredata/third_party_data_privacy_agreement.go b/pkg/coredata/third_party_data_privacy_agreement.go index be4a6aaee..4b9abc8ee 100644 --- a/pkg/coredata/third_party_data_privacy_agreement.go +++ b/pkg/coredata/third_party_data_privacy_agreement.go @@ -61,6 +61,7 @@ func (vdpa *ThirdPartyDataPrivacyAgreement) AuthorizationAttributes(ctx context. if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query thirdParty data privacy agreement authorization attributes: %w", err) } @@ -297,6 +298,7 @@ ON CONFLICT (organization_id, third_party_id) DO UPDATE SET if err != nil { return fmt.Errorf("cannot upsert thirdParty data privacy agreement: %w", err) } + return nil } @@ -320,6 +322,7 @@ WHERE maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) + return err } @@ -344,5 +347,6 @@ WHERE maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) + return err } diff --git a/pkg/coredata/third_party_risk_assessment.go b/pkg/coredata/third_party_risk_assessment.go index 99a2ae193..8a9f3a318 100644 --- a/pkg/coredata/third_party_risk_assessment.go +++ b/pkg/coredata/third_party_risk_assessment.go @@ -63,6 +63,7 @@ func (v *ThirdPartyRiskAssessment) AuthorizationAttributes(ctx context.Context, if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query thirdParty risk assessment authorization attributes: %w", err) } @@ -116,6 +117,7 @@ VALUES ( "updated_at": r.UpdatedAt, } _, err := conn.Exec(ctx, q, args) + return err } diff --git a/pkg/coredata/third_party_service.go b/pkg/coredata/third_party_service.go index c441a0985..024a80f49 100644 --- a/pkg/coredata/third_party_service.go +++ b/pkg/coredata/third_party_service.go @@ -60,6 +60,7 @@ func (vs *ThirdPartyService) AuthorizationAttributes(ctx context.Context, conn p if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query thirdParty service authorization attributes: %w", err) } diff --git a/pkg/coredata/third_party_service_order_field.go b/pkg/coredata/third_party_service_order_field.go index a32f50019..a23407bc9 100644 --- a/pkg/coredata/third_party_service_order_field.go +++ b/pkg/coredata/third_party_service_order_field.go @@ -47,5 +47,6 @@ func (p *ThirdPartyServiceOrderField) UnmarshalText(text []byte) error { *p = ThirdPartyServiceOrderField(val) return nil } + return fmt.Errorf("invalid ThirdPartyServiceOrderField value: %q", val) } diff --git a/pkg/coredata/tracker_pattern.go b/pkg/coredata/tracker_pattern.go index 0fc10d0e7..c1f2c7857 100644 --- a/pkg/coredata/tracker_pattern.go +++ b/pkg/coredata/tracker_pattern.go @@ -64,6 +64,7 @@ func (tp *TrackerPattern) CursorKey(field TrackerPatternOrderField) page.CursorK if tp.LastMatchedAt == nil { return page.NewCursorKey(tp.ID, time.Time{}) } + return page.NewCursorKey(tp.ID, *tp.LastMatchedAt) case TrackerPatternOrderFieldUpdatedAt: return page.NewCursorKey(tp.ID, tp.UpdatedAt) @@ -71,6 +72,7 @@ func (tp *TrackerPattern) CursorKey(field TrackerPatternOrderField) page.CursorK if tp.Source == nil { return page.NewCursorKey(tp.ID, "") } + return page.NewCursorKey(tp.ID, string(*tp.Source)) } @@ -141,6 +143,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect tracker pattern: %w", err) } @@ -209,6 +212,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect tracker pattern: %w", err) } @@ -288,6 +292,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect tracker pattern: %w", err) } @@ -374,6 +379,7 @@ INSERT INTO tracker_patterns ( return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot insert tracker pattern: %w", err) } @@ -501,6 +507,7 @@ WHERE return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot update tracker pattern: %w", err) } @@ -955,6 +962,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect tracker pattern for mapping: %w", err) } diff --git a/pkg/coredata/tracker_pattern_match_type.go b/pkg/coredata/tracker_pattern_match_type.go index e53dfd243..030997436 100644 --- a/pkg/coredata/tracker_pattern_match_type.go +++ b/pkg/coredata/tracker_pattern_match_type.go @@ -40,6 +40,7 @@ func (m TrackerPatternMatchType) String() string { func (m *TrackerPatternMatchType) Scan(value any) error { var v string + switch val := value.(type) { case string: v = val @@ -59,6 +60,7 @@ func (m *TrackerPatternMatchType) Scan(value any) error { default: return fmt.Errorf("invalid TrackerPatternMatchType value: %q", v) } + return nil } diff --git a/pkg/coredata/tracker_pattern_order_field.go b/pkg/coredata/tracker_pattern_order_field.go index c9c22d1f8..53e95832a 100644 --- a/pkg/coredata/tracker_pattern_order_field.go +++ b/pkg/coredata/tracker_pattern_order_field.go @@ -39,6 +39,7 @@ func (p TrackerPatternOrderField) Column() string { case TrackerPatternOrderFieldSource: return "COALESCE(source, '')" } + panic(fmt.Sprintf("unsupported order by: %s", p)) } @@ -51,6 +52,7 @@ func (p TrackerPatternOrderField) IsValid() bool { TrackerPatternOrderFieldSource: return true } + return false } @@ -63,6 +65,7 @@ func (p *TrackerPatternOrderField) UnmarshalText(text []byte) error { if !p.IsValid() { return fmt.Errorf("%s is not a valid TrackerPatternOrderField", string(text)) } + return nil } diff --git a/pkg/coredata/tracker_resource.go b/pkg/coredata/tracker_resource.go index 3c52570b8..10e3c610f 100644 --- a/pkg/coredata/tracker_resource.go +++ b/pkg/coredata/tracker_resource.go @@ -56,6 +56,7 @@ func (tr *TrackerResource) CursorKey(field TrackerResourceOrderField) page.Curso if tr.LastDetectedAt == nil { return page.NewCursorKey(tr.ID, time.Time{}) } + return page.NewCursorKey(tr.ID, *tr.LastDetectedAt) case TrackerResourceOrderFieldOrigin: return page.NewCursorKey(tr.ID, tr.Origin) @@ -125,6 +126,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect tracker resource: %w", err) } @@ -188,6 +190,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect tracker resource: %w", err) } @@ -259,6 +262,7 @@ INSERT INTO tracker_resources ( return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot insert tracker resource: %w", err) } diff --git a/pkg/coredata/tracker_resource_order_field.go b/pkg/coredata/tracker_resource_order_field.go index cf476277f..51900101e 100644 --- a/pkg/coredata/tracker_resource_order_field.go +++ b/pkg/coredata/tracker_resource_order_field.go @@ -36,6 +36,7 @@ func (p TrackerResourceOrderField) Column() string { case TrackerResourceOrderFieldUpdatedAt: return "updated_at" } + panic(fmt.Sprintf("unsupported order by: %s", p)) } @@ -47,6 +48,7 @@ func (p TrackerResourceOrderField) IsValid() bool { TrackerResourceOrderFieldUpdatedAt: return true } + return false } @@ -59,6 +61,7 @@ func (p *TrackerResourceOrderField) UnmarshalText(text []byte) error { if !p.IsValid() { return fmt.Errorf("%s is not a valid TrackerResourceOrderField", string(text)) } + return nil } diff --git a/pkg/coredata/tracker_resource_type.go b/pkg/coredata/tracker_resource_type.go index f43a294be..3dfad4758 100644 --- a/pkg/coredata/tracker_resource_type.go +++ b/pkg/coredata/tracker_resource_type.go @@ -53,6 +53,7 @@ func (s TrackerResourceType) String() string { func (s *TrackerResourceType) Scan(value any) error { var v string + switch val := value.(type) { case string: v = val @@ -84,6 +85,7 @@ func (s *TrackerResourceType) Scan(value any) error { default: return fmt.Errorf("invalid TrackerResourceType value: %q", v) } + return nil } diff --git a/pkg/coredata/tracker_type.go b/pkg/coredata/tracker_type.go index 876c7b4b7..0773a0265 100644 --- a/pkg/coredata/tracker_type.go +++ b/pkg/coredata/tracker_type.go @@ -45,6 +45,7 @@ func (s TrackerType) String() string { func (s *TrackerType) Scan(value any) error { var v string + switch val := value.(type) { case string: v = val @@ -68,6 +69,7 @@ func (s *TrackerType) Scan(value any) error { default: return fmt.Errorf("invalid TrackerType value: %q", v) } + return nil } diff --git a/pkg/coredata/transfer_impact_assessment.go b/pkg/coredata/transfer_impact_assessment.go index c7735f504..f94fcdf58 100644 --- a/pkg/coredata/transfer_impact_assessment.go +++ b/pkg/coredata/transfer_impact_assessment.go @@ -50,6 +50,7 @@ WHERE if errors.Is(err, pgx.ErrNoRows) { return nil, nil } + if err != nil { return nil, fmt.Errorf("cannot get TIA list document ID: %w", err) } @@ -200,6 +201,7 @@ WHERE row := conn.QueryRow(ctx, q, args) var count int + err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count transfer impact assessments: %w", err) @@ -342,6 +344,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect transfer impact assessment: %w", err) } @@ -391,6 +394,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect transfer impact assessment: %w", err) } diff --git a/pkg/coredata/transfer_impact_assessment_order_field.go b/pkg/coredata/transfer_impact_assessment_order_field.go index 10512b3bd..6952dfe40 100644 --- a/pkg/coredata/transfer_impact_assessment_order_field.go +++ b/pkg/coredata/transfer_impact_assessment_order_field.go @@ -41,5 +41,6 @@ func (p *TransferImpactAssessmentOrderField) UnmarshalText(text []byte) error { *p = TransferImpactAssessmentOrderFieldCreatedAt return nil } + return fmt.Errorf("invalid TransferImpactAssessmentOrderField value: %q", val) } diff --git a/pkg/coredata/trust_center.go b/pkg/coredata/trust_center.go index 55babc629..cf7888e3d 100644 --- a/pkg/coredata/trust_center.go +++ b/pkg/coredata/trust_center.go @@ -64,6 +64,7 @@ func (tc *TrustCenter) AuthorizationAttributes(ctx context.Context, conn pg.Quer if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query trust center authorization attributes: %w", err) } @@ -332,6 +333,7 @@ INSERT INTO trust_centers ( return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot insert trust center: %w", err) } diff --git a/pkg/coredata/trust_center_access.go b/pkg/coredata/trust_center_access.go index f2718c845..4a002ea5b 100644 --- a/pkg/coredata/trust_center_access.go +++ b/pkg/coredata/trust_center_access.go @@ -60,6 +60,7 @@ func (tca *TrustCenterAccess) AuthorizationAttributes(ctx context.Context, conn if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query trust center access authorization attributes: %w", err) } @@ -213,6 +214,7 @@ INSERT INTO trust_center_accesses ( return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot insert trust center access: %w", err) } diff --git a/pkg/coredata/trust_center_access_state.go b/pkg/coredata/trust_center_access_state.go index 940a9ea19..31a442c48 100644 --- a/pkg/coredata/trust_center_access_state.go +++ b/pkg/coredata/trust_center_access_state.go @@ -32,6 +32,7 @@ func (s TrustCenterAccessState) String() string { func (s *TrustCenterAccessState) Scan(value any) error { var str string + switch v := value.(type) { case string: str = v @@ -49,6 +50,7 @@ func (s *TrustCenterAccessState) Scan(value any) error { default: return fmt.Errorf("invalid TrustCenterAccessState value: %q", str) } + return nil } diff --git a/pkg/coredata/trust_center_document_access.go b/pkg/coredata/trust_center_document_access.go index 40a38d367..53d55ccd5 100644 --- a/pkg/coredata/trust_center_document_access.go +++ b/pkg/coredata/trust_center_document_access.go @@ -61,6 +61,7 @@ func (tcda *TrustCenterDocumentAccess) AuthorizationAttributes(ctx context.Conte if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query trust center document access authorization attributes: %w", err) } @@ -107,6 +108,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect trust center document access: %w", err) } @@ -280,6 +282,7 @@ INSERT INTO trust_center_document_accesses ( } } } + return fmt.Errorf("cannot insert trust center document access: %w", err) } diff --git a/pkg/coredata/trust_center_document_access_order_field.go b/pkg/coredata/trust_center_document_access_order_field.go index 4e26449ef..a4679ea27 100644 --- a/pkg/coredata/trust_center_document_access_order_field.go +++ b/pkg/coredata/trust_center_document_access_order_field.go @@ -43,5 +43,6 @@ func (tcdaof *TrustCenterDocumentAccessOrderField) UnmarshalText(text []byte) er *tcdaof = TrustCenterDocumentAccessOrderField(val) return nil } + return fmt.Errorf("invalid TrustCenterDocumentAccessOrderField value: %q", val) } diff --git a/pkg/coredata/trust_center_document_access_status.go b/pkg/coredata/trust_center_document_access_status.go index ef1d36503..5fd4828ed 100644 --- a/pkg/coredata/trust_center_document_access_status.go +++ b/pkg/coredata/trust_center_document_access_status.go @@ -34,6 +34,7 @@ func (tcdas TrustCenterDocumentAccessStatus) String() string { func (tcdas *TrustCenterDocumentAccessStatus) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -55,6 +56,7 @@ func (tcdas *TrustCenterDocumentAccessStatus) Scan(value any) error { default: return fmt.Errorf("invalid TrustCenterDocumentAccessStatus value: %q", s) } + return nil } diff --git a/pkg/coredata/trust_center_file.go b/pkg/coredata/trust_center_file.go index 5186d105b..077145bbe 100644 --- a/pkg/coredata/trust_center_file.go +++ b/pkg/coredata/trust_center_file.go @@ -51,6 +51,7 @@ func (t TrustCenterFile) CursorKey(orderBy TrustCenterFileOrderField) page.Curso case TrustCenterFileOrderFieldUpdatedAt: return page.NewCursorKey(t.ID, t.UpdatedAt) } + panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } @@ -62,6 +63,7 @@ func (t *TrustCenterFile) AuthorizationAttributes(ctx context.Context, conn pg.Q if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query trust center file authorization attributes: %w", err) } @@ -355,6 +357,7 @@ WHERE maps.Copy(args, scope.SQLArguments()) var count int + err := conn.QueryRow(ctx, q, args).Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count trust center files: %w", err) diff --git a/pkg/coredata/trust_center_file_filter.go b/pkg/coredata/trust_center_file_filter.go index 5f8caac7a..a65a427ad 100644 --- a/pkg/coredata/trust_center_file_filter.go +++ b/pkg/coredata/trust_center_file_filter.go @@ -50,6 +50,7 @@ func (f *TrustCenterFileFilter) SQLArguments() pgx.NamedArgs { visibilities[i] = v.String() } } + return pgx.NamedArgs{ "trust_center_visibilities": visibilities, } diff --git a/pkg/coredata/trust_center_reference.go b/pkg/coredata/trust_center_reference.go index d683f6e32..731a0e0b6 100644 --- a/pkg/coredata/trust_center_reference.go +++ b/pkg/coredata/trust_center_reference.go @@ -56,6 +56,7 @@ func (t TrustCenterReference) CursorKey(orderBy TrustCenterReferenceOrderField) case TrustCenterReferenceOrderFieldUpdatedAt: return page.NewCursorKey(t.ID, t.UpdatedAt) } + panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } @@ -67,6 +68,7 @@ func (t *TrustCenterReference) AuthorizationAttributes(ctx context.Context, conn if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query trust center reference authorization attributes: %w", err) } @@ -175,6 +177,7 @@ RETURNING rank; return ErrResourceAlreadyExists } } + return fmt.Errorf("cannot insert trust center reference: %w", err) } @@ -369,6 +372,7 @@ WHERE maps.Copy(args, scope.SQLArguments()) var count int + err := conn.QueryRow(ctx, q, args).Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count trust center references: %w", err) diff --git a/pkg/coredata/trust_center_visibility.go b/pkg/coredata/trust_center_visibility.go index bf4a19c9e..2e576f00c 100644 --- a/pkg/coredata/trust_center_visibility.go +++ b/pkg/coredata/trust_center_visibility.go @@ -41,6 +41,7 @@ func (tcv TrustCenterVisibility) String() string { func (tcv *TrustCenterVisibility) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -60,6 +61,7 @@ func (tcv *TrustCenterVisibility) Scan(value any) error { default: return fmt.Errorf("invalid TrustCenterVisibility value: %q", s) } + return nil } diff --git a/pkg/coredata/webhook_data.go b/pkg/coredata/webhook_data.go index 06c2e200c..cb733b453 100644 --- a/pkg/coredata/webhook_data.go +++ b/pkg/coredata/webhook_data.go @@ -115,6 +115,7 @@ FOR UPDATE SKIP LOCKED } *w = data + return nil } diff --git a/pkg/coredata/webhook_event.go b/pkg/coredata/webhook_event.go index 40e222ee9..908c40d40 100644 --- a/pkg/coredata/webhook_event.go +++ b/pkg/coredata/webhook_event.go @@ -88,6 +88,7 @@ WHERE } *w = events + return nil } diff --git a/pkg/coredata/webhook_event_order_field.go b/pkg/coredata/webhook_event_order_field.go index dd2701f2a..bd85a3a0d 100644 --- a/pkg/coredata/webhook_event_order_field.go +++ b/pkg/coredata/webhook_event_order_field.go @@ -39,6 +39,7 @@ func (p WebhookEventOrderField) IsValid() bool { case WebhookEventOrderFieldCreatedAt: return true } + return false } @@ -51,5 +52,6 @@ func (p *WebhookEventOrderField) UnmarshalText(text []byte) error { if !p.IsValid() { return fmt.Errorf("%s is not a valid WebhookEventOrderField", string(text)) } + return nil } diff --git a/pkg/coredata/webhook_event_status.go b/pkg/coredata/webhook_event_status.go index d89836f49..daae5aedd 100644 --- a/pkg/coredata/webhook_event_status.go +++ b/pkg/coredata/webhook_event_status.go @@ -36,6 +36,7 @@ func (s WebhookEventStatus) IsValid() bool { case WebhookEventStatusPending, WebhookEventStatusSucceeded, WebhookEventStatusFailed: return true } + return false } @@ -48,6 +49,7 @@ func (s *WebhookEventStatus) UnmarshalText(text []byte) error { if !s.IsValid() { return fmt.Errorf("%s is not a valid WebhookEventStatus", string(text)) } + return nil } diff --git a/pkg/coredata/webhook_event_type.go b/pkg/coredata/webhook_event_type.go index 772a9d922..062e33cf2 100644 --- a/pkg/coredata/webhook_event_type.go +++ b/pkg/coredata/webhook_event_type.go @@ -45,6 +45,7 @@ func (w WebhookEventType) IsValid() bool { WebhookEventTypeObligationCreated, WebhookEventTypeObligationUpdated, WebhookEventTypeObligationDeleted: return true } + return false } @@ -57,11 +58,13 @@ func (w *WebhookEventType) UnmarshalText(text []byte) error { if !w.IsValid() { return fmt.Errorf("%s is not a valid WebhookEventType", string(text)) } + return nil } func (w *WebhookEventType) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -116,10 +119,12 @@ func (s *WebhookEventTypes) scanFromString(str string) error { if err := et.Scan(part); err != nil { return fmt.Errorf("invalid webhook event type in array: %s", part) } + result[i] = et } *s = result + return nil } diff --git a/pkg/coredata/webhook_subscription.go b/pkg/coredata/webhook_subscription.go index 8dfdb113b..ab7d15ffc 100644 --- a/pkg/coredata/webhook_subscription.go +++ b/pkg/coredata/webhook_subscription.go @@ -92,6 +92,7 @@ func (w *WebhookSubscription) AuthorizationAttributes(ctx context.Context, conn if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } + return nil, fmt.Errorf("cannot query webhook subscription authorization attributes: %w", err) } @@ -141,6 +142,7 @@ LIMIT 1; } *w = wc + return nil } @@ -184,6 +186,7 @@ WHERE } *w = subscriptions + return nil } @@ -210,6 +213,7 @@ WHERE maps.Copy(args, scope.SQLArguments()) row := conn.QueryRow(ctx, q, args) + var count int if err := row.Scan(&count); err != nil { return 0, fmt.Errorf("cannot count webhook subscriptions: %w", err) @@ -289,8 +293,8 @@ VALUES ( "created_at": w.CreatedAt, "updated_at": w.UpdatedAt, } - _, err := conn.Exec(ctx, q, args) + _, err := conn.Exec(ctx, q, args) if err != nil { return fmt.Errorf("cannot insert webhook subscription: %w", err) } @@ -377,6 +381,7 @@ WHERE } *w = subscriptions + return nil } diff --git a/pkg/coredata/webhook_subscription_order_field.go b/pkg/coredata/webhook_subscription_order_field.go index 294ee885b..6c6c62f47 100644 --- a/pkg/coredata/webhook_subscription_order_field.go +++ b/pkg/coredata/webhook_subscription_order_field.go @@ -39,6 +39,7 @@ func (p WebhookSubscriptionOrderField) IsValid() bool { case WebhookSubscriptionOrderFieldCreatedAt: return true } + return false } @@ -51,5 +52,6 @@ func (p *WebhookSubscriptionOrderField) UnmarshalText(text []byte) error { if !p.IsValid() { return fmt.Errorf("%s is not a valid WebhookSubscriptionOrderField", string(text)) } + return nil } diff --git a/pkg/crypto/cipher/cipher.go b/pkg/crypto/cipher/cipher.go index a3469ee6b..e8146fd1e 100644 --- a/pkg/crypto/cipher/cipher.go +++ b/pkg/crypto/cipher/cipher.go @@ -52,6 +52,7 @@ func (k *EncryptionKey) UnmarshalText(text []byte) error { } copy(k[:], decoded) + return nil } @@ -94,6 +95,7 @@ func Decrypt(data []byte, key EncryptionKey) ([]byte, error) { if len(data) < 12 { return nil, fmt.Errorf("ciphertext too short") } + nonce, ciphertext := data[:12], data[12:] plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil) diff --git a/pkg/crypto/jose/jose.go b/pkg/crypto/jose/jose.go index 5f13716d1..4a0b56d92 100644 --- a/pkg/crypto/jose/jose.go +++ b/pkg/crypto/jose/jose.go @@ -87,6 +87,7 @@ func SignJWT(privateKey *rsa.PrivateKey, kid string, claims any) (string, error) signingInput := headerB64 + "." + claimsB64 h := sha256.Sum256([]byte(signingInput)) + signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, h[:]) if err != nil { return "", fmt.Errorf("cannot sign jwt: %w", err) diff --git a/pkg/crypto/jose/jose_test.go b/pkg/crypto/jose/jose_test.go index 3f3e8fccd..c6ae6f1f5 100644 --- a/pkg/crypto/jose/jose_test.go +++ b/pkg/crypto/jose/jose_test.go @@ -140,6 +140,7 @@ func TestSignJWT(t *testing.T) { require.NoError(t, err) var header jose.JWTHeader + err = json.Unmarshal(headerJSON, &header) require.NoError(t, err) @@ -168,6 +169,7 @@ func TestSignJWT(t *testing.T) { require.NoError(t, err) var decoded map[string]any + err = json.Unmarshal(claimsJSON, &decoded) require.NoError(t, err) @@ -215,6 +217,7 @@ func TestJWK_JSON(t *testing.T) { require.NoError(t, err) var raw map[string]string + err = json.Unmarshal(data, &raw) require.NoError(t, err) @@ -251,6 +254,7 @@ func TestJWKS_JSON(t *testing.T) { var raw struct { Keys []json.RawMessage `json:"keys"` } + err = json.Unmarshal(data, &raw) require.NoError(t, err) @@ -277,6 +281,7 @@ func TestJWTHeader_JSON(t *testing.T) { require.NoError(t, err) var raw map[string]string + err = json.Unmarshal(data, &raw) require.NoError(t, err) diff --git a/pkg/crypto/keys/keys_test.go b/pkg/crypto/keys/keys_test.go index 2ee788035..a702dc62f 100644 --- a/pkg/crypto/keys/keys_test.go +++ b/pkg/crypto/keys/keys_test.go @@ -57,6 +57,7 @@ func TestGenerate(t *testing.T) { checkFunc: func(t *testing.T, key any) { rsaKey, ok := key.(*rsa.PrivateKey) require.True(t, ok, "expected *rsa.PrivateKey, got %T", key) + bitSize := rsaKey.N.BitLen() assert.GreaterOrEqual(t, bitSize, 2047, "RSA key too small") assert.LessOrEqual(t, bitSize, 2048, "RSA key too large") @@ -68,6 +69,7 @@ func TestGenerate(t *testing.T) { checkFunc: func(t *testing.T, key any) { rsaKey, ok := key.(*rsa.PrivateKey) require.True(t, ok, "expected *rsa.PrivateKey, got %T", key) + bitSize := rsaKey.N.BitLen() assert.GreaterOrEqual(t, bitSize, 4095, "RSA key too small") assert.LessOrEqual(t, bitSize, 4096, "RSA key too large") @@ -116,6 +118,7 @@ func TestGenerateConcurrency(t *testing.T) { t.Parallel() const numGoroutines = 10 + errorsChan := make(chan error, numGoroutines) for range numGoroutines { @@ -125,10 +128,12 @@ func TestGenerateConcurrency(t *testing.T) { errorsChan <- err return } + if key == nil { errorsChan <- errors.New("generated key is nil") return } + errorsChan <- nil }() } diff --git a/pkg/crypto/passwdhash/passwdhash.go b/pkg/crypto/passwdhash/passwdhash.go index b93392618..c2eead280 100644 --- a/pkg/crypto/passwdhash/passwdhash.go +++ b/pkg/crypto/passwdhash/passwdhash.go @@ -61,6 +61,7 @@ func NewProfile(pepper []byte, iterations uint32) (*Profile, error) { func (hp Profile) applyPepper(input []byte) []byte { mac := hmac.New(sha256.New, hp.pepper) mac.Write(input) + return mac.Sum(nil) } diff --git a/pkg/crypto/pem/pem.go b/pkg/crypto/pem/pem.go index e1ac8a416..ea936efb7 100644 --- a/pkg/crypto/pem/pem.go +++ b/pkg/crypto/pem/pem.go @@ -36,6 +36,7 @@ func EncodeCertificate(der []byte) []byte { Type: BlockTypeCertificate, Bytes: der, } + return pem.EncodeToMemory(block) } @@ -44,12 +45,15 @@ func EncodeCertificateChain(derCerts [][]byte) []byte { for _, der := range derCerts { chain = append(chain, EncodeCertificate(der)...) } + return chain } func EncodePrivateKey(key crypto.Signer) ([]byte, error) { - var keyDER []byte - var keyType string + var ( + keyDER []byte + keyType string + ) switch k := key.(type) { case *ecdsa.PrivateKey: @@ -57,6 +61,7 @@ func EncodePrivateKey(key crypto.Signer) ([]byte, error) { if err != nil { return nil, fmt.Errorf("cannot marshal EC private key: %w", err) } + keyDER = der keyType = BlockTypeECPrivateKey case *rsa.PrivateKey: @@ -67,6 +72,7 @@ func EncodePrivateKey(key crypto.Signer) ([]byte, error) { if err != nil { return nil, fmt.Errorf("cannot marshal ED25519 private key: %w", err) } + keyDER = der keyType = BlockTypePKCS8PrivateKey default: @@ -97,10 +103,12 @@ func DecodePrivateKey(pemData []byte) (crypto.Signer, error) { if err != nil { return nil, fmt.Errorf("cannot parse PKCS8 private key: %w", err) } + signer, ok := key.(crypto.Signer) if !ok { return nil, fmt.Errorf("key is not a crypto.Signer") } + return signer, nil default: return nil, fmt.Errorf("unsupported PEM block type: %s", block.Type) diff --git a/pkg/crypto/pem/pem_test.go b/pkg/crypto/pem/pem_test.go index f35f0934e..2722938d8 100644 --- a/pkg/crypto/pem/pem_test.go +++ b/pkg/crypto/pem/pem_test.go @@ -122,10 +122,12 @@ func TestEncodePrivateKey(t *testing.T) { if err != nil { return nil, err } + signer, ok := key.(crypto.Signer) if !ok { return nil, errors.New("key is not a crypto.Signer") } + return signer, nil }, }, @@ -205,6 +207,7 @@ func TestRoundTrip(t *testing.T) { // Parse key based on type var parsedKey crypto.Signer + switch block.Type { case "EC PRIVATE KEY": parsedKey, err = x509.ParseECPrivateKey(block.Bytes) @@ -228,8 +231,10 @@ func TestRoundTrip(t *testing.T) { h.Write(testData) hashed := h.Sum(nil) - var dataToSign []byte - var hashFunc crypto.Hash + var ( + dataToSign []byte + hashFunc crypto.Hash + ) switch originalKey.(type) { case *rsa.PrivateKey: @@ -309,6 +314,7 @@ func BenchmarkEncodeCertificate(b *testing.B) { certDER, _ := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) b.ResetTimer() + for i := 0; i < b.N; i++ { _ = pem.EncodeCertificate(certDER) } @@ -344,5 +350,6 @@ func mustGenerateKey(gen func() (crypto.Signer, error)) crypto.Signer { if err != nil { panic(err) } + return key } diff --git a/pkg/docgen/generator.go b/pkg/docgen/generator.go index 2f2608ee9..e37219311 100644 --- a/pkg/docgen/generator.go +++ b/pkg/docgen/generator.go @@ -49,24 +49,29 @@ var ( if b == nil { return "" } + if *b { return "yes" } + return "no" }, "derefString": func(s *string) string { if s == nil { return "" } + return *s }, "boolToYesNoDash": func(b *bool) string { if b == nil { return "-" } + if *b { return "Yes" } + return "No" }, "imgTag": func(src, alt, class string) template.HTML { @@ -106,6 +111,7 @@ var ( if safeguard == nil { return "" } + switch *safeguard { case coredata.ProcessingActivityTransferSafeguardStandardContractualClauses: return "Standard Contractual Clauses" @@ -157,6 +163,7 @@ var ( if risk == nil { return "" } + switch *risk { case coredata.DataProtectionImpactAssessmentResidualRiskLow: return "Low" @@ -468,6 +475,7 @@ func BoolLabel(v bool) string { if v { return "Yes" } + return "No" } @@ -486,6 +494,7 @@ func MaturityLabel(l coredata.ControlMaturityLevel) string { case coredata.ControlMaturityLevelOptimizing: return "5 - Optimizing" } + return "Not set" } @@ -503,14 +512,17 @@ func ProseMirrorJSONToHTML(content json.RawMessage) template.HTML { if s == "" { return template.HTML("") } + node, err := prosemirror.Parse(s) if err != nil { return template.HTML(fmt.Sprintf("

%s

", html.EscapeString(s))) } + htmlStr, err := prosemirror.RenderHTML(node) if err != nil { return template.HTML(fmt.Sprintf("

%s

", html.EscapeString(s))) } + return template.HTML(htmlStr) } diff --git a/pkg/docgen/generator_test.go b/pkg/docgen/generator_test.go index eefa6443f..3aee54731 100644 --- a/pkg/docgen/generator_test.go +++ b/pkg/docgen/generator_test.go @@ -396,10 +396,12 @@ func TestDocumentVersionSignatureStates(t *testing.T) { func TestLargeContent(t *testing.T) { var largeContent strings.Builder largeContent.WriteString(`{"type":"doc","content":[`) + for i := range 1000 { if i > 0 { largeContent.WriteByte(',') } + largeContent.WriteString(`{"type":"heading","attrs":{"level":1},"content":[{"type":"text","text":"Section `) largeContent.WriteByte(byte('A' + i%26)) largeContent.WriteString(`"}]},`) @@ -416,6 +418,7 @@ func TestLargeContent(t *testing.T) { `{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"List item 3"}]}]}` + `]}`) } + largeContent.WriteString(`]}`) data := DocumentData{ @@ -465,6 +468,7 @@ func BenchmarkGenerateHTML(b *testing.B) { } b.ResetTimer() + for i := 0; i < b.N; i++ { _, err := RenderHTML(data) if err != nil { diff --git a/pkg/esign/certgen.go b/pkg/esign/certgen.go index fc7257d36..9730989ac 100644 --- a/pkg/esign/certgen.go +++ b/pkg/esign/certgen.go @@ -96,6 +96,7 @@ func (g *CertificateGenerator) Generate( if signature.SignedAt == nil { return nil, fmt.Errorf("cannot generate certificate: signature %s has no signed_at timestamp", signature.ID) } + data.SignedAt = signature.SignedAt.UTC().Format(time.RFC3339) if len(signature.TSAToken) == 0 { @@ -161,6 +162,7 @@ func tsaAuthorityName(ts *timestamp.Timestamp) string { if cert.Subject.CommonName != "" { return org + " (" + cert.Subject.CommonName + ")" } + return org } diff --git a/pkg/esign/completion_certificate_worker.go b/pkg/esign/completion_certificate_worker.go index f9f95ce99..09b29ac29 100644 --- a/pkg/esign/completion_certificate_worker.go +++ b/pkg/esign/completion_certificate_worker.go @@ -105,6 +105,7 @@ func (h *completionCertificateHandler) Claim(ctx context.Context) (coredata.Elec if errors.Is(err, coredata.ErrResourceNotFound) { return coredata.ElectronicSignature{}, worker.ErrNoTask } + return coredata.ElectronicSignature{}, err } @@ -118,6 +119,7 @@ func (h *completionCertificateHandler) Process(ctx context.Context, signature co if err := h.handleCertFailure(ctx, &signature, scope, err); err != nil { h.logger.ErrorCtx(ctx, "cannot handle certificate failure", log.Error(err)) } + return err } @@ -148,6 +150,7 @@ func (h *completionCertificateHandler) generateAndCommit( ctx, func(ctx context.Context, tx pg.Tx) error { signature.CertificateFileID = &attachments[1].FileID + signature.UpdatedAt = time.Now() if err := signature.Update(ctx, tx, scope); err != nil { return fmt.Errorf("cannot update signature: %w", err) @@ -261,12 +264,14 @@ func (h *completionCertificateHandler) generateCertificate( if err != nil { return nil, nil, fmt.Errorf("cannot resolve presenter config: %w", err) } + emailPresenter := emails.NewPresenterFromConfig(h.fileManager, presenterCfg, ref.UnrefOrZero(signature.SignerFullName)) docName := ref.UnrefOrZero(signature.DocumentName) if docName == "" { docName = signature.DocumentType.DisplayName() } + subject, textBody, htmlBody, err := emailPresenter.RenderElectronicSignatureCertificate(ctx, ref.UnrefOrZero(signature.SignerFullName), docName) if err != nil { return nil, nil, fmt.Errorf("cannot render email: %w", err) diff --git a/pkg/esign/sealing_worker.go b/pkg/esign/sealing_worker.go index adc476f82..1f86246dd 100644 --- a/pkg/esign/sealing_worker.go +++ b/pkg/esign/sealing_worker.go @@ -104,6 +104,7 @@ func (h *sealingHandler) Claim(ctx context.Context) (coredata.ElectronicSignatur signature.ProcessingStartedAt = &now signature.AttemptCount++ signature.LastAttemptedAt = &now + signature.UpdatedAt = now if err := signature.Update(ctx, tx, coredata.NewNoScope()); err != nil { return fmt.Errorf("cannot update signature: %w", err) @@ -115,6 +116,7 @@ func (h *sealingHandler) Claim(ctx context.Context) (coredata.ElectronicSignatur if errors.Is(err, coredata.ErrResourceNotFound) { return coredata.ElectronicSignature{}, worker.ErrNoTask } + return coredata.ElectronicSignature{}, err } @@ -126,8 +128,10 @@ func (h *sealingHandler) Process(ctx context.Context, signature coredata.Electro if err := h.failSignature(ctx, &signature, err); err != nil { h.logger.ErrorCtx(ctx, "cannot fail signature", log.Error(err)) } + return err } + return nil } @@ -175,6 +179,7 @@ func (h *sealingHandler) sealAndCommit( if err != nil { return fmt.Errorf("%w: %w", ErrComputeSeal, err) } + signature.Seal = &seal signature.SealVersion = currentSealVersion events = append( @@ -187,10 +192,12 @@ func (h *sealingHandler) sealAndCommit( tsaCtx, cancel := context.WithTimeout(ctx, h.tsaTimeout) defer cancel() + tsaToken, err := h.tsaClient.Timestamp(tsaCtx, []byte(seal)) if err != nil { return fmt.Errorf("%w: %w", ErrTSATimestamp, err) } + signature.TSAToken = tsaToken events = append( events, @@ -213,10 +220,12 @@ func (h *sealingHandler) sealAndCommit( } signature.Status = coredata.ElectronicSignatureStatusCompleted + signature.UpdatedAt = time.Now() if err := signature.Update(ctx, tx, scope); err != nil { return fmt.Errorf("cannot update signature: %w", err) } + events = append( events, signature.NewEvent( @@ -260,12 +269,14 @@ func (h *sealingHandler) failSignature( errStr := userFacingError(processingError) signature.LastError = &errStr signature.ProcessingStartedAt = nil + signature.UpdatedAt = time.Now() if signature.AttemptCount >= signature.MaxAttempts { signature.Status = coredata.ElectronicSignatureStatusFailed } else { signature.Status = coredata.ElectronicSignatureStatusAccepted } + if err := signature.Update(ctx, tx, scope); err != nil { return fmt.Errorf("cannot update signature: %w", err) } diff --git a/pkg/esign/service.go b/pkg/esign/service.go index 858ea28b9..39fa60269 100644 --- a/pkg/esign/service.go +++ b/pkg/esign/service.go @@ -119,6 +119,7 @@ func (s *Service) Run(ctx context.Context, presenterConfigFunc EmailPresenterCon s.logger.Named("sealing-worker"), nil, ) + g.Go(func() error { return sealingWorker.Run(sealingWorkerCtx) }) certWorkerCtx, stopCertWorker := context.WithCancel(ctx) @@ -130,6 +131,7 @@ func (s *Service) Run(ctx context.Context, presenterConfigFunc EmailPresenterCon s.bucket, s.logger.Named("completion-certificate-worker"), ) + g.Go(func() error { return certWorker.Run(certWorkerCtx) }) <-gctx.Done() @@ -148,6 +150,7 @@ func (s *Service) CreateSignature( consentText := req.ConsentText if consentText == "" { var err error + consentText, err = req.DocumentType.ConsentText() if err != nil { return nil, fmt.Errorf("cannot derive consent text: %w", err) @@ -500,6 +503,7 @@ func (s *Service) GetEventsBySignatureID( scope = coredata.NewScopeFromObjectID(signatureID) events = coredata.ElectronicSignatureEvents{} ) + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { @@ -510,7 +514,6 @@ func (s *Service) GetEventsBySignatureID( return nil }, ) - if err != nil { return nil, err } diff --git a/pkg/esign/stamp.go b/pkg/esign/stamp.go index ae94220eb..00f6bd214 100644 --- a/pkg/esign/stamp.go +++ b/pkg/esign/stamp.go @@ -32,6 +32,7 @@ func StampSignatureID(pdfData []byte, signatureID string) ([]byte, error) { } reader := bytes.NewReader(pdfData) + var buf bytes.Buffer if err := api.AddWatermarks(reader, &buf, nil, wm, nil); err != nil { diff --git a/pkg/esign/tsa.go b/pkg/esign/tsa.go index 66306cab5..8829e95ad 100644 --- a/pkg/esign/tsa.go +++ b/pkg/esign/tsa.go @@ -57,12 +57,14 @@ func (c *TSAClient) Timestamp(ctx context.Context, data []byte) ([]byte, error) if err != nil { return nil, fmt.Errorf("esign: cannot build TSA HTTP request: %w", err) } + req.Header.Set("Content-Type", "application/timestamp-query") resp, err := httpClient.Do(req) if err != nil { return nil, fmt.Errorf("esign: TSA request failed: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { diff --git a/pkg/evidencedescriber/evidencedescriber.go b/pkg/evidencedescriber/evidencedescriber.go index 1254e0da4..db7d55216 100644 --- a/pkg/evidencedescriber/evidencedescriber.go +++ b/pkg/evidencedescriber/evidencedescriber.go @@ -77,5 +77,6 @@ func (d *Describer) Describe(ctx context.Context, filename string, mimeType stri } text := result.FinalMessage().Text() + return &text, nil } diff --git a/pkg/filemanager/service.go b/pkg/filemanager/service.go index f51fcdb96..c45552037 100644 --- a/pkg/filemanager/service.go +++ b/pkg/filemanager/service.go @@ -58,6 +58,7 @@ func (s *Service) GetFileBase64( if err != nil { return "", "", fmt.Errorf("cannot get file from S3: %w", err) } + defer func() { _ = result.Body.Close() }() fileData, err := io.ReadAll(result.Body) @@ -86,6 +87,7 @@ func (s *Service) GetFileBytes( if err != nil { return nil, fmt.Errorf("cannot get file from S3: %w", err) } + defer func() { _ = result.Body.Close() }() data, err := io.ReadAll(result.Body) diff --git a/pkg/filevalidation/validator.go b/pkg/filevalidation/validator.go index 43127d665..e8b9d3544 100644 --- a/pkg/filevalidation/validator.go +++ b/pkg/filevalidation/validator.go @@ -124,6 +124,7 @@ func WithCategories(categories ...string) Option { if v.AllowedExtensions[ext] == nil { v.AllowedExtensions[ext] = []string{} } + v.AllowedExtensions[ext] = append(v.AllowedExtensions[ext], fileType.MimeType) } } diff --git a/pkg/filevalidation/validator_test.go b/pkg/filevalidation/validator_test.go index 7c7fbb84e..41e2a53e7 100644 --- a/pkg/filevalidation/validator_test.go +++ b/pkg/filevalidation/validator_test.go @@ -402,6 +402,7 @@ func TestMultipleExtensionsPerMimeType(t *testing.T) { func TestExtensionsWithMultipleMimeTypes(t *testing.T) { // Create a map of extensions to MIME types extToMimes := make(map[string][]string) + for _, fileType := range FileTypes { for _, ext := range fileType.Extensions { extToMimes[ext] = append(extToMimes[ext], fileType.MimeType) @@ -438,6 +439,7 @@ func TestExtensionsWithMultipleMimeTypes(t *testing.T) { // BenchmarkValidate benchmarks the Validate function func BenchmarkValidate(b *testing.B) { v := NewValidator(WithCategories(CategoryDocument)) + b.ResetTimer() for i := 0; i < b.N; i++ { @@ -494,6 +496,7 @@ func TestValidateCustomAllowedExtensions(t *testing.T) { if err == nil { t.Error("Expected error for unregistered extension, got none") } + if !contains(err.Error(), "file extension \".jpg\" is not allowed") { t.Errorf("Unexpected error message: %s", err.Error()) } @@ -514,6 +517,7 @@ func TestValidateCustomAllowedExtensions(t *testing.T) { if err == nil { t.Error("Expected error for content type not matching extension, got none") } + if !contains(err.Error(), "content type \"image/png\" does not match extension \".jpg\"") { t.Errorf("Unexpected error message: %s", err.Error()) } diff --git a/pkg/geoloc/service.go b/pkg/geoloc/service.go index da766708a..fa1f3839f 100644 --- a/pkg/geoloc/service.go +++ b/pkg/geoloc/service.go @@ -52,6 +52,7 @@ func (s *Service) ImportFromDir(ctx context.Context, dataDir string) error { } code := strings.ToUpper(entry.Name()) + var cc coredata.CountryCode if err := cc.Scan(code); err != nil { continue @@ -64,6 +65,7 @@ func (s *Service) ImportFromDir(ctx context.Context, dataDir string) error { if errors.Is(err, os.ErrNotExist) { continue } + if err != nil { return fmt.Errorf("cannot parse CIDR file %s: %w", path, err) } @@ -166,9 +168,11 @@ func parseCIDRFile(path string) ([]string, error) { if err != nil { return nil, err } + defer func() { _ = f.Close() }() var cidrs []string + scanner := bufio.NewScanner(f) for scanner.Scan() { diff --git a/pkg/gid/gid.go b/pkg/gid/gid.go index 7aed4404a..9b186f65b 100644 --- a/pkg/gid/gid.go +++ b/pkg/gid/gid.go @@ -40,10 +40,12 @@ var ( // ParseGID parses a string representation of a GID func ParseGID(encoded string) (GID, error) { gid := GID{} + err := gid.UnmarshalText([]byte(encoded)) if err != nil { return Nil, err } + return gid, nil } @@ -64,6 +66,7 @@ func New(tenantID TenantID, entityType uint16) GID { // This should never happen with a valid random source panic(fmt.Sprintf("cannot generate GID: %v", err)) } + return id } @@ -104,6 +107,7 @@ func (gid GID) Value() (driver.Value, error) { func (gid GID) TenantID() TenantID { var tenantID TenantID copy(tenantID[:], gid[0:8]) + return tenantID } @@ -121,6 +125,7 @@ func (gid GID) Timestamp() time.Time { // Scan implements the database/sql/driver.Scanner interface func (gid *GID) Scan(value any) error { var str string + switch v := value.(type) { case string: str = v @@ -131,6 +136,7 @@ func (gid *GID) Scan(value any) error { } enc := base64.RawURLEncoding + id, err := enc.DecodeString(str) if err != nil { return err @@ -155,6 +161,7 @@ func (gid GID) MarshalText() ([]byte, error) { enc := base64.RawURLEncoding buf := make([]byte, enc.EncodedLen(len(gid))) enc.Encode(buf, gid[:]) + return buf, nil } @@ -162,6 +169,7 @@ func (gid GID) MarshalText() ([]byte, error) { func (gid *GID) UnmarshalText(encoded []byte) error { enc := base64.RawURLEncoding dst := make([]byte, enc.DecodedLen(len(encoded))) + n, err := enc.Decode(dst, encoded) if err != nil { return err @@ -172,5 +180,6 @@ func (gid *GID) UnmarshalText(encoded []byte) error { } copy((*gid)[:], dst) + return nil } diff --git a/pkg/gid/tenant_id.go b/pkg/gid/tenant_id.go index be8f96d3b..4256adf1b 100644 --- a/pkg/gid/tenant_id.go +++ b/pkg/gid/tenant_id.go @@ -114,6 +114,7 @@ func (id *TenantID) Scan(value any) error { } copy((*id)[:], decoded) + return nil default: return fmt.Errorf("invalid type for TenantID: expected string, got %T", value) @@ -143,6 +144,7 @@ func (id *TenantID) UnmarshalText(text []byte) error { } copy((*id)[:], decoded) + return nil } diff --git a/pkg/html2pdf/converter.go b/pkg/html2pdf/converter.go index 589632990..fed566ab8 100644 --- a/pkg/html2pdf/converter.go +++ b/pkg/html2pdf/converter.go @@ -112,6 +112,7 @@ func getPageDimensions(format PageFormat, orientation Orientation) (width, heigh if orientation == OrientationLandscape { return h, w // Swap width and height for landscape } + return w, h } @@ -174,6 +175,7 @@ func (c *Converter) GeneratePDF(ctx context.Context, htmlDocument []byte, cfg Re if cfg.WaitForExpression == "" { return nil } + deadline := time.Now().Add(waitTimeout) for time.Now().Before(deadline) { var ready bool @@ -181,11 +183,14 @@ func (c *Converter) GeneratePDF(ctx context.Context, htmlDocument []byte, cfg Re time.Sleep(100 * time.Millisecond) continue } + if ready { return nil } + time.Sleep(100 * time.Millisecond) } + return nil // proceed even on timeout }) @@ -198,9 +203,11 @@ func (c *Converter) GeneratePDF(ctx context.Context, htmlDocument []byte, cfg Re if err != nil { return fmt.Errorf("cannot get frame tree: %w", err) } + if err := page.SetDocumentContent(frameTree.Frame.ID, htmlContent).Do(ctx); err != nil { return fmt.Errorf("cannot set document content: %w", err) } + return nil }), chromedp.WaitReady("body"), @@ -224,7 +231,6 @@ func (c *Converter) GeneratePDF(ctx context.Context, htmlDocument []byte, cfg Re }, ), ) - if err != nil { err2 := fmt.Errorf("cannot run chromedp: %w", err) diff --git a/pkg/html2pdf/margin_test.go b/pkg/html2pdf/margin_test.go index 50fa5eabc..4f63ccb52 100644 --- a/pkg/html2pdf/margin_test.go +++ b/pkg/html2pdf/margin_test.go @@ -325,6 +325,7 @@ func BenchmarkParseMargin(b *testing.B) { } b.ResetTimer() + for i := 0; i < b.N; i++ { for _, tc := range testCases { ParseMargin(tc) @@ -341,6 +342,7 @@ func BenchmarkMarginToInches(b *testing.B) { } b.ResetTimer() + for i := 0; i < b.N; i++ { for _, margin := range margins { margin.ToInches() @@ -357,6 +359,7 @@ func BenchmarkMarginString(b *testing.B) { } b.ResetTimer() + for i := 0; i < b.N; i++ { for _, margin := range margins { _ = margin.String() diff --git a/pkg/iam/account_service.go b/pkg/iam/account_service.go index 5aefe8868..3db661301 100644 --- a/pkg/iam/account_service.go +++ b/pkg/iam/account_service.go @@ -101,6 +101,7 @@ func (s AccountService) ChangeEmail(ctx context.Context, identityID gid.GID, req ctx, func(ctx context.Context, tx pg.Tx) error { identity := &coredata.Identity{} + err := identity.LoadByID(ctx, tx, identityID) if err != nil { if err == coredata.ErrResourceNotFound { @@ -164,6 +165,7 @@ func (s AccountService) VerifyEmail(ctx context.Context, token string) error { ctx, func(ctx context.Context, tx pg.Tx) error { identity := &coredata.Identity{} + err := identity.LoadByID(ctx, tx, payload.Data.IdentityID) if err != nil { if err == coredata.ErrResourceNotFound { @@ -208,6 +210,7 @@ func (s *AccountService) ListPendingInvitations( ctx, func(ctx context.Context, conn pg.Querier) error { profile := coredata.MembershipProfile{} + err := profile.LoadByID(ctx, conn, scope, userID) if err != nil { if err == coredata.ErrResourceNotFound { @@ -227,7 +230,6 @@ func (s *AccountService) ListPendingInvitations( return nil }, ) - if err != nil { return nil, err } @@ -244,6 +246,7 @@ func (s AccountService) ChangePassword(ctx context.Context, identityID gid.GID, ctx, func(ctx context.Context, tx pg.Tx) error { identity := &coredata.Identity{} + err := identity.LoadByID(ctx, tx, identityID) if err != nil { if err == coredata.ErrResourceNotFound { @@ -294,6 +297,7 @@ func (s AccountService) CountSessions(ctx context.Context, identityID gid.GID) ( ctx, func(ctx context.Context, conn pg.Querier) (err error) { sessions := coredata.Sessions{} + count, err = sessions.CountByIdentityID(ctx, conn, identityID) if err != nil { return fmt.Errorf("cannot count sessions: %w", err) @@ -324,7 +328,6 @@ func (s AccountService) ListSessions( return nil }, ) - if err != nil { return nil, err } @@ -411,7 +414,6 @@ func (s AccountService) ListPersonalAPIKeys( return nil }, ) - if err != nil { return nil, err } @@ -426,6 +428,7 @@ func (s AccountService) CountPersonalAPIKeys(ctx context.Context, identityID gid ctx, func(ctx context.Context, conn pg.Querier) (err error) { personalAccessTokens := coredata.PersonalAPIKeys{} + count, err = personalAccessTokens.CountByIdentityID(ctx, conn, identityID) if err != nil { return fmt.Errorf("cannot count personal access tokens: %w", err) @@ -452,6 +455,7 @@ func (s *AccountService) RevealPersonalAPIKeyToken( if err == coredata.ErrResourceNotFound { return NewPersonalAPIKeyNotFoundError(personalAPIKeyID) } + return fmt.Errorf("cannot load personal api key: %w", err) } @@ -470,7 +474,6 @@ func (s *AccountService) RevealPersonalAPIKeyToken( return nil }, ) - if err != nil { return "", err } @@ -488,6 +491,7 @@ func (s AccountService) GetIdentityForMembership(ctx context.Context, membership ctx, func(ctx context.Context, conn pg.Querier) error { membership := &coredata.Membership{} + err := membership.LoadByID(ctx, conn, scope, membershipID) if err != nil { if err == coredata.ErrResourceNotFound { @@ -509,7 +513,6 @@ func (s AccountService) GetIdentityForMembership(ctx context.Context, membership return nil }, ) - if err != nil { return nil, err } @@ -557,7 +560,6 @@ func (s *AccountService) CreatePersonalAPIKey( return nil }, ) - if err != nil { return nil, "", err } @@ -574,6 +576,7 @@ func (s *AccountService) DeletePersonalAPIKey( ctx, func(ctx context.Context, tx pg.Tx) error { personalAPIKey := &coredata.PersonalAPIKey{} + err := personalAPIKey.LoadByID(ctx, tx, personalAPIKeyID) if err != nil { if err == coredata.ErrResourceNotFound { @@ -599,6 +602,7 @@ func (s *AccountService) DeletePersonalAPIKey( func (s AccountService) ListOrganizations(ctx context.Context, identityID gid.GID) ([]*coredata.Organization, error) { var organizations coredata.Organizations + orderBy := page.OrderBy[coredata.OrganizationOrderField]{ Field: coredata.OrganizationOrderFieldCreatedAt, Direction: page.OrderDirectionDesc, @@ -616,7 +620,6 @@ func (s AccountService) ListOrganizations(ctx context.Context, identityID gid.GI return nil }, ) - if err != nil { return nil, err } @@ -657,7 +660,6 @@ func (s AccountService) GetMembershipForOrganization( return nil }, ) - if err != nil { return nil, err } @@ -736,7 +738,6 @@ func (s *AccountService) ListProfilesForIdentity( return nil }, ) - if err != nil { return nil, err } @@ -757,6 +758,7 @@ func (s AccountService) CountProfiles( ctx, func(ctx context.Context, conn pg.Querier) (err error) { profiles := coredata.MembershipProfiles{} + count, err = profiles.CountByIdentityID(ctx, conn, identityID, filter) if err != nil { return fmt.Errorf("cannot count profiles: %w", err) diff --git a/pkg/iam/api_key.go b/pkg/iam/api_key.go index 95d2cfe98..bd94d3f9a 100644 --- a/pkg/iam/api_key.go +++ b/pkg/iam/api_key.go @@ -75,7 +75,6 @@ func (s *APIKeyService) GetAPIKey(ctx context.Context, keyID gid.GID) (*coredata return nil }, ) - if err != nil { return nil, err } diff --git a/pkg/iam/auth_service.go b/pkg/iam/auth_service.go index 91b3f8bb9..4bb8f077a 100644 --- a/pkg/iam/auth_service.go +++ b/pkg/iam/auth_service.go @@ -96,6 +96,7 @@ func (req ResetPasswordRequest) Validate() error { v := validator.New() v.Check(req.Token, "token", validator.NotEmpty()) v.Check(req.Password, "password", PasswordValidator()) + return v.Error() } @@ -107,6 +108,7 @@ func (req ChangePasswordRequest) Validate() error { v.Check(req.CurrentPassword, "currentPassword", validator.NotEmpty(), validator.MaxLen(255)) v.Check(req.NewPassword, "newPassword", PasswordValidator()) + return v.Error() } @@ -202,6 +204,7 @@ func (s *AuthService) ActivateAccount( // Expire other pending invitations for user invitations := &coredata.Invitations{} + onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending}) if err := invitations.ExpireByUserID( ctx, @@ -258,6 +261,7 @@ func (s AuthService) ResetPassword( ctx, func(ctx context.Context, tx pg.Tx) error { identity := &coredata.Identity{} + err := identity.LoadByEmail(ctx, tx, payload.Data.Email) if err != nil { if err == coredata.ErrResourceNotFound { @@ -433,6 +437,7 @@ func (s AuthService) OpenSessionWithSAML(ctx context.Context, identityID gid.GID ctx, func(ctx context.Context, conn pg.Tx) (err error) { session = coredata.NewRootSession(identityID, coredata.AuthMethodSAML, s.sessionDuration) + err = session.Insert(ctx, conn) if err != nil { return fmt.Errorf("cannot insert session: %w", err) @@ -441,7 +446,6 @@ func (s AuthService) OpenSessionWithSAML(ctx context.Context, identityID gid.GID return nil }, ) - if err != nil { return nil, err } @@ -456,6 +460,7 @@ func (s AuthService) OpenSessionWithOIDC(ctx context.Context, identityID gid.GID ctx, func(ctx context.Context, conn pg.Tx) (err error) { session = coredata.NewRootSession(identityID, authMethod, s.sessionDuration) + err = session.Insert(ctx, conn) if err != nil { return fmt.Errorf("cannot insert session: %w", err) @@ -464,7 +469,6 @@ func (s AuthService) OpenSessionWithOIDC(ctx context.Context, identityID gid.GID return nil }, ) - if err != nil { return nil, err } @@ -528,6 +532,7 @@ func (s AuthService) OpenSessionWithPassword(ctx context.Context, identityID gid ctx, func(ctx context.Context, conn pg.Tx) (err error) { session = coredata.NewRootSession(identityID, coredata.AuthMethodPassword, s.sessionDuration) + err = session.Insert(ctx, conn) if err != nil { return fmt.Errorf("cannot insert session: %w", err) @@ -536,7 +541,6 @@ func (s AuthService) OpenSessionWithPassword(ctx context.Context, identityID gid return nil }, ) - if err != nil { return nil, err } @@ -562,6 +566,7 @@ func (s AuthService) SendMagicLink(ctx context.Context, req *SendMagicLinkReques ctx, func(ctx context.Context, tx pg.Tx) error { hashedToken := HashToken(tokenString) + token := &coredata.Token{ ID: gid.New(gid.NilTenant, coredata.TokenEntityType), HashedValue: hashedToken, @@ -590,8 +595,10 @@ func (s AuthService) SendMagicLink(ctx context.Context, req *SendMagicLinkReques } emailPresenterCfg := emails.DefaultPresenterConfig(s.bucket, s.baseURL) + if req.CompliancePageID != nil { var err error + emailPresenterCfg, err = s.CompliancePageService.EmailPresenterConfig(ctx, *req.CompliancePageID) if err != nil { return fmt.Errorf("cannot get compliance page email presenter config: %w", err) @@ -701,6 +708,7 @@ func (s AuthService) OpenSessionWithMagicLink(ctx context.Context, tokenString s } session = coredata.NewRootSession(identity.ID, coredata.AuthMethodMagicLink, s.sessionDuration) + err = session.Insert(ctx, tx) if err != nil { return fmt.Errorf("cannot insert session: %w", err) diff --git a/pkg/iam/authorizer.go b/pkg/iam/authorizer.go index 9fe590f40..ef4766a1d 100644 --- a/pkg/iam/authorizer.go +++ b/pkg/iam/authorizer.go @@ -101,8 +101,10 @@ func (a *Authorizer) authorize(ctx context.Context, tx pg.Tx, params AuthorizePa *params.Session, membership.ID, ); err != nil { - var errSessionNotFound *ErrSessionNotFound - var errSessionExpired *ErrSessionExpired + var ( + errSessionNotFound *ErrSessionNotFound + errSessionExpired *ErrSessionExpired + ) if errors.As(err, &errSessionNotFound) || errors.As(err, &errSessionExpired) { return NewAssumptionRequiredError(params.Principal, membership.ID) @@ -222,6 +224,7 @@ func (a *Authorizer) buildPrincipalAttributes( if err != nil { return nil, fmt.Errorf("cannot load principal attributes: %w", err) } + maps.Copy(attrs, entityAttrs) } } @@ -252,6 +255,7 @@ func (a *Authorizer) buildResourceAttributes( if err != nil { return nil, fmt.Errorf("cannot load resource attributes: %w", err) } + maps.Copy(attrs, entityAttrs) if params.ResourceAttributes != nil { @@ -312,6 +316,7 @@ func (a *Authorizer) recordAuditLog( "cannot parse organization id for audit log", log.Error(err), ) + return } @@ -331,6 +336,7 @@ func (a *Authorizer) recordAuditLog( "cannot marshal audit log metadata", log.Error(err), ) + return } diff --git a/pkg/iam/oauth2server/errors.go b/pkg/iam/oauth2server/errors.go index 25da8480a..ce4aa7d81 100644 --- a/pkg/iam/oauth2server/errors.go +++ b/pkg/iam/oauth2server/errors.go @@ -32,6 +32,7 @@ func (e *OAuth2Error) Error() string { if e.description != "" { return e.code + ": " + e.description } + return e.code } @@ -43,6 +44,7 @@ func (e *OAuth2Error) Is(target error) bool { if !ok { return false } + return e.code == t.code } @@ -66,6 +68,7 @@ func NewError(code *OAuth2Error, opts ...ErrorOption) *OAuth2Error { for _, opt := range opts { opt(e) } + return e } diff --git a/pkg/iam/oauth2server/gc.go b/pkg/iam/oauth2server/gc.go index 2182dc046..95af08261 100644 --- a/pkg/iam/oauth2server/gc.go +++ b/pkg/iam/oauth2server/gc.go @@ -88,24 +88,28 @@ func (h *gcHandler) cleanup(ctx context.Context) error { ctx, func(ctx context.Context, tx pg.Tx) error { var authCode coredata.OAuth2AuthorizationCode + authCodesDeleted, err := authCode.DeleteExpired(ctx, tx, now) if err != nil { return fmt.Errorf("cannot delete expired authorization codes: %w", err) } var accessToken coredata.OAuth2AccessToken + accessTokensDeleted, err := accessToken.DeleteExpired(ctx, tx, now) if err != nil { return fmt.Errorf("cannot delete expired access tokens: %w", err) } var refreshToken coredata.OAuth2RefreshToken + refreshTokensDeleted, err := refreshToken.DeleteExpired(ctx, tx, now) if err != nil { return fmt.Errorf("cannot delete expired refresh tokens: %w", err) } var deviceCode coredata.OAuth2DeviceCode + deviceCodesDeleted, err := deviceCode.DeleteExpired(ctx, tx, now) if err != nil { return fmt.Errorf("cannot delete expired device codes: %w", err) diff --git a/pkg/iam/oauth2server/service.go b/pkg/iam/oauth2server/service.go index 83e068ddf..3f051c7ff 100644 --- a/pkg/iam/oauth2server/service.go +++ b/pkg/iam/oauth2server/service.go @@ -154,6 +154,7 @@ func NewService( opts ...Option, ) *Service { var activeIdx []int + for i, k := range signingKeys { if k.Active { activeIdx = append(activeIdx, i) @@ -185,6 +186,7 @@ func NewService( func (s *Service) signingKey() *SigningKey { n := s.rrCounter.Add(1) idx := s.activeSigningIdx[n%uint64(len(s.activeSigningIdx))] + return &s.signingKeys[idx] } @@ -772,6 +774,7 @@ func (s *Service) PollDeviceCode( // Rate limiting. var slowDown bool + if deviceCode.LastPolledAt != nil { elapsed := now.Sub(ref.UnrefOrZero(deviceCode.LastPolledAt)) if elapsed < time.Duration(deviceCode.PollInterval)*time.Second { @@ -1091,6 +1094,7 @@ func (s *Service) RegisterClient( ) var membership coredata.Membership + err := s.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { @@ -1171,9 +1175,12 @@ func (s *Service) IntrospectToken( if errors.Is(err, coredata.ErrResourceNotFound) { return nil } + return fmt.Errorf("cannot load access token: %w", err) } + hasAccess = true + return nil } @@ -1182,9 +1189,12 @@ func (s *Service) IntrospectToken( if errors.Is(err, coredata.ErrResourceNotFound) { return nil } + return fmt.Errorf("cannot load refresh token: %w", err) } + hasRefresh = true + return nil } @@ -1197,18 +1207,22 @@ func (s *Service) IntrospectToken( if err := loadRefresh(ctx, conn); err != nil { return err } + if hasRefresh { return nil } + return loadAccess(ctx, conn) } if err := loadAccess(ctx, conn); err != nil { return err } + if hasAccess { return nil } + return loadRefresh(ctx, conn) }, ); err != nil { @@ -1220,6 +1234,7 @@ func (s *Service) IntrospectToken( if now.After(accessToken.ExpiresAt) { return nil, nil } + return &IntrospectResult{ ClientID: accessToken.ClientID, IdentityID: accessToken.IdentityID, @@ -1232,6 +1247,7 @@ func (s *Service) IntrospectToken( if refreshToken.RevokedAt != nil || now.After(refreshToken.ExpiresAt) { return nil, nil } + return &IntrospectResult{ ClientID: refreshToken.ClientID, IdentityID: refreshToken.IdentityID, @@ -1299,10 +1315,12 @@ func (s *Service) RevokeToken( func(ctx context.Context, tx pg.Tx) error { if tokenTypeHint != nil && *tokenTypeHint == coredata.OAuth2TokenTypeHintRefreshToken { refreshToken := coredata.OAuth2RefreshToken{} + err := refreshToken.LoadByHashedValueAndClientID(ctx, tx, hashedValue, clientID) if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) { return fmt.Errorf("cannot load refresh token: %w", err) } + if err == nil { now := time.Now() if err := refreshToken.Revoke(ctx, tx, now); err != nil { @@ -1320,10 +1338,12 @@ func (s *Service) RevokeToken( } accessToken := coredata.OAuth2AccessToken{} + err = accessToken.LoadByHashedValueAndClientID(ctx, tx, hashedValue, clientID) if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) { return fmt.Errorf("cannot load access token: %w", err) } + if err == nil { if err := accessToken.Delete(ctx, tx); err != nil { return fmt.Errorf("cannot delete access token: %w", err) @@ -1334,22 +1354,27 @@ func (s *Service) RevokeToken( } accessToken := coredata.OAuth2AccessToken{} + err := accessToken.LoadByHashedValueAndClientID(ctx, tx, hashedValue, clientID) if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) { return fmt.Errorf("cannot load access token: %w", err) } + if err == nil { if err := accessToken.Delete(ctx, tx); err != nil { return fmt.Errorf("cannot delete access token: %w", err) } + return nil } refreshToken := coredata.OAuth2RefreshToken{} + err = refreshToken.LoadByHashedValueAndClientID(ctx, tx, hashedValue, clientID) if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) { return fmt.Errorf("cannot load refresh token: %w", err) } + if err == nil { now := time.Now() if err := refreshToken.Revoke(ctx, tx, now); err != nil { @@ -1452,6 +1477,7 @@ func (s *Service) Authorize( requestedScopes, ); err == nil { var err error + code, err = s.issueAuthorizationCode( ctx, tx, @@ -1517,6 +1543,7 @@ func (s *Service) GetConsentByID( consentID gid.GID, ) (*coredata.OAuth2Consent, error) { var consent coredata.OAuth2Consent + if err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { @@ -1645,6 +1672,7 @@ func (s *Service) ApproveConsent( } result.IsDeviceFlow = true + return nil } diff --git a/pkg/iam/oidc/gc.go b/pkg/iam/oidc/gc.go index 9eb2b6f06..11adffed7 100644 --- a/pkg/iam/oidc/gc.go +++ b/pkg/iam/oidc/gc.go @@ -94,6 +94,7 @@ func (gc *GarbageCollector) cleanup(ctx context.Context) error { ctx, func(ctx context.Context, tx pg.Tx) error { var state coredata.OIDCState + deleted, err := state.DeleteExpired(ctx, tx, now) if err != nil { return fmt.Errorf("cannot delete expired oidc states: %w", err) diff --git a/pkg/iam/oidc/service.go b/pkg/iam/oidc/service.go index 6ad0ea42e..bb0b2d3eb 100644 --- a/pkg/iam/oidc/service.go +++ b/pkg/iam/oidc/service.go @@ -126,6 +126,7 @@ func (c *idTokenClaims) hasAudience(clientID string) bool { } } } + return false } @@ -136,6 +137,7 @@ func (c *idTokenClaims) isEmailVerified() bool { case string: return strings.EqualFold(v, "true") } + return false } @@ -228,11 +230,13 @@ func NewService( func (s *Service) Run(ctx context.Context) error { wg := sync.WaitGroup{} + ctx, cancel := context.WithCancelCause(ctx) defer cancel(context.Canceled) gcCtx, stopGC := context.WithCancel(context.WithoutCancel(ctx)) gc := NewGarbageCollector(s.pg, s.logger) + wg.Go( func() { if err := gc.Run(gcCtx); err != nil { @@ -260,7 +264,9 @@ func (s *Service) EnabledProviders() []coredata.OIDCProvider { for p := range s.providers { providers = append(providers, p) } + slices.Sort(providers) + return providers } @@ -306,6 +312,7 @@ func (s *Service) InitiateLogin( if err := oidcState.Insert(ctx, tx); err != nil { return fmt.Errorf("cannot store oidc state: %w", err) } + return nil }, ) @@ -337,6 +344,7 @@ func (s *Service) HandleCallback( } var oidcState coredata.OIDCState + err := s.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { @@ -344,6 +352,7 @@ func (s *Service) HandleCallback( if errors.Is(err, coredata.ErrResourceNotFound) { return NewInvalidStateError() } + return fmt.Errorf("cannot load oidc state: %w", err) } @@ -403,12 +412,14 @@ func (s *Service) HandleCallback( } var identity *coredata.Identity + now := time.Now() err = s.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { identity = &coredata.Identity{} + err := identity.LoadByEmail(ctx, tx, email) if err != nil { if !errors.Is(err, coredata.ErrResourceNotFound) { @@ -475,6 +486,7 @@ func (s *Service) verifyAndParseIDToken(ctx context.Context, info *providerInfo, } signedContent := parts[0] + "." + parts[1] + signature, err := base64.RawURLEncoding.DecodeString(parts[2]) if err != nil { return nil, fmt.Errorf("cannot decode signature: %w", err) @@ -525,6 +537,7 @@ func (s *Service) getSigningKey(ctx context.Context, jwksURL string, kid string) } entry = &jwksEntry{keys: keys, fetchedAt: time.Now()} + s.jwksMu.Lock() s.jwksCache[jwksURL] = entry s.jwksMu.Unlock() @@ -543,6 +556,7 @@ func (s *Service) getSigningKey(ctx context.Context, jwksURL string, kid string) } entry = &jwksEntry{keys: keys, fetchedAt: time.Now()} + s.jwksMu.Lock() s.jwksCache[jwksURL] = entry s.jwksMu.Unlock() @@ -566,6 +580,7 @@ func fetchJWKS(ctx context.Context, httpClient *http.Client, jwksURL string) ([] if err != nil { return nil, fmt.Errorf("cannot fetch jwks: %w", err) } + defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) @@ -592,12 +607,14 @@ func parseJWK(k jwk) (crypto.PublicKey, error) { if err != nil { return nil, fmt.Errorf("cannot decode RSA modulus: %w", err) } + eBytes, err := base64.RawURLEncoding.DecodeString(k.E) if err != nil { return nil, fmt.Errorf("cannot decode RSA exponent: %w", err) } n := new(big.Int).SetBytes(nBytes) + e := 0 for _, b := range eBytes { e = e<<8 + int(b) @@ -607,6 +624,7 @@ func parseJWK(k jwk) (crypto.PublicKey, error) { case "EC": var curve elliptic.Curve + switch k.Crv { case "P-256": curve = elliptic.P256() @@ -622,6 +640,7 @@ func parseJWK(k jwk) (crypto.PublicKey, error) { if err != nil { return nil, fmt.Errorf("cannot decode EC X: %w", err) } + yBytes, err := base64.RawURLEncoding.DecodeString(k.Y) if err != nil { return nil, fmt.Errorf("cannot decode EC Y: %w", err) @@ -647,6 +666,7 @@ func verifySignature(alg string, key crypto.PublicKey, signedContent []byte, sig if !ok { return fmt.Errorf("cannot verify RS256 signature: expected RSA public key") } + return rsa.VerifyPKCS1v15(rsaKey, crypto.SHA256, hash[:], signature) case "ES256": @@ -676,6 +696,7 @@ func verifySignature(alg string, key crypto.PublicKey, signedContent []byte, sig if !ecdsa.VerifyASN1(ecKey, hash[:], derSig) { return fmt.Errorf("cannot verify ECDSA signature") } + return nil default: @@ -693,5 +714,6 @@ func generateRandomString(length int) (string, error) { if _, err := io.ReadFull(cryptoRandReader, b); err != nil { return "", fmt.Errorf("cannot generate random bytes: %w", err) } + return base64.RawURLEncoding.EncodeToString(b), nil } diff --git a/pkg/iam/organization_service.go b/pkg/iam/organization_service.go index 0b72e3fac..77231d84c 100644 --- a/pkg/iam/organization_service.go +++ b/pkg/iam/organization_service.go @@ -190,12 +190,15 @@ func (req UpdateOrganizationRequest) Validate() error { v.Check(req.Email, "email", validator.SafeText(255)) v.Check(req.HeadquarterAddress, "headquarter_address", validator.SafeText(2048)) v.Check(req.LogoFile, "logo_file", validator.NotEmpty()) + if req.LogoFile != nil { if err := fv.Validate(req.LogoFile.Filename, req.LogoFile.ContentType, req.LogoFile.Size); err != nil { return fmt.Errorf("invalid logo file: %w", err) } } + v.Check(req.HorizontalLogoFile, "horizontal_logo_file", validator.NotEmpty()) + if req.HorizontalLogoFile != nil { if err := fv.Validate(req.HorizontalLogoFile.Filename, req.HorizontalLogoFile.ContentType, req.HorizontalLogoFile.Size); err != nil { return fmt.Errorf("invalid horizontal logo file: %w", err) @@ -250,10 +253,10 @@ func (s *OrganizationService) UpdateMembership( scope := coredata.NewScopeFromObjectID(organizationID) membership := coredata.Membership{} + if err := s.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { - if err := membership.LoadByID(ctx, tx, scope, membershipID); err != nil { if err == coredata.ErrResourceNotFound { return NewMembershipNotFoundError(membershipID) @@ -273,6 +276,7 @@ func (s *OrganizationService) UpdateMembership( if membership.Role == coredata.MembershipRoleOwner && role != coredata.MembershipRoleOwner && profile.State == coredata.ProfileStateActive { profiles := coredata.MembershipProfiles{} + count, err := profiles.CountActiveOwnerByOrganizationID(ctx, tx, scope, organizationID) if err != nil { return fmt.Errorf("cannot count active owners: %w", err) @@ -334,6 +338,7 @@ func (s *OrganizationService) RemoveUser( if membership.Role == coredata.MembershipRoleOwner && profile.State == coredata.ProfileStateActive { profiles := coredata.MembershipProfiles{} + count, err := profiles.CountActiveOwnerByOrganizationID(ctx, tx, scope, organizationID) if err != nil { return fmt.Errorf("cannot count active owners: %w", err) @@ -382,6 +387,7 @@ func (s *OrganizationService) InviteUser( ctx, func(ctx context.Context, tx pg.Tx) error { organization := coredata.Organization{} + err := organization.LoadByID(ctx, tx, scope, req.OrganizationID) if err != nil { if err == coredata.ErrResourceNotFound { @@ -448,7 +454,6 @@ func (s *OrganizationService) InviteUser( return nil }, ) - if err != nil { return nil, err } @@ -556,7 +561,6 @@ func (s *OrganizationService) CreateOrganization( "organization-id": organization.ID.String(), }, ) - if err != nil { return nil, nil, fmt.Errorf("cannot upload logo file: %w", err) } @@ -594,7 +598,6 @@ func (s *OrganizationService) CreateOrganization( "organization-id": organization.ID.String(), }, ) - if err != nil { return nil, nil, fmt.Errorf("cannot upload logo file: %w", err) } @@ -606,6 +609,7 @@ func (s *OrganizationService) CreateOrganization( ctx, func(ctx context.Context, tx pg.Tx) error { identity := &coredata.Identity{} + err := identity.LoadByID(ctx, tx, identityID) if err != nil { return fmt.Errorf("cannot load identity: %w", err) @@ -737,7 +741,6 @@ func (s *OrganizationService) UpdateOrganization(ctx context.Context, organizati "organization-id": organizationID.String(), }, ) - if err != nil { return nil, fmt.Errorf("cannot upload logo file: %w", err) } @@ -775,7 +778,6 @@ func (s *OrganizationService) UpdateOrganization(ctx context.Context, organizati "organization-id": organizationID.String(), }, ) - if err != nil { return nil, fmt.Errorf("cannot upload logo file: %w", err) } @@ -811,6 +813,7 @@ func (s *OrganizationService) UpdateOrganization(ctx context.Context, organizati return fmt.Errorf("invalid email address: %w", err) } } + organization.Email = *req.Email } @@ -871,6 +874,7 @@ func (s *OrganizationService) DeleteOrganization(ctx context.Context, organizati ctx, func(ctx context.Context, tx pg.Tx) error { organization := &coredata.Organization{} + err := organization.LoadByID(ctx, tx, scope, organizationID) if err != nil { return fmt.Errorf("cannot load organization: %w", err) @@ -970,7 +974,6 @@ func (s *OrganizationService) CreateUser(ctx context.Context, req *CreateUserReq return nil }, ) - if err != nil { return nil, err } @@ -1017,6 +1020,7 @@ func (s *OrganizationService) UpdateUser(ctx context.Context, req *UpdateUserReq } membership := &coredata.Membership{} + var webhookPayload *webhooktypes.User if err := membership.LoadByIdentityIDAndOrganizationID(ctx, conn, scope, profile.IdentityID, profile.OrganizationID); err != nil { @@ -1042,7 +1046,6 @@ func (s *OrganizationService) UpdateUser(ctx context.Context, req *UpdateUserReq return nil }, ) - if err != nil { return nil, err } @@ -1077,7 +1080,6 @@ func (s *OrganizationService) UpdateUserState( return nil }, ) - if err != nil { return nil, err } @@ -1102,7 +1104,6 @@ func (s *OrganizationService) GetProfile(ctx context.Context, profileID gid.GID) return nil }, ) - if err != nil { return nil, err } @@ -1162,7 +1163,6 @@ func (s *OrganizationService) GetProfileForIdentityAndOrganization(ctx context.C return nil }, ) - if err != nil { return nil, err } @@ -1191,7 +1191,6 @@ func (s *OrganizationService) ListProfiles( return nil }, ) - if err != nil { return nil, err } @@ -1213,6 +1212,7 @@ func (s OrganizationService) CountProfiles( ctx, func(ctx context.Context, conn pg.Querier) (err error) { profiles := coredata.MembershipProfiles{} + count, err = profiles.CountByOrganizationID(ctx, conn, scope, organizationID, filter) if err != nil { return fmt.Errorf("cannot count profiles: %w", err) @@ -1235,6 +1235,7 @@ func (s *OrganizationService) GetOrganizationForMembership(ctx context.Context, ctx, func(ctx context.Context, conn pg.Querier) error { membership := &coredata.Membership{} + err := membership.LoadByID(ctx, conn, scope, membershipID) if err != nil { if err == coredata.ErrResourceNotFound { @@ -1256,7 +1257,6 @@ func (s *OrganizationService) GetOrganizationForMembership(ctx context.Context, return nil }, ) - if err != nil { return nil, err } @@ -1425,6 +1425,7 @@ func (s OrganizationService) CountSAMLConfigurations( ctx, func(ctx context.Context, conn pg.Querier) (err error) { samlConfigurations := coredata.SAMLConfigurations{} + count, err = samlConfigurations.CountByOrganizationID(ctx, conn, scope, organizationID) if err != nil { return fmt.Errorf("cannot count saml configurations: %w", err) @@ -1478,6 +1479,7 @@ func (s OrganizationService) CountSCIMEvents( ctx, func(ctx context.Context, conn pg.Querier) (err error) { scimEvents := coredata.SCIMEvents{} + count, err = scimEvents.CountByOrganizationID(ctx, conn, scope, organizationID) if err != nil { return fmt.Errorf("cannot count scim events: %w", err) @@ -1510,10 +1512,10 @@ func (s OrganizationService) GetSCIMConfiguration( return fmt.Errorf("cannot load SCIM configuration: %w", err) } + return nil }, ) - if err != nil { return nil, err } @@ -1551,12 +1553,13 @@ func (s OrganizationService) CreateSCIMConfiguration( if err == coredata.ErrResourceAlreadyExists { return scim.NewSCIMConfigurationAlreadyExistsError(organizationID) } + return fmt.Errorf("cannot insert SCIM configuration: %w", err) } + return nil }, ) - if err != nil { return nil, "", err } @@ -1575,6 +1578,7 @@ func (s OrganizationService) DeleteSCIMConfiguration( ctx, func(ctx context.Context, tx pg.Tx) error { config := &coredata.SCIMConfiguration{} + err := config.LoadByID(ctx, tx, scope, configID) if err != nil { if err == coredata.ErrResourceNotFound { @@ -1589,6 +1593,7 @@ func (s OrganizationService) DeleteSCIMConfiguration( } profiles := &coredata.MembershipProfiles{} + err = profiles.ResetSCIMSources(ctx, tx, scope, config.OrganizationID) if err != nil { return fmt.Errorf("cannot reset user sources: %w", err) @@ -1596,6 +1601,7 @@ func (s OrganizationService) DeleteSCIMConfiguration( // Delete SCIM bridge and its connector if they exist bridge := &coredata.SCIMBridge{} + err = bridge.LoadBySCIMConfigurationID(ctx, tx, scope, configID) if err != nil && err != coredata.ErrResourceNotFound { return fmt.Errorf("cannot load SCIM bridge: %w", err) @@ -1608,6 +1614,7 @@ func (s OrganizationService) DeleteSCIMConfiguration( // bridge alone is sufficient to unbind SCIM from the connector. if bridge.ConnectorID != nil { accessSources := &coredata.AccessSources{} + count, err := accessSources.CountByConnectorID(ctx, tx, scope, *bridge.ConnectorID) if err != nil { return fmt.Errorf("cannot count access sources for connector: %w", err) @@ -1615,6 +1622,7 @@ func (s OrganizationService) DeleteSCIMConfiguration( if count == 0 { connector := &coredata.Connector{ID: *bridge.ConnectorID} + err = connector.Delete(ctx, tx, scope) if err != nil && err != coredata.ErrResourceNotFound { return fmt.Errorf("cannot delete connector: %w", err) @@ -1680,7 +1688,6 @@ func (s OrganizationService) RegenerateSCIMToken( return nil }, ) - if err != nil { return nil, "", err } @@ -1724,7 +1731,6 @@ func (s OrganizationService) UpdateSCIMBridge( return nil }, ) - if err != nil { return nil, err } @@ -1773,6 +1779,7 @@ func (s OrganizationService) CountSCIMEventsByConfigID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { scimEvents := coredata.SCIMEvents{} + count, err = scimEvents.CountBySCIMConfigurationID(ctx, conn, scope, scimConfigurationID) if err != nil { return fmt.Errorf("cannot count scim events: %w", err) @@ -1833,6 +1840,7 @@ func (s OrganizationService) CreateSAMLConfiguration( ctx, func(ctx context.Context, tx pg.Tx) error { organization := &coredata.Organization{} + err := organization.LoadByID(ctx, tx, scope, organizationID) if err != nil { return fmt.Errorf("cannot load organization: %w", err) @@ -1872,12 +1880,14 @@ func (s OrganizationService) UpdateSAMLConfiguration( ctx, func(ctx context.Context, tx pg.Tx) error { organization := &coredata.Organization{} + err := organization.LoadByID(ctx, tx, scope, organizationID) if err != nil { return fmt.Errorf("cannot load organization: %w", err) } config = &coredata.SAMLConfiguration{} + err = config.LoadByID(ctx, tx, scope, configID) if err != nil { return fmt.Errorf("cannot load saml configuration: %w", err) @@ -1938,7 +1948,6 @@ func (s OrganizationService) UpdateSAMLConfiguration( } return config, nil - } func (s OrganizationService) GetOrganization(ctx context.Context, organizationID gid.GID) (*coredata.Organization, error) { @@ -1990,7 +1999,6 @@ func (s OrganizationService) GetSCIMBridgeByID(ctx context.Context, bridgeID gid return nil }, ) - if err != nil { return nil, err } @@ -2021,7 +2029,6 @@ func (s OrganizationService) GetConnectorMetadataByID(ctx context.Context, conne return nil }, ) - if err != nil { return nil, err } @@ -2050,7 +2057,6 @@ func (s OrganizationService) GetSCIMBridgeByOrganizationID(ctx context.Context, return nil }, ) - if err != nil { return nil, err } @@ -2079,20 +2085,24 @@ func (s OrganizationService) CreateSCIMBridge( ctx, func(ctx context.Context, tx pg.Tx) error { organization := &coredata.Organization{} + err := organization.LoadByID(ctx, tx, scope, organizationID) if err != nil { if err == coredata.ErrResourceNotFound { return NewOrganizationNotFoundError(organizationID) } + return fmt.Errorf("cannot load organization: %w", err) } config := &coredata.SCIMConfiguration{} + err = config.LoadByID(ctx, tx, scope, scimConfigurationID) if err != nil { if err == coredata.ErrResourceNotFound { return scim.NewSCIMConfigurationNotFoundError(scimConfigurationID) } + return fmt.Errorf("cannot load SCIM configuration: %w", err) } @@ -2102,6 +2112,7 @@ func (s OrganizationService) CreateSCIMBridge( // Load and validate the connector (metadata only, no decryption needed) existingConnector := &coredata.Connector{} + err = existingConnector.LoadMetadataByID(ctx, tx, scope, connectorID) if err != nil { if err == coredata.ErrResourceNotFound { @@ -2118,6 +2129,7 @@ func (s OrganizationService) CreateSCIMBridge( // Map connector provider to bridge type var bridgeType coredata.SCIMBridgeType + switch existingConnector.Provider { case coredata.ConnectorProviderGoogleWorkspace: bridgeType = coredata.SCIMBridgeTypeGoogleWorkspace @@ -2146,7 +2158,6 @@ func (s OrganizationService) CreateSCIMBridge( return nil }, ) - if err != nil { return nil, err } @@ -2164,6 +2175,7 @@ func (s OrganizationService) DeleteSCIMBridge(ctx context.Context, organizationI ctx, func(ctx context.Context, tx pg.Tx) error { organization := &coredata.Organization{} + err := organization.LoadByID(ctx, tx, scope, organizationID) if err != nil { return fmt.Errorf("cannot load organization: %w", err) @@ -2184,7 +2196,6 @@ func (s OrganizationService) DeleteSCIMBridge(ctx context.Context, organizationI return nil }, ) - if err != nil { return err } @@ -2256,6 +2267,7 @@ func (s *OrganizationService) CountAuditLogEntries( ctx, func(ctx context.Context, conn pg.Querier) (err error) { entries := coredata.AuditLogEntries{} + count, err = entries.CountByOrganizationID(ctx, conn, scope, organizationID, filter) if err != nil { return fmt.Errorf("cannot count audit log entries: %w", err) diff --git a/pkg/iam/policy/evaluator.go b/pkg/iam/policy/evaluator.go index d678e5ae5..4e21ad0c4 100644 --- a/pkg/iam/policy/evaluator.go +++ b/pkg/iam/policy/evaluator.go @@ -135,12 +135,14 @@ func (e *Evaluator) statementMatches(stmt *Statement, req AuthorizationRequest) // Check resource match (if resources are specified) if len(stmt.Resources) > 0 { resourceMatched := false + for _, pattern := range stmt.Resources { if pattern.MatchesResource(req.Resource) { resourceMatched = true break } } + if !resourceMatched { return false } diff --git a/pkg/iam/policy/evaluator_test.go b/pkg/iam/policy/evaluator_test.go index ec86f8d89..de9b6cab6 100644 --- a/pkg/iam/policy/evaluator_test.go +++ b/pkg/iam/policy/evaluator_test.go @@ -390,12 +390,15 @@ func TestEvaluator_Evaluate_MatchedStatementAndPolicy(t *testing.T) { if result.MatchedStatement == nil { t.Fatal("Expected matched statement") } + if result.MatchedStatement.SID != "allow-get" { t.Errorf("Expected SID 'allow-get', got %q", result.MatchedStatement.SID) } + if result.MatchedPolicy == nil { t.Fatal("Expected matched policy") } + if result.MatchedPolicy.ID != "test-policy" { t.Errorf("Expected policy ID 'test-policy', got %q", result.MatchedPolicy.ID) } @@ -414,6 +417,7 @@ func TestEvaluator_Evaluate_MatchedStatementAndPolicy(t *testing.T) { if result.MatchedStatement == nil { t.Fatal("Expected matched statement") } + if result.MatchedStatement.SID != "deny-delete" { t.Errorf("Expected SID 'deny-delete', got %q", result.MatchedStatement.SID) } @@ -432,6 +436,7 @@ func TestEvaluator_Evaluate_MatchedStatementAndPolicy(t *testing.T) { if result.MatchedStatement != nil { t.Error("Expected no matched statement") } + if result.MatchedPolicy != nil { t.Error("Expected no matched policy") } diff --git a/pkg/iam/policy/matcher.go b/pkg/iam/policy/matcher.go index 20fecfc8f..b6937336b 100644 --- a/pkg/iam/policy/matcher.go +++ b/pkg/iam/policy/matcher.go @@ -56,6 +56,7 @@ func (m *ActionMatcher) Matches(pattern, target string) bool { if patternParts[1] == "*" { return patternParts[0] == targetParts[0] || patternParts[0] == "*" } + return false case 3: @@ -74,6 +75,7 @@ func (m *ActionMatcher) matchPart(pattern, target string) bool { if pattern == "*" { return true } + return pattern == target } @@ -84,5 +86,6 @@ func (m *ActionMatcher) MatchesAny(patterns []string, target string) bool { return true } } + return false } diff --git a/pkg/iam/policy/statement.go b/pkg/iam/policy/statement.go index 7ae97eff2..87ec5eeab 100644 --- a/pkg/iam/policy/statement.go +++ b/pkg/iam/policy/statement.go @@ -127,6 +127,7 @@ func (c Condition) Evaluate(ctx ConditionContext) bool { return true } } + return false case ConditionNotEquals: @@ -136,6 +137,7 @@ func (c Condition) Evaluate(ctx ConditionContext) bool { return false } } + return true case ConditionIn: @@ -153,6 +155,7 @@ func (c Condition) Evaluate(ctx ConditionContext) bool { return true } } + continue } @@ -160,6 +163,7 @@ func (c Condition) Evaluate(ctx ConditionContext) bool { return true } } + return false case ConditionNotIn: @@ -175,6 +179,7 @@ func (c Condition) Evaluate(ctx ConditionContext) bool { return false } } + continue } @@ -182,6 +187,7 @@ func (c Condition) Evaluate(ctx ConditionContext) bool { return false } } + return true default: @@ -196,12 +202,14 @@ func resolveKey(key string, ctx ConditionContext) (string, bool) { if len(key) > 10 && key[:10] == "principal." { attrKey := key[10:] val, ok := ctx.Principal[attrKey] + return val, ok } if len(key) > 9 && key[:9] == "resource." { attrKey := key[9:] val, ok := ctx.Resource[attrKey] + return val, ok } diff --git a/pkg/iam/policy/statement_test.go b/pkg/iam/policy/statement_test.go index 9a271c03a..cf65e990d 100644 --- a/pkg/iam/policy/statement_test.go +++ b/pkg/iam/policy/statement_test.go @@ -284,9 +284,11 @@ func TestConditionHelpers(t *testing.T) { if c.Operator != ConditionEquals { t.Errorf("Expected ConditionEquals, got %v", c.Operator) } + if c.Key != "principal.id" { t.Errorf("Expected principal.id, got %v", c.Key) } + if len(c.Values) != 2 { t.Errorf("Expected 2 values, got %d", len(c.Values)) } diff --git a/pkg/iam/policy_set.go b/pkg/iam/policy_set.go index f3f98ab69..054f5e6c1 100644 --- a/pkg/iam/policy_set.go +++ b/pkg/iam/policy_set.go @@ -51,7 +51,9 @@ func (ps *PolicySet) Merge(other *PolicySet) *PolicySet { for role, policies := range other.RolePolicies { ps.RolePolicies[role] = append(ps.RolePolicies[role], policies...) } + ps.IdentityScopedPolicies = append(ps.IdentityScopedPolicies, other.IdentityScopedPolicies...) + return ps } diff --git a/pkg/iam/saml/attributes.go b/pkg/iam/saml/attributes.go index 7b3fc1c86..052c027ed 100644 --- a/pkg/iam/saml/attributes.go +++ b/pkg/iam/saml/attributes.go @@ -39,6 +39,7 @@ func extractUserAttributes(assertion *saml.Assertion, config *coredata.SAMLConfi fullname = email.String() role = nil + return email, fullname, role, nil } diff --git a/pkg/iam/saml/service.go b/pkg/iam/saml/service.go index 8e2cd3b70..096324e59 100644 --- a/pkg/iam/saml/service.go +++ b/pkg/iam/saml/service.go @@ -72,11 +72,13 @@ func NewService( func (s *Service) Run(ctx context.Context) error { wg := sync.WaitGroup{} + ctx, cancel := context.WithCancelCause(ctx) defer cancel(context.Canceled) gcCtx, stopGC := context.WithCancel(context.WithoutCancel(ctx)) gc := NewGarbageCollector(s.pg, s.logger) + wg.Go(func() { if err := gc.Run(gcCtx); err != nil { cancel(fmt.Errorf("saml garbage collector crashed: %w", err)) @@ -112,6 +114,7 @@ func (s *Service) InitiateLogin( ctx, func(ctx context.Context, tx pg.Tx) error { config := &coredata.SAMLConfiguration{} + err := config.LoadByID(ctx, tx, coredata.NewNoScope(), configID) if err != nil { if err == coredata.ErrResourceNotFound { @@ -349,10 +352,12 @@ func (s *Service) HandleAssertion( if profile.Source != coredata.ProfileSourceSCIM { profile.FullName = fullname + profile.UpdatedAt = now if profile.Source == coredata.ProfileSourceManual { profile.Source = coredata.ProfileSourceSAML } + err = profile.Update(ctx, tx, scope) if err != nil { return fmt.Errorf("cannot update profile: %w", err) @@ -371,6 +376,7 @@ func (s *Service) HandleAssertion( // Expire pending invitations for user (in case source switched to SAML) invitations := &coredata.Invitations{} + onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending}) if err := invitations.ExpireByUserID( ctx, @@ -385,7 +391,6 @@ func (s *Service) HandleAssertion( return nil }, ) - if err != nil { return nil, nil, err } @@ -436,6 +441,7 @@ func (s *Service) validateAssertion(assertion *saml.Assertion, config *coredata. expectedAudience := baseurl.MustParse(s.baseURL).WithPath("/api/connect/v1/saml/2.0/metadata").MustString() audienceValid := false + for _, restriction := range assertion.Conditions.AudienceRestrictions { if restriction.Audience.Value == expectedAudience { audienceValid = true diff --git a/pkg/iam/saml_domain_verifier.go b/pkg/iam/saml_domain_verifier.go index af1400a00..b6fcde464 100644 --- a/pkg/iam/saml_domain_verifier.go +++ b/pkg/iam/saml_domain_verifier.go @@ -204,6 +204,7 @@ func (v *SAMLDomainVerifier) checkDNSTXTRecord(emailDomain string, expectedValue msg.Question = []dns.RR{&dns.TXT{Hdr: dns.Header{Name: fqdn, Class: dns.ClassINET}}} client := dns.NewClient() + resp, _, err := client.Exchange(context.Background(), msg, "udp", v.resolverAddr) if err != nil { return fmt.Errorf("cannot query TXT record for %q: %w", emailDomain, err) diff --git a/pkg/iam/scim/bridge/bridge.go b/pkg/iam/scim/bridge/bridge.go index 2eebd6887..a3da4f5ab 100644 --- a/pkg/iam/scim/bridge/bridge.go +++ b/pkg/iam/scim/bridge/bridge.go @@ -67,6 +67,7 @@ func (s *Bridge) Run(ctx context.Context) (created, updated, deleted, deactivate } scimUsersByEmail := make(map[string]*scimclient.User) + for i := range scimUsers { email := strings.ToLower(scimUsers[i].UserName) scimUsersByEmail[email] = &scimUsers[i] @@ -86,6 +87,7 @@ func (s *Bridge) Run(ctx context.Context) (created, updated, deleted, deactivate errs = append(errs, fmt.Errorf("cannot create user %q: %w", pu.ExternalID, err)) continue } + created++ } else { needsUpdate := existingSCIM.Active != pu.Active || @@ -107,6 +109,7 @@ func (s *Bridge) Run(ctx context.Context) (created, updated, deleted, deactivate errs = append(errs, fmt.Errorf("cannot update user %q: %w", pu.ExternalID, err)) continue } + updated++ } else { skipped++ @@ -124,7 +127,9 @@ func (s *Bridge) Run(ctx context.Context) (created, updated, deleted, deactivate errs = append(errs, fmt.Errorf("cannot delete user %q: %w", scimUser.ExternalID, err)) continue } + deleted++ + continue } @@ -136,6 +141,7 @@ func (s *Bridge) Run(ctx context.Context) (created, updated, deleted, deactivate errs = append(errs, fmt.Errorf("cannot deactivate user %q: %w", scimUser.ExternalID, err)) continue } + deactivated++ } @@ -148,5 +154,6 @@ func (s *Bridge) isExcluded(email string) bool { return true } } + return false } diff --git a/pkg/iam/scim/bridge/client/client.go b/pkg/iam/scim/bridge/client/client.go index 4d58bd388..e030fe782 100644 --- a/pkg/iam/scim/bridge/client/client.go +++ b/pkg/iam/scim/bridge/client/client.go @@ -72,6 +72,7 @@ func NewClient(httpClient *http.Client, endpoint, token string) *Client { func (c *Client) ListUsers(ctx context.Context) (Users, error) { var allUsers Users + startIndex := 1 count := 100 @@ -107,6 +108,7 @@ func (c *Client) listUsersPage(ctx context.Context, startIndex, count int) (User if err != nil { return nil, 0, fmt.Errorf("cannot fetch users: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { @@ -131,6 +133,7 @@ func (c *Client) CreateUser(ctx context.Context, user *User) error { } reqURL := fmt.Sprintf("%s/Users", c.endpoint) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, bytes.NewReader(body)) if err != nil { return fmt.Errorf("cannot create request: %w", err) @@ -143,6 +146,7 @@ func (c *Client) CreateUser(ctx context.Context, user *User) error { if err != nil { return fmt.Errorf("cannot create user: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { @@ -162,6 +166,7 @@ func (c *Client) UpdateUser(ctx context.Context, userID string, user *User) erro } reqURL := fmt.Sprintf("%s/Users/%s", c.endpoint, url.PathEscape(userID)) + req, err := http.NewRequestWithContext(ctx, http.MethodPut, reqURL, bytes.NewReader(body)) if err != nil { return fmt.Errorf("cannot create request: %w", err) @@ -174,6 +179,7 @@ func (c *Client) UpdateUser(ctx context.Context, userID string, user *User) erro if err != nil { return fmt.Errorf("cannot update user: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { @@ -244,6 +250,7 @@ func (c *Client) DeactivateUser(ctx context.Context, userID string) error { } reqURL := fmt.Sprintf("%s/Users/%s", c.endpoint, url.PathEscape(userID)) + req, err := http.NewRequestWithContext(ctx, http.MethodPatch, reqURL, bytes.NewReader(body)) if err != nil { return fmt.Errorf("cannot create request: %w", err) @@ -256,6 +263,7 @@ func (c *Client) DeactivateUser(ctx context.Context, userID string) error { if err != nil { return fmt.Errorf("cannot deactivate user: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { @@ -268,6 +276,7 @@ func (c *Client) DeactivateUser(ctx context.Context, userID string) error { func (c *Client) DeleteUser(ctx context.Context, userID string) error { reqURL := fmt.Sprintf("%s/Users/%s", c.endpoint, url.PathEscape(userID)) + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, reqURL, nil) if err != nil { return fmt.Errorf("cannot create request: %w", err) @@ -279,6 +288,7 @@ func (c *Client) DeleteUser(ctx context.Context, userID string) error { if err != nil { return fmt.Errorf("cannot delete user: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusNotFound { diff --git a/pkg/iam/scim/bridge/client/client_test.go b/pkg/iam/scim/bridge/client/client_test.go index c16d46be2..a5414cd15 100644 --- a/pkg/iam/scim/bridge/client/client_test.go +++ b/pkg/iam/scim/bridge/client/client_test.go @@ -57,6 +57,7 @@ func TestUser_UnmarshalJSON(t *testing.T) { }`) var user scimclient.User + err := json.Unmarshal(data, &user) require.NoError(t, err) @@ -96,6 +97,7 @@ func TestUser_UnmarshalJSON(t *testing.T) { }`) var user scimclient.User + err := json.Unmarshal(data, &user) require.NoError(t, err) @@ -123,6 +125,7 @@ func TestUser_UnmarshalJSON(t *testing.T) { }`) var user scimclient.User + err := json.Unmarshal(data, &user) require.NoError(t, err) diff --git a/pkg/iam/scim/bridge/provider/googleworkspace/provider.go b/pkg/iam/scim/bridge/provider/googleworkspace/provider.go index 81b4acee5..e5ec58cb2 100644 --- a/pkg/iam/scim/bridge/provider/googleworkspace/provider.go +++ b/pkg/iam/scim/bridge/provider/googleworkspace/provider.go @@ -55,6 +55,7 @@ func (p *Provider) isExcluded(email string) bool { return true } } + return false } @@ -65,6 +66,7 @@ func (p *Provider) ListUsers(ctx context.Context) (scimclient.Users, error) { } var allUsers scimclient.Users + pageToken := "" for { @@ -128,12 +130,16 @@ func (p *Provider) extractOrganizationFields(raw any, user *scimclient.User) { return } - var primary *admin.UserOrganization - var first *admin.UserOrganization + var ( + primary *admin.UserOrganization + first *admin.UserOrganization + ) + for i := range orgs { if first == nil { first = &orgs[i] } + if orgs[i].Primary { primary = &orgs[i] break @@ -144,6 +150,7 @@ func (p *Provider) extractOrganizationFields(raw any, user *scimclient.User) { if org == nil { org = first } + if org == nil { return } diff --git a/pkg/iam/scim/bridge/provider/microsoft365/provider.go b/pkg/iam/scim/bridge/provider/microsoft365/provider.go index 6e26bd73f..6b37e92d7 100644 --- a/pkg/iam/scim/bridge/provider/microsoft365/provider.go +++ b/pkg/iam/scim/bridge/provider/microsoft365/provider.go @@ -79,6 +79,7 @@ func (p *Provider) isExcluded(email string) bool { return true } } + return false } @@ -115,6 +116,7 @@ func (p *Provider) ListUsers(ctx context.Context) (scimclient.Users, error) { } var allUsers scimclient.Users + for range graphMaxPages { users, next, err := p.fetchPage(ctx, endpoint) if err != nil { @@ -126,9 +128,11 @@ func (p *Provider) ListUsers(ctx context.Context) (scimclient.Users, error) { if email == "" { email = u.UserPrincipalName } + if email == "" { continue } + if p.isExcluded(email) { continue } @@ -151,6 +155,7 @@ func (p *Provider) ListUsers(ctx context.Context) (scimclient.Users, error) { if next == "" { return allUsers, nil } + endpoint = next } @@ -177,12 +182,14 @@ func (p *Provider) fetchPage(ctx context.Context, endpoint string) ([]graphUser, if err != nil { return nil, "", fmt.Errorf("cannot create graph users request: %w", err) } + req.Header.Set("Accept", "application/json") resp, err := p.httpClient.Do(req) if err != nil { return nil, "", fmt.Errorf("cannot list graph users: %w", err) } + defer func() { _ = resp.Body.Close() }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { diff --git a/pkg/iam/scim/bridge_runner.go b/pkg/iam/scim/bridge_runner.go index 1c3e44d85..6ae7d385f 100644 --- a/pkg/iam/scim/bridge_runner.go +++ b/pkg/iam/scim/bridge_runner.go @@ -77,18 +77,23 @@ func NewBridgeRunner( if cfg.Interval == 0 { cfg.Interval = 15 * time.Minute } + if cfg.PollInterval == 0 { cfg.PollInterval = 30 * time.Second } + if cfg.SyncTimeout == 0 { cfg.SyncTimeout = 5 * time.Minute } + if cfg.MaxBackoff == 0 { cfg.MaxBackoff = DefaultMaxBackoff } + if cfg.MaxConsecutiveFailures == 0 { cfg.MaxConsecutiveFailures = DefaultMaxConsecutiveFailures } + if cfg.StaleSyncThreshold == 0 { cfg.StaleSyncThreshold = DefaultStaleSyncThreshold } @@ -161,6 +166,7 @@ func (r *BridgeRunner) processBridge(ctx context.Context) error { if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "sync failed") + return r.transitionToFailed(ctx, bridge, scope, err, duration, logger) } diff --git a/pkg/iam/scim/bridge_runner_backoff.go b/pkg/iam/scim/bridge_runner_backoff.go index 4c8ec56e4..948f298d6 100644 --- a/pkg/iam/scim/bridge_runner_backoff.go +++ b/pkg/iam/scim/bridge_runner_backoff.go @@ -37,6 +37,7 @@ func (r *BridgeRunner) calculateBackoff(consecutiveFailures int) time.Duration { // Cap the shift exponent to prevent integer overflow from the shift itself. // Bit 63 is the sign bit, so shifting by 63+ produces negative or zero values. const maxShift = 62 + shiftAmount := min(consecutiveFailures, maxShift) backoff := r.cfg.Interval * time.Duration(1< 0 { reqOpts = append(reqOpts, option.WithRequestTimeout(cfg.requestTimeout)) } + if cfg.maxRetries != nil { reqOpts = append(reqOpts, option.WithMaxRetries(*cfg.maxRetries)) } client := anthropic.NewClient(reqOpts...) + return &Provider{client: &client} } @@ -111,6 +115,7 @@ func (p *Provider) ChatCompletionStream(ctx context.Context, req *llm.ChatComple } stream := p.client.Messages.NewStreaming(ctx, params) + return &anthropicStream{stream: stream}, nil } @@ -134,37 +139,46 @@ func buildParams(req *llm.ChatCompletionRequest) (anthropic.MessageNewParams, er for i, s := range system { blocks[i] = anthropic.TextBlockParam{Text: s} } + params.System = blocks } if req.Temperature != nil { params.Temperature = param.NewOpt(*req.Temperature) } + if req.TopP != nil { params.TopP = param.NewOpt(*req.TopP) } + if len(req.StopSequences) > 0 { params.StopSequences = req.StopSequences } + if len(req.Tools) > 0 { params.Tools = buildTools(req.Tools) } + if req.ToolChoice != nil { params.ToolChoice = buildToolChoice(req.ToolChoice) } + if req.Thinking != nil && req.Thinking.Enabled { params.Thinking = anthropic.ThinkingConfigParamOfEnabled(int64(req.Thinking.BudgetTokens)) } + if req.ResponseFormat != nil { switch req.ResponseFormat.Type { case llm.ResponseFormatJSONSchema: if req.ResponseFormat.JSONSchema == nil { return anthropic.MessageNewParams{}, fmt.Errorf("cannot apply JSON schema output format: schema is nil") } + var schema map[string]any if err := json.Unmarshal(req.ResponseFormat.JSONSchema.Schema, &schema); err != nil { return anthropic.MessageNewParams{}, fmt.Errorf("cannot unmarshal JSON schema for output format: %w", err) } + params.OutputConfig = anthropic.OutputConfigParam{ Format: anthropic.JSONOutputFormatParam{Schema: schema}, } @@ -186,6 +200,7 @@ func extractSystem(messages []llm.Message) (system []string, rest []llm.Message) rest = append(rest, msg) } } + return } @@ -213,9 +228,11 @@ func buildMessages(messages []llm.Message) []anthropic.MessageParam { blocks = append(blocks, buildFilePart(p)) } } + out = append(out, anthropic.NewUserMessage(blocks...)) case llm.RoleAssistant: var blocks []anthropic.ContentBlockParamUnion + for _, p := range msg.Parts { switch part := p.(type) { case llm.ThinkingPart: @@ -226,13 +243,16 @@ func buildMessages(messages []llm.Message) []anthropic.MessageParam { } } } + for _, tc := range msg.ToolCalls { var input any if err := json.Unmarshal([]byte(tc.Function.Arguments), &input); err != nil || input == nil { input = map[string]any{} } + blocks = append(blocks, anthropic.NewToolUseBlock(tc.ID, input, tc.Function.Name)) } + out = append(out, anthropic.NewAssistantMessage(blocks...)) case llm.RoleTool: out = append( @@ -264,6 +284,7 @@ func buildTools(tools []llm.Tool) []anthropic.ToolUnionParam { if err := json.Unmarshal(t.Parameters, &schema); err == nil { props := schema["properties"] required, _ := schema["required"].([]any) + reqStrings := make([]string, 0, len(required)) for _, r := range required { if s, ok := r.(string); ok { @@ -272,6 +293,7 @@ func buildTools(tools []llm.Tool) []anthropic.ToolUnionParam { } extra := make(map[string]any) + for k, v := range schema { switch k { case "type", "properties", "required": @@ -287,12 +309,14 @@ func buildTools(tools []llm.Tool) []anthropic.ToolUnionParam { if len(extra) > 0 { inputSchema.ExtraFields = extra } + tool.InputSchema = inputSchema } } out[i] = anthropic.ToolUnionParam{OfTool: &tool} } + return out } @@ -392,13 +416,16 @@ func parseRetryAfter(resp *http.Response) time.Duration { if resp == nil { return 0 } + h := resp.Header.Get("Retry-After") if h == "" { return 0 } + if secs, err := strconv.Atoi(h); err == nil { return time.Duration(secs) * time.Second } + return 0 } @@ -415,12 +442,14 @@ type anthropicStream struct { func (s *anthropicStream) Next() bool { for s.stream.Next() { event := s.stream.Current() + mapped, ok := s.mapStreamEvent(&event) if ok { s.current = mapped return true } } + return false } @@ -433,6 +462,7 @@ func (s *anthropicStream) Err() error { if err != nil { return mapError(err) } + return nil } @@ -448,6 +478,7 @@ func (s *anthropicStream) mapStreamEvent(event *anthropic.MessageStreamEventUnio case "tool_use": s.inToolUse = true tu := cb.AsToolUse() + return llm.ChatCompletionStreamEvent{ Delta: llm.MessageDelta{ ToolCalls: []llm.ToolCallDelta{{ @@ -460,6 +491,7 @@ func (s *anthropicStream) mapStreamEvent(event *anthropic.MessageStreamEventUnio case "thinking": return llm.ChatCompletionStreamEvent{}, false } + return llm.ChatCompletionStreamEvent{}, false case "content_block_delta": @@ -475,6 +507,7 @@ func (s *anthropicStream) mapStreamEvent(event *anthropic.MessageStreamEventUnio }, true case "signature_delta": s.thinkingSignature = delta.Signature + return llm.ChatCompletionStreamEvent{ Delta: llm.MessageDelta{ThinkingSignature: delta.Signature}, }, true @@ -488,6 +521,7 @@ func (s *anthropicStream) mapStreamEvent(event *anthropic.MessageStreamEventUnio }, }, true } + return llm.ChatCompletionStreamEvent{}, false case "content_block_stop": @@ -495,10 +529,12 @@ func (s *anthropicStream) mapStreamEvent(event *anthropic.MessageStreamEventUnio s.toolCallIndex++ s.inToolUse = false } + return llm.ChatCompletionStreamEvent{}, false case "message_delta": fr := mapStopReason(anthropic.StopReason(event.Delta.StopReason)) + evt := llm.ChatCompletionStreamEvent{ FinishReason: &fr, } @@ -508,6 +544,7 @@ func (s *anthropicStream) mapStreamEvent(event *anthropic.MessageStreamEventUnio OutputTokens: int(event.Usage.OutputTokens), } } + return evt, true case "message_start": @@ -520,6 +557,7 @@ func (s *anthropicStream) mapStreamEvent(event *anthropic.MessageStreamEventUnio OutputTokens: int(event.Message.Usage.OutputTokens), } } + return evt, true default: @@ -540,6 +578,7 @@ func buildFilePart(p llm.FilePart) anthropic.ContentBlockParamUnion { if err != nil { return anthropic.NewTextBlock(fmt.Sprintf("[file: %s, type: %s, error decoding content]", p.Filename, p.MimeType)) } + return anthropic.NewDocumentBlock(anthropic.PlainTextSourceParam{ Data: string(decoded), }) diff --git a/pkg/llm/bedrock/provider.go b/pkg/llm/bedrock/provider.go index 665b00ce0..8f3dd4767 100644 --- a/pkg/llm/bedrock/provider.go +++ b/pkg/llm/bedrock/provider.go @@ -48,6 +48,7 @@ func NewProvider(cfg aws.Config, opts ...Option) *Provider { } client := bedrockruntime.NewFromConfig(cfg, fns...) + return &Provider{client: client} } @@ -112,14 +113,17 @@ func buildInferenceConfig(req *llm.ChatCompletionRequest) *types.InferenceConfig v := int32(*req.MaxTokens) cfg.MaxTokens = &v } + if req.Temperature != nil { v := float32(*req.Temperature) cfg.Temperature = &v } + if req.TopP != nil { v := float32(*req.TopP) cfg.TopP = &v } + if len(req.StopSequences) > 0 { cfg.StopSequences = req.StopSequences } @@ -129,6 +133,7 @@ func buildInferenceConfig(req *llm.ChatCompletionRequest) *types.InferenceConfig func buildSystem(messages []llm.Message) []types.SystemContentBlock { var system []types.SystemContentBlock + for _, msg := range messages { if msg.Role == llm.RoleSystem { system = append( @@ -139,6 +144,7 @@ func buildSystem(messages []llm.Message) []types.SystemContentBlock { ) } } + return system } @@ -151,11 +157,13 @@ func buildMessages(messages []llm.Message) []types.Message { continue case llm.RoleUser: var content []types.ContentBlock + for _, p := range msg.Parts { if tp, ok := p.(llm.TextPart); ok { content = append(content, &types.ContentBlockMemberText{Value: tp.Text}) } } + out = append( out, types.Message{ Role: types.ConversationRoleUser, @@ -168,8 +176,10 @@ func buildMessages(messages []llm.Message) []types.Message { if text := msg.Text(); text != "" { content = append(content, &types.ContentBlockMemberText{Value: text}) } + for _, tc := range msg.ToolCalls { var input any + _ = json.Unmarshal([]byte(tc.Function.Arguments), &input) content = append( content, @@ -182,6 +192,7 @@ func buildMessages(messages []llm.Message) []types.Message { }, ) } + out = append( out, types.Message{ Role: types.ConversationRoleAssistant, @@ -220,13 +231,16 @@ func buildToolConfig(req *llm.ChatCompletionRequest) *types.ToolConfiguration { } if t.Parameters != nil { var schema any + _ = json.Unmarshal(t.Parameters, &schema) spec.InputSchema = &types.ToolInputSchemaMemberJson{ Value: document.NewLazyDocument(schema), } } + tools[i] = &types.ToolMemberToolSpec{Value: spec} } + config.Tools = tools if req.ToolChoice != nil { @@ -283,6 +297,7 @@ func mapResponse(output *bedrockruntime.ConverseOutput, model string) *llm.ChatC if b.Value.Input != nil { _ = b.Value.Input.UnmarshalSmithyDocument(&args) } + argsJSON, _ := json.Marshal(args) resp.Message.ToolCalls = append(resp.Message.ToolCalls, llm.ToolCall{ ID: aws.ToString(b.Value.ToolUseId), @@ -321,6 +336,7 @@ func mapError(err error) error { if strings.Contains(msg, "throttling") || strings.Contains(msg, "ThrottlingException") { return &llm.ErrRateLimit{Err: err} } + return err } @@ -334,6 +350,7 @@ func mapError(err error) error { if strings.Contains(msg, "context") || strings.Contains(msg, "token") { return &llm.ErrContextLength{Err: err} } + return err default: return err @@ -368,7 +385,9 @@ func (s *bedrockStream) Next() bool { mapped.Model = s.model s.modelSent = true } + s.current = mapped + return true } } @@ -376,6 +395,7 @@ func (s *bedrockStream) Next() bool { if err := s.eventStream.Err(); err != nil { s.err = mapError(err) } + return false } @@ -396,6 +416,7 @@ func (s *bedrockStream) mapEvent(event types.ConverseStreamOutput) (llm.ChatComp case *types.ConverseStreamOutputMemberContentBlockStart: if start, ok := e.Value.Start.(*types.ContentBlockStartMemberToolUse); ok { s.inToolUse = true + return llm.ChatCompletionStreamEvent{ Delta: llm.MessageDelta{ ToolCalls: []llm.ToolCallDelta{{ @@ -406,7 +427,9 @@ func (s *bedrockStream) mapEvent(event types.ConverseStreamOutput) (llm.ChatComp }, }, true } + s.inToolUse = false + return llm.ChatCompletionStreamEvent{}, false case *types.ConverseStreamOutputMemberContentBlockDelta: @@ -425,6 +448,7 @@ func (s *bedrockStream) mapEvent(event types.ConverseStreamOutput) (llm.ChatComp }, }, true } + return llm.ChatCompletionStreamEvent{}, false case *types.ConverseStreamOutputMemberContentBlockStop: @@ -432,10 +456,12 @@ func (s *bedrockStream) mapEvent(event types.ConverseStreamOutput) (llm.ChatComp s.toolIndex++ s.inToolUse = false } + return llm.ChatCompletionStreamEvent{}, false case *types.ConverseStreamOutputMemberMessageStop: fr := mapStopReason(e.Value.StopReason) + return llm.ChatCompletionStreamEvent{ FinishReason: &fr, }, true @@ -449,6 +475,7 @@ func (s *bedrockStream) mapEvent(event types.ConverseStreamOutput) (llm.ChatComp }, }, true } + return llm.ChatCompletionStreamEvent{}, false default: diff --git a/pkg/llm/chat.go b/pkg/llm/chat.go index 153737e7c..56e2af163 100644 --- a/pkg/llm/chat.go +++ b/pkg/llm/chat.go @@ -211,6 +211,7 @@ func (a *StreamAccumulator) Response() *ChatCompletionResponse { Signature: a.thinkingSignature, }) } + parts = append(parts, TextPart{Text: a.content.String()}) return &ChatCompletionResponse{ @@ -232,6 +233,7 @@ func (a *StreamAccumulator) accumulate(event ChatCompletionStreamEvent) { a.content.WriteString(event.Delta.Content) a.thinking.WriteString(event.Delta.Thinking) + if event.Delta.ThinkingSignature != "" { a.thinkingSignature = event.Delta.ThinkingSignature } @@ -246,15 +248,18 @@ func (a *StreamAccumulator) accumulate(event ChatCompletionStreamEvent) { if tcd.ID != "" { tc.ID = tcd.ID } + if tcd.Name != "" { tc.Function.Name = tcd.Name } + tc.Function.Arguments += tcd.Arguments } if event.Usage != nil { a.usage = *event.Usage } + if event.FinishReason != nil { a.finishReason = *event.FinishReason } diff --git a/pkg/llm/errors.go b/pkg/llm/errors.go index fb74b4a0b..ff931b39b 100644 --- a/pkg/llm/errors.go +++ b/pkg/llm/errors.go @@ -50,6 +50,7 @@ func (e *ErrRateLimit) Error() string { if e.RetryAfter > 0 { return fmt.Sprintf("rate limited (retry after %s): %v", e.RetryAfter, e.Err) } + return fmt.Sprintf("rate limited: %v", e.Err) } @@ -59,6 +60,7 @@ func (e *ErrContextLength) Error() string { if e.MaxTokens > 0 { return fmt.Sprintf("context length exceeded (max %d tokens): %v", e.MaxTokens, e.Err) } + return fmt.Sprintf("context length exceeded: %v", e.Err) } diff --git a/pkg/llm/llm.go b/pkg/llm/llm.go index 005b8042d..f53c99d93 100644 --- a/pkg/llm/llm.go +++ b/pkg/llm/llm.go @@ -95,6 +95,7 @@ func (c *Client) ChatCompletion(ctx context.Context, req *ChatCompletionRequest) log.Error(err), ) endChatSpan(span, nil, err) + return nil, err } @@ -109,6 +110,7 @@ func (c *Client) ChatCompletion(ctx context.Context, req *ChatCompletionRequest) ) endChatSpan(span, resp, nil) + return resp, nil } @@ -132,6 +134,7 @@ func (c *Client) ChatCompletionStream(ctx context.Context, req *ChatCompletionRe log.Error(err), ) endChatSpan(span, nil, err) + return nil, err } diff --git a/pkg/llm/llm_test.go b/pkg/llm/llm_test.go index ac46dfb26..e2861b051 100644 --- a/pkg/llm/llm_test.go +++ b/pkg/llm/llm_test.go @@ -60,8 +60,10 @@ func (s *mockStream) Next() bool { if s.idx >= len(s.events) { return false } + s.current = s.events[s.idx] s.idx++ + return true } @@ -78,6 +80,7 @@ func newTestClient(provider llm.Provider) (*llm.Client, *tracetest.SpanRecorder) "test", llm.WithTracerProvider(tp), ) + return client, recorder } @@ -86,10 +89,12 @@ func spanAttrMap(recorder *tracetest.SpanRecorder) map[string]any { if len(spans) == 0 { return nil } + m := make(map[string]any) for _, a := range spans[0].Attributes() { m[string(a.Key)] = a.Value.AsInterface() } + return m } @@ -179,6 +184,7 @@ func TestErrors(t *testing.T) { t.Run("with retry after", func(t *testing.T) { t.Parallel() + e := &llm.ErrRateLimit{RetryAfter: 30 * time.Second, Err: inner} assert.Contains(t, e.Error(), "retry after 30s") assert.Contains(t, e.Error(), "upstream") @@ -187,6 +193,7 @@ func TestErrors(t *testing.T) { t.Run("without retry after", func(t *testing.T) { t.Parallel() + e := &llm.ErrRateLimit{Err: inner} assert.Contains(t, e.Error(), "rate limited") assert.NotContains(t, e.Error(), "retry after") @@ -195,7 +202,9 @@ func TestErrors(t *testing.T) { t.Run("errors.As", func(t *testing.T) { t.Parallel() + var target *llm.ErrRateLimit + e := &llm.ErrRateLimit{RetryAfter: 5 * time.Second, Err: inner} require.ErrorAs(t, e, &target) assert.Equal(t, 5*time.Second, target.RetryAfter) @@ -207,6 +216,7 @@ func TestErrors(t *testing.T) { t.Run("with max tokens", func(t *testing.T) { t.Parallel() + e := &llm.ErrContextLength{MaxTokens: 4096, Err: inner} assert.Contains(t, e.Error(), "4096") assert.ErrorIs(t, e, inner) @@ -214,6 +224,7 @@ func TestErrors(t *testing.T) { t.Run("without max tokens", func(t *testing.T) { t.Parallel() + e := &llm.ErrContextLength{Err: inner} assert.Contains(t, e.Error(), "context length exceeded") assert.NotContains(t, e.Error(), "max") @@ -223,6 +234,7 @@ func TestErrors(t *testing.T) { t.Run("ErrContentFilter", func(t *testing.T) { t.Parallel() + e := &llm.ErrContentFilter{Err: inner} assert.Contains(t, e.Error(), "content filtered") assert.ErrorIs(t, e, inner) @@ -230,6 +242,7 @@ func TestErrors(t *testing.T) { t.Run("ErrAuthentication", func(t *testing.T) { t.Parallel() + e := &llm.ErrAuthentication{Err: inner} assert.Contains(t, e.Error(), "authentication failed") assert.ErrorIs(t, e, inner) @@ -374,12 +387,14 @@ func TestChatCompletionStream(t *testing.T) { require.NoError(t, err) var collected []string + for stream.Next() { e := stream.Event() if e.Delta.Content != "" { collected = append(collected, e.Delta.Content) } } + require.NoError(t, stream.Err()) require.NoError(t, stream.Close()) @@ -428,6 +443,7 @@ func TestChatCompletionStream(t *testing.T) { for stream.Next() { } + assert.ErrorContains(t, stream.Err(), "connection reset") _ = stream.Close() @@ -511,6 +527,7 @@ func TestChatCompletionStream(t *testing.T) { for stream.Next() { } + require.NoError(t, stream.Err()) require.NoError(t, stream.Close()) @@ -521,6 +538,7 @@ func TestChatCompletionStream(t *testing.T) { for _, a := range spans[0].Attributes() { attrs[string(a.Key)] = a.Value.AsInterface() } + assert.Equal(t, int64(100), attrs["gen_ai.usage.input_tokens"]) assert.Equal(t, int64(50), attrs["gen_ai.usage.output_tokens"]) assert.Equal(t, []string{"length"}, attrs["gen_ai.response.finish_reasons"]) @@ -564,6 +582,7 @@ func TestStreamAccumulator(t *testing.T) { acc := llm.NewStreamAccumulator(&mockStream{events: events}) for acc.Next() { } + require.NoError(t, acc.Err()) resp := acc.Response() @@ -614,6 +633,7 @@ func TestStreamAccumulator(t *testing.T) { acc := llm.NewStreamAccumulator(&mockStream{events: events}) for acc.Next() { } + require.NoError(t, acc.Err()) resp := acc.Response() @@ -642,6 +662,7 @@ func TestStreamAccumulator(t *testing.T) { acc := llm.NewStreamAccumulator(&mockStream{events: events}) for acc.Next() { } + require.NoError(t, acc.Err()) resp := acc.Response() @@ -660,7 +681,9 @@ func TestStreamAccumulator(t *testing.T) { } acc := llm.NewStreamAccumulator(&mockStream{events: events}) + var seen []string + for acc.Next() { e := acc.Event() if e.Delta.Content != "" { diff --git a/pkg/llm/message.go b/pkg/llm/message.go index 54eb6e236..4bc35a218 100644 --- a/pkg/llm/message.go +++ b/pkg/llm/message.go @@ -165,20 +165,24 @@ func (m *Message) UnmarshalJSON(data []byte) error { func (m Message) Text() string { var s strings.Builder + for _, p := range m.Parts { if tp, ok := p.(TextPart); ok { s.WriteString(tp.Text) } } + return s.String() } func (m Message) Thinking() string { var s strings.Builder + for _, p := range m.Parts { if tp, ok := p.(ThinkingPart); ok { s.WriteString(tp.Text) } } + return s.String() } diff --git a/pkg/llm/message_test.go b/pkg/llm/message_test.go index 36ea11835..326286702 100644 --- a/pkg/llm/message_test.go +++ b/pkg/llm/message_test.go @@ -95,6 +95,7 @@ func TestMessageJSONRoundTrip(t *testing.T) { require.NoError(t, err) var got Message + err = json.Unmarshal(data, &got) require.NoError(t, err) assert.Equal(t, tt.msg, got) diff --git a/pkg/llm/openai/provider.go b/pkg/llm/openai/provider.go index 99f8b73c1..37a0e3cb4 100644 --- a/pkg/llm/openai/provider.go +++ b/pkg/llm/openai/provider.go @@ -87,23 +87,29 @@ func NewProvider(apiKey string, opts ...Option) *Provider { if cfg.httpClient != nil { reqOpts = append(reqOpts, option.WithHTTPClient(cfg.httpClient)) } + if cfg.baseURL != "" { reqOpts = append(reqOpts, option.WithBaseURL(cfg.baseURL)) } + if cfg.organization != "" { reqOpts = append(reqOpts, option.WithOrganization(cfg.organization)) } + if cfg.project != "" { reqOpts = append(reqOpts, option.WithProject(cfg.project)) } + if cfg.requestTimeout > 0 { reqOpts = append(reqOpts, option.WithRequestTimeout(cfg.requestTimeout)) } + if cfg.maxRetries != nil { reqOpts = append(reqOpts, option.WithMaxRetries(*cfg.maxRetries)) } client := openai.NewClient(reqOpts...) + return &Provider{client: &client} } @@ -125,6 +131,7 @@ func (p *Provider) ChatCompletionStream(ctx context.Context, req *llm.ChatComple } stream := p.client.Chat.Completions.NewStreaming(ctx, params) + return &openaiStream{stream: stream}, nil } @@ -137,35 +144,45 @@ func buildParams(req *llm.ChatCompletionRequest) openai.ChatCompletionNewParams if req.MaxTokens != nil { params.MaxCompletionTokens = param.NewOpt(int64(*req.MaxTokens)) } + if req.Temperature != nil { params.Temperature = param.NewOpt(*req.Temperature) } + if req.TopP != nil { params.TopP = param.NewOpt(*req.TopP) } + if req.FrequencyPenalty != nil { params.FrequencyPenalty = param.NewOpt(*req.FrequencyPenalty) } + if req.PresencePenalty != nil { params.PresencePenalty = param.NewOpt(*req.PresencePenalty) } + if len(req.StopSequences) > 0 { params.Stop = openai.ChatCompletionNewParamsStopUnion{ OfStringArray: req.StopSequences, } } + if len(req.Tools) > 0 { params.Tools = buildTools(req.Tools) } + if req.ToolChoice != nil { params.ToolChoice = buildToolChoice(req.ToolChoice) } + if req.ParallelToolCalls != nil { params.ParallelToolCalls = param.NewOpt(*req.ParallelToolCalls) } + if req.ResponseFormat != nil { params.ResponseFormat = buildResponseFormat(req.ResponseFormat) } + if req.Thinking != nil && req.Thinking.Enabled && isReasoningModel(req.Model) { switch { case req.Thinking.BudgetTokens <= 1024: @@ -205,6 +222,7 @@ func buildMessages(messages []llm.Message) []openai.ChatCompletionMessageParamUn parts = append(parts, buildFilePart(p)) } } + out = append(out, openai.UserMessage(parts)) case llm.RoleAssistant: m := openai.ChatCompletionAssistantMessageParam{ @@ -224,6 +242,7 @@ func buildMessages(messages []llm.Message) []openai.ChatCompletionMessageParamUn } } } + out = append(out, openai.ChatCompletionMessageParamUnion{OfAssistant: &m}) case llm.RoleTool: out = append(out, openai.ToolMessage(msg.Text(), msg.ToolCallID)) @@ -247,8 +266,10 @@ func buildTools(tools []llm.Tool) []openai.ChatCompletionToolParam { fn.Parameters = params } } + out[i] = openai.ChatCompletionToolParam{Function: fn} } + return out } @@ -294,13 +315,16 @@ func buildResponseFormat(rf *llm.ResponseFormat) openai.ChatCompletionNewParamsR if rf.JSONSchema.Description != "" { schema.Description = param.NewOpt(rf.JSONSchema.Description) } + if rf.JSONSchema.Schema != nil { schema.Schema = rf.JSONSchema.Schema } + return openai.ChatCompletionNewParamsResponseFormatUnion{ OfJSONSchema: &shared.ResponseFormatJSONSchemaParam{JSONSchema: schema}, } } + return openai.ChatCompletionNewParamsResponseFormatUnion{} default: return openai.ChatCompletionNewParamsResponseFormatUnion{} @@ -319,6 +343,7 @@ func mapResponse(c *openai.ChatCompletion) *llm.ChatCompletionResponse { if len(c.Choices) > 0 { choice := c.Choices[0] resp.FinishReason = mapFinishReason(choice.FinishReason) + resp.Message = llm.Message{ Role: llm.RoleAssistant, Parts: []llm.Part{llm.TextPart{Text: choice.Message.Content}}, @@ -371,9 +396,11 @@ func mapError(err error) error { if apiErr.Code == "context_length_exceeded" { return &llm.ErrContextLength{Err: err} } + if apiErr.Code == "content_filter" { return &llm.ErrContentFilter{Err: err} } + return err default: return err @@ -384,13 +411,16 @@ func parseRetryAfter(resp *http.Response) time.Duration { if resp == nil { return 0 } + h := resp.Header.Get("Retry-After") if h == "" { return 0 } + if secs, err := strconv.Atoi(h); err == nil { return time.Duration(secs) * time.Second } + return 0 } @@ -407,6 +437,7 @@ func (s *openaiStream) Next() bool { chunk := s.stream.Current() s.current = mapChunkToEvent(&chunk) + return true } @@ -419,6 +450,7 @@ func (s *openaiStream) Err() error { if err != nil { return mapError(err) } + return nil } @@ -474,6 +506,7 @@ func isReasoningModel(model string) bool { return true } } + return false } @@ -488,6 +521,7 @@ func buildFilePart(p llm.FilePart) openai.ChatCompletionContentPartUnionParam { if err != nil { return openai.TextContentPart(fmt.Sprintf("[file: %s, type: %s, error decoding content]", p.Filename, p.MimeType)) } + return openai.TextContentPart(fmt.Sprintf("File: %s\n\n%s", p.Filename, string(decoded))) default: return openai.FileContentPart(openai.ChatCompletionContentPartFileFileParam{ diff --git a/pkg/llm/registry.go b/pkg/llm/registry.go index 8c20c238f..1cc0f23b2 100644 --- a/pkg/llm/registry.go +++ b/pkg/llm/registry.go @@ -66,6 +66,7 @@ func NewRegistry(models map[string]ModelDefinition) *Registry { m.ID = id r.index(&m) } + return r } @@ -74,6 +75,7 @@ func DefaultRegistry() *Registry { defaultRegistryOnce.Do(func() { defaultRegistry = NewRegistry(generatedModels) }) + return defaultRegistry } @@ -84,9 +86,11 @@ func (r *Registry) Lookup(modelID string) (ModelDefinition, bool) { if m, ok := r.byID[modelID]; ok { return *m, true } + if m, ok := r.byID[normalizeModelID(modelID)]; ok { return *m, true } + return ModelDefinition{}, false } @@ -114,5 +118,6 @@ func normalizeModelID(id string) string { if idx := strings.IndexByte(id, '/'); idx >= 0 { id = id[idx+1:] } + return strings.ReplaceAll(id, ".", "-") } diff --git a/pkg/llm/trace.go b/pkg/llm/trace.go index ce54f1f22..97ea270bd 100644 --- a/pkg/llm/trace.go +++ b/pkg/llm/trace.go @@ -36,12 +36,15 @@ func startChatSpan(ctx context.Context, tracer trace.Tracer, system string, req if req.Temperature != nil { attrs = append(attrs, semconv.GenAIRequestTemperature(*req.Temperature)) } + if req.MaxTokens != nil { attrs = append(attrs, semconv.GenAIRequestMaxTokens(*req.MaxTokens)) } + if req.TopP != nil { attrs = append(attrs, semconv.GenAIRequestTopP(*req.TopP)) } + if len(req.StopSequences) > 0 { attrs = append(attrs, semconv.GenAIRequestStopSequences(req.StopSequences...)) } @@ -59,6 +62,7 @@ func endChatSpan(span trace.Span, resp *ChatCompletionResponse, err error) { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) span.End() + return } @@ -95,13 +99,16 @@ func (s *tracedStream) Next() bool { s.finalizeSpan() return false } + s.lastEvent = s.inner.Event() if s.lastEvent.FinishReason != nil { s.finishReason = s.lastEvent.FinishReason } + if s.lastEvent.Usage != nil { s.usage = s.lastEvent.Usage } + return true } @@ -119,7 +126,9 @@ func (s *tracedStream) Close() error { s.span.RecordError(err) s.span.SetStatus(codes.Error, err.Error()) } + s.finalizeSpan() + return err } @@ -129,6 +138,7 @@ func (s *tracedStream) finalizeSpan() { s.span.RecordError(err) s.span.SetStatus(codes.Error, err.Error()) s.span.End() + return } @@ -139,12 +149,15 @@ func (s *tracedStream) finalizeSpan() { semconv.GenAIUsageOutputTokens(s.usage.OutputTokens), ) } + if s.finishReason != nil { attrs = append(attrs, semconv.GenAIResponseFinishReasons(string(*s.finishReason))) } + if len(attrs) > 0 { s.span.SetAttributes(attrs...) } + s.span.End() }) } diff --git a/pkg/mail/addr.go b/pkg/mail/addr.go index 9575ba273..e5b4a204c 100644 --- a/pkg/mail/addr.go +++ b/pkg/mail/addr.go @@ -81,6 +81,7 @@ func (a *Addr) Scan(value any) error { } var str string + switch v := value.(type) { case string: str = v @@ -126,13 +127,16 @@ func (a Addrs) Value() (driver.Value, error) { if a == nil { return nil, nil } + if len(a) == 0 { return "{}", nil } + strs := make([]string, len(a)) for i, addr := range a { strs[i] = addr.String() } + return "{" + strings.Join(strs, ",") + "}", nil } @@ -143,6 +147,7 @@ func (a *Addrs) Scan(value any) error { } var strs []string + switch v := value.(type) { case []string: strs = v @@ -167,10 +172,12 @@ func (a *Addrs) Scan(value any) error { strs[i] = "" continue } + str, ok := elem.(string) if !ok { return fmt.Errorf("array element is not a string: %T", elem) } + strs[i] = str } default: @@ -183,11 +190,14 @@ func (a *Addrs) Scan(value any) error { (*a)[i] = Nil continue } + parsed, err := ParseAddr(str) if err != nil { return fmt.Errorf("invalid email at index %d: %w", i, err) } + (*a)[i] = parsed } + return nil } diff --git a/pkg/mailer/mailer.go b/pkg/mailer/mailer.go index a69a33dbe..6633bbb6d 100644 --- a/pkg/mailer/mailer.go +++ b/pkg/mailer/mailer.go @@ -122,6 +122,7 @@ func (h *sendingHandler) Claim(ctx context.Context) (coredata.Email, error) { if errors.Is(err, coredata.ErrNoUnsentEmail) { return coredata.Email{}, worker.ErrNoTask } + return coredata.Email{}, err } @@ -133,8 +134,10 @@ func (h *sendingHandler) Process(ctx context.Context, email coredata.Email) erro if failErr := h.failEmail(ctx, &email, sendErr); failErr != nil { h.logger.ErrorCtx(ctx, "cannot fail email", log.Error(failErr)) } + return sendErr } + return nil } @@ -222,6 +225,7 @@ func (h *sendingHandler) sendAndCommit( if errors.Is(err, context.DeadlineExceeded) { return fmt.Errorf("email sending timed out after %s: %w", h.smtpTimeout, err) } + return fmt.Errorf("cannot send email: %w", err) } @@ -299,6 +303,7 @@ func (h *sendingHandler) sendMail(ctx context.Context, to []string, msg []byte) if err != nil { return fmt.Errorf("connection error: %w", err) } + defer func() { _ = conn.Close() }() if deadline, ok := ctx.Deadline(); ok { @@ -311,6 +316,7 @@ func (h *sendingHandler) sendMail(ctx context.Context, to []string, msg []byte) if err != nil { return fmt.Errorf("SMTP client creation error: %w", err) } + defer func() { _ = c.Quit() }() if h.smtp.TLSRequired { diff --git a/pkg/mailman/compliance_mailing_list.go b/pkg/mailman/compliance_mailing_list.go index 0f71d7c4e..6efc2bd1b 100644 --- a/pkg/mailman/compliance_mailing_list.go +++ b/pkg/mailman/compliance_mailing_list.go @@ -71,26 +71,32 @@ func (s *Service) mailingListEmailConfig( if err := mailingList.LoadByID(ctx, conn, scope, mailingListID); err != nil { return fmt.Errorf("cannot load mailing list: %w", err) } + if err := compliancePage.LoadByMailingListID(ctx, conn, scope, mailingListID); err != nil { if errors.Is(err, coredata.ErrResourceNotFound) { return err } + return fmt.Errorf("cannot load compliance page: %w", err) } + if compliancePage.LogoFileID != nil { if err := logoFile.LoadByID(ctx, conn, scope, *compliancePage.LogoFileID); err != nil { return fmt.Errorf("cannot load logo file: %w", err) } } + if err := organization.LoadByID(ctx, conn, scope, compliancePage.OrganizationID); err != nil { return fmt.Errorf("cannot load organization: %w", err) } + customDomain = &coredata.CustomDomain{} if err := customDomain.LoadByOrganizationID(ctx, conn, scope, organization.ID); err != nil { if !errors.Is(err, coredata.ErrResourceNotFound) { return fmt.Errorf("cannot load custom domain: %w", err) } } + return nil }, ) @@ -131,6 +137,7 @@ func (s *Service) presenterConfigFromTrustCenter( if err != nil { return cfg, "", fmt.Errorf("cannot parse custom domain URL: %w", err) } + compliancePageBase = customBase.WithPath("") } @@ -148,10 +155,12 @@ func (s *Service) presenterConfigFromTrustCenter( BucketName: logoFile.BucketName, MimeType: logoFile.MimeType, } + cfg.SenderCompanyName = organization.Name if organization.WebsiteURL != nil { cfg.SenderCompanyWebsiteURL = *organization.WebsiteURL } + if organization.HeadquarterAddress != nil { cfg.SenderCompanyHeadquarterAddress = *organization.HeadquarterAddress } diff --git a/pkg/mailman/mailing_list_worker.go b/pkg/mailman/mailing_list_worker.go index eab3cffc7..8c76c1fc3 100644 --- a/pkg/mailman/mailing_list_worker.go +++ b/pkg/mailman/mailing_list_worker.go @@ -78,6 +78,7 @@ func (h *mailingListHandler) Claim(ctx context.Context) (coredata.MailingListUpd if errors.Is(err, coredata.ErrResourceNotFound) { return coredata.MailingListUpdate{}, worker.ErrNoTask } + return coredata.MailingListUpdate{}, err } @@ -115,6 +116,7 @@ func (h *mailingListHandler) RecoverStale(ctx context.Context) error { if err := coredata.ResetStaleProcessingMailingListUpdates(ctx, tx, h.staleAfter); err != nil { return fmt.Errorf("cannot reset stale processing mailing list updates: %w", err) } + return nil }, ) diff --git a/pkg/mailman/service.go b/pkg/mailman/service.go index 4dd575437..25fdf7098 100644 --- a/pkg/mailman/service.go +++ b/pkg/mailman/service.go @@ -115,6 +115,7 @@ func (s *Service) UpdateMailingList( replyTo *mail.Addr, ) (*coredata.MailingList, error) { var ml coredata.MailingList + scope := coredata.NewScopeFromObjectID(id) err := s.pg.WithTx( @@ -124,6 +125,7 @@ func (s *Service) UpdateMailingList( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrMailingListNotFound } + return fmt.Errorf("cannot load mailing list: %w", err) } @@ -166,6 +168,7 @@ func (s *Service) GetSubscriber( if errors.Is(err, coredata.ErrResourceNotFound) { return nil, nil } + return nil, err } @@ -186,11 +189,14 @@ func (s *Service) CreateSubscriber( scope := coredata.NewScopeFromObjectID(mailingListID) status := coredata.MailingListSubscriberStatusPending + var emailRecord *coredata.Email + if req.Confirmed { status = coredata.MailingListSubscriberStatusConfirmed } else { var err error + emailRecord, err = s.buildConfirmationMail(ctx, mailingListID, email, fullName) if err != nil { return nil, fmt.Errorf("cannot build confirmation mail: %w", err) @@ -215,12 +221,14 @@ func (s *Service) CreateSubscriber( if err := ml.LoadByID(ctx, tx, scope, mailingListID); err != nil { return fmt.Errorf("cannot load mailing list: %w", err) } + subscriber.OrganizationID = ml.OrganizationID if err := subscriber.Insert(ctx, tx, scope); err != nil { if errors.Is(err, coredata.ErrResourceAlreadyExists) { return ErrSubscriberAlreadyExist } + return fmt.Errorf("cannot insert mailing list subscriber: %w", err) } @@ -245,6 +253,7 @@ func (s *Service) UnsubscribeByEmail( email mail.Addr, ) error { scope := coredata.NewScopeFromObjectID(mailingListID) + return s.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { @@ -253,6 +262,7 @@ func (s *Service) UnsubscribeByEmail( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrSubscriberNotFound } + return fmt.Errorf("cannot load mailing list subscriber: %w", err) } @@ -262,6 +272,7 @@ func (s *Service) UnsubscribeByEmail( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrSubscriberNotFound } + return fmt.Errorf("cannot delete mailing list subscriber: %w", err) } @@ -296,6 +307,7 @@ func (s *Service) ConfirmSubscriberByEmail( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrSubscriberNotFound } + return fmt.Errorf("cannot load mailing list subscriber: %w", err) } @@ -306,6 +318,7 @@ func (s *Service) ConfirmSubscriberByEmail( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrSubscriberNotFound } + return fmt.Errorf("cannot update mailing list subscriber: %w", err) } @@ -328,6 +341,7 @@ func (s *Service) DeleteSubscriber( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrSubscriberNotFound } + return fmt.Errorf("cannot load mailing list subscriber: %w", err) } @@ -337,6 +351,7 @@ func (s *Service) DeleteSubscriber( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrSubscriberNotFound } + return fmt.Errorf("cannot delete mailing list subscriber: %w", err) } @@ -361,16 +376,19 @@ func (s *Service) CountSubscribers( mailingListID gid.GID, ) (int, error) { scope := coredata.NewScopeFromObjectID(mailingListID) + var count int err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) (err error) { subscribers := coredata.MailingListSubscribers{} + count, err = subscribers.CountByMailingListID(ctx, conn, scope, mailingListID) if err != nil { return fmt.Errorf("cannot count mailing list subscribers: %w", err) } + return nil }, ) @@ -387,6 +405,7 @@ func (s *Service) ListSubscribers( cursor *page.Cursor[coredata.MailingListSubscriberOrderField], ) (*page.Page[*coredata.MailingListSubscriber, coredata.MailingListSubscriberOrderField], error) { scope := coredata.NewScopeFromObjectID(mailingListID) + var subscribers coredata.MailingListSubscribers err := s.pg.WithConn( @@ -395,6 +414,7 @@ func (s *Service) ListSubscribers( if err := subscribers.LoadByMailingListID(ctx, conn, scope, mailingListID, cursor); err != nil { return fmt.Errorf("cannot load mailing list subscribers: %w", err) } + return nil }, ) @@ -435,6 +455,7 @@ func (s *Service) CreateMailingListUpdate( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrMailingListNotFound } + return fmt.Errorf("cannot load mailing list: %w", err) } @@ -459,6 +480,7 @@ func (s *Service) GetMailingListUpdate( id gid.GID, ) (*coredata.MailingListUpdate, error) { scope := coredata.NewScopeFromObjectID(id) + var mlu coredata.MailingListUpdate err := s.pg.WithConn( @@ -468,8 +490,10 @@ func (s *Service) GetMailingListUpdate( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrMailingListUpdateNotFound } + return fmt.Errorf("cannot load mailing list update: %w", err) } + return nil }, ) @@ -489,6 +513,7 @@ func (s *Service) UpdateMailingListUpdate( } scope := coredata.NewScopeFromObjectID(req.ID) + var mlu coredata.MailingListUpdate err := s.pg.WithTx( @@ -498,6 +523,7 @@ func (s *Service) UpdateMailingListUpdate( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrMailingListUpdateNotFound } + return fmt.Errorf("cannot load mailing list update: %w", err) } @@ -508,9 +534,11 @@ func (s *Service) UpdateMailingListUpdate( if req.Title != nil { mlu.Title = *req.Title } + if req.Body != nil { mlu.Body = *req.Body } + mlu.UpdatedAt = time.Now() if err := mlu.Update(ctx, tx, scope); err != nil { @@ -532,6 +560,7 @@ func (s *Service) SendMailingListUpdate( id gid.GID, ) (*coredata.MailingListUpdate, error) { scope := coredata.NewScopeFromObjectID(id) + var mlu coredata.MailingListUpdate err := s.pg.WithTx( @@ -541,6 +570,7 @@ func (s *Service) SendMailingListUpdate( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrMailingListUpdateNotFound } + return fmt.Errorf("cannot load mailing list update: %w", err) } @@ -579,8 +609,10 @@ func (s *Service) DeleteMailingListUpdate( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrMailingListUpdateNotFound } + return fmt.Errorf("cannot delete mailing list update: %w", err) } + return nil }, ) @@ -592,6 +624,7 @@ func (s *Service) ListMailingListUpdates( cursor *page.Cursor[coredata.MailingListUpdateOrderField], ) (*page.Page[*coredata.MailingListUpdate, coredata.MailingListUpdateOrderField], error) { scope := coredata.NewScopeFromObjectID(mailingListID) + var items coredata.MailingListUpdateItems err := s.pg.WithConn( @@ -600,6 +633,7 @@ func (s *Service) ListMailingListUpdates( if err := items.LoadByMailingListID(ctx, conn, scope, mailingListID, cursor); err != nil { return fmt.Errorf("cannot load mailing list updates: %w", err) } + return nil }, ) @@ -616,6 +650,7 @@ func (s *Service) ListSentMailingListUpdates( cursor *page.Cursor[coredata.MailingListUpdateOrderField], ) (*page.Page[*coredata.MailingListUpdate, coredata.MailingListUpdateOrderField], error) { scope := coredata.NewScopeFromObjectID(mailingListID) + var items coredata.MailingListUpdateItems err := s.pg.WithConn( @@ -624,6 +659,7 @@ func (s *Service) ListSentMailingListUpdates( if err := items.LoadSentByMailingListID(ctx, conn, scope, mailingListID, cursor); err != nil { return fmt.Errorf("cannot load sent mailing list updates: %w", err) } + return nil }, ) @@ -639,17 +675,22 @@ func (s *Service) CountMailingListUpdates( mailingListID gid.GID, ) (int, error) { scope := coredata.NewScopeFromObjectID(mailingListID) + var count int err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { - var items coredata.MailingListUpdateItems - var err error + var ( + items coredata.MailingListUpdateItems + err error + ) + count, err = items.CountByMailingListID(ctx, conn, scope, mailingListID) if err != nil { return fmt.Errorf("cannot count mailing list updates: %w", err) } + return nil }, ) @@ -801,10 +842,12 @@ func (s *Service) buildUnsubscribeURL(mailingListID gid.GID, email mail.Addr) (s if s.tokenSecret == "" { return "", nil } + token, err := newUnsubscribeToken(s.tokenSecret, mailingListID, email) if err != nil { return "", err } + return s.apiBaseURL.WithPath(pathUnsubscribe).WithQuery("token", token).String() } @@ -812,9 +855,11 @@ func (s *Service) buildConfirmURL(mailingListID gid.GID, email mail.Addr) (strin if s.tokenSecret == "" { return "", nil } + token, err := newConfirmToken(s.tokenSecret, mailingListID, email) if err != nil { return "", err } + return s.apiBaseURL.WithPath(pathConfirm).WithQuery("token", token).String() } diff --git a/pkg/net/net.go b/pkg/net/net.go index 2308be8c6..95e641205 100644 --- a/pkg/net/net.go +++ b/pkg/net/net.go @@ -26,5 +26,6 @@ func IsLoopback(host string) bool { } ip := net.ParseIP(host) + return ip != nil && ip.IsLoopback() } diff --git a/pkg/page/cursor.go b/pkg/page/cursor.go index 1ef1fb7fe..3b206b535 100644 --- a/pkg/page/cursor.go +++ b/pkg/page/cursor.go @@ -61,6 +61,7 @@ func (c *Cursor[T]) SQLFragment() string { fieldName := c.OrderBy.Field.Column() var orderDirection string + switch { case c.OrderBy.Direction == OrderDirectionAsc && c.Position == Head: orderDirection = "ASC" diff --git a/pkg/page/cursor_key.go b/pkg/page/cursor_key.go index 50dda296c..5e973afc0 100644 --- a/pkg/page/cursor_key.go +++ b/pkg/page/cursor_key.go @@ -92,6 +92,7 @@ func (ck CursorKey) String() string { if err != nil { return "" } + return base64.RawURLEncoding.EncodeToString(data) } @@ -108,7 +109,9 @@ func (ck *CursorKey) UnmarshalText(data []byte) error { if err != nil { return err } + *ck = newCk + return nil } @@ -145,6 +148,7 @@ func (ck *CursorKey) UnmarshalBinary(data []byte) error { ck.ID = id ck.Value = value + return nil } diff --git a/pkg/pdfutils/pdfutils.go b/pkg/pdfutils/pdfutils.go index eb4590063..2af52ddc8 100644 --- a/pkg/pdfutils/pdfutils.go +++ b/pkg/pdfutils/pdfutils.go @@ -89,12 +89,14 @@ func AddConfidentialWithTimestamp(pdfData []byte, email mail.Addr) ([]byte, erro watermarkOpacity, watermarkScaleFactor, ) + watermarkConf, err := api.ImageWatermarkForReader(imageReader, desc, true, false, types.POINTS) if err != nil { return nil, fmt.Errorf("cannot create watermark from reader: %w", err) } var buf bytes.Buffer + err = api.AddWatermarks(reader, &buf, nil, watermarkConf, nil) if err != nil { return nil, fmt.Errorf("cannot add watermark: %w", err) diff --git a/pkg/probo/agent_run_handler.go b/pkg/probo/agent_run_handler.go index fe21e2742..e5c361adb 100644 --- a/pkg/probo/agent_run_handler.go +++ b/pkg/probo/agent_run_handler.go @@ -81,6 +81,7 @@ func (h *agentRunHandler) Claim(ctx context.Context) (coredata.AgentRun, error) if errors.Is(err, coredata.ErrResourceNotFound) { return coredata.AgentRun{}, worker.ErrNoTask } + return coredata.AgentRun{}, err } @@ -104,6 +105,7 @@ func (h *agentRunHandler) Process(ctx context.Context, run coredata.AgentRun) er forwarderDone := make(chan struct{}) defer close(forwarderDone) + go func() { select { case <-h.shutdownCh: @@ -114,6 +116,7 @@ func (h *agentRunHandler) Process(ctx context.Context, run coredata.AgentRun) er heartbeatCtx, cancelHeartbeat := context.WithCancel(ctx) defer cancelHeartbeat() + go h.heartbeatLease(heartbeatCtx, run.ID.String(), cancelRun) return h.executeRun(runCtx, &run) @@ -130,6 +133,7 @@ func (h *agentRunHandler) RecoverStale(ctx context.Context) error { ); err != nil { return fmt.Errorf("cannot reset stale agent runs: %w", err) } + return nil } @@ -172,11 +176,13 @@ func (h *agentRunHandler) heartbeatLease( }, ); err != nil { h.logger.ErrorCtx(ctx, "cannot heartbeat agent run lease", log.Error(err)) + if errors.Is(err, ErrAgentRunLeaseLost) { cancelRun(ErrAgentRunLeaseLost) } else { cancelRun(fmt.Errorf("%w: %w", ErrAgentRunHeartbeatFailed, err)) } + return } } @@ -202,6 +208,7 @@ func sanitizeAgentRunError(err error) string { for cut > 0 && !utf8.RuneStart(msg[cut]) { cut-- } + return msg[:cut] + "…" } @@ -246,6 +253,7 @@ func (h *agentRunHandler) executeRun(ctx context.Context, run *coredata.AgentRun log.String("run_id", runID), log.Error(cause), ) + return cause } @@ -261,6 +269,7 @@ func (h *agentRunHandler) executeRun(ctx context.Context, run *coredata.AgentRun "agent run suspended by infrastructure; leaving for stale recovery", log.String("run_id", runID), ) + return nil } } @@ -273,6 +282,7 @@ func (h *agentRunHandler) executeRun(ctx context.Context, run *coredata.AgentRun if runErr == nil { run.Status = coredata.AgentRunStatusCompleted + if result != nil { data, err := json.Marshal(result) if err != nil { @@ -287,6 +297,7 @@ func (h *agentRunHandler) executeRun(ctx context.Context, run *coredata.AgentRun if runErr != nil { run.Status = coredata.AgentRunStatusFailed run.Result = nil + h.logger.ErrorCtx( context.WithoutCancel(ctx), "agent run failed", diff --git a/pkg/probo/asset_service.go b/pkg/probo/asset_service.go index 17c757306..1b0f5f92b 100644 --- a/pkg/probo/asset_service.go +++ b/pkg/probo/asset_service.go @@ -94,7 +94,6 @@ func (s AssetService) Get( return asset.LoadByID(ctx, conn, s.svc.scope, assetID) }, ) - if err != nil { return nil, err } @@ -114,7 +113,6 @@ func (s AssetService) GetByOwnerID( return asset.LoadByOwnerID(ctx, conn, s.svc.scope) }, ) - if err != nil { return nil, err } @@ -132,6 +130,7 @@ func (s AssetService) CountForOrganizationID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { assets := coredata.Assets{} + count, err = assets.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID) if err != nil { return fmt.Errorf("cannot count assets: %w", err) @@ -140,7 +139,6 @@ func (s AssetService) CountForOrganizationID( return nil }, ) - if err != nil { return 0, err } @@ -167,7 +165,6 @@ func (s AssetService) ListForOrganizationID( ) }, ) - if err != nil { return nil, err } @@ -196,19 +193,24 @@ func (s AssetService) Update( if req.Name != nil { asset.Name = *req.Name } + if req.Amount != nil { asset.Amount = *req.Amount } + if req.OwnerID != nil { profile := &coredata.MembershipProfile{} if err := profile.LoadByID(ctx, conn, s.svc.scope, *req.OwnerID); err != nil { return fmt.Errorf("cannot load owner profile: %w", err) } + asset.OwnerID = *req.OwnerID } + if req.AssetType != nil { asset.AssetType = *req.AssetType } + if req.DataTypesStored != nil { asset.DataTypesStored = *req.DataTypesStored } @@ -225,7 +227,6 @@ func (s AssetService) Update( return nil }) - if err != nil { return nil, err } @@ -275,7 +276,6 @@ func (s AssetService) Create( return nil }) - if err != nil { return nil, err } diff --git a/pkg/probo/audit_service.go b/pkg/probo/audit_service.go index 1899fdf1c..b59dde832 100644 --- a/pkg/probo/audit_service.go +++ b/pkg/probo/audit_service.go @@ -88,6 +88,7 @@ func (uarr *UploadAuditReportRequest) Validate() error { v := validator.New() v.Check(uarr.AuditID, "audit_id", validator.Required(), validator.GID(coredata.AuditEntityType)) + if err := v.Error(); err != nil { return err } @@ -115,7 +116,6 @@ func (s AuditService) Get( return audit.LoadByID(ctx, conn, s.svc.scope, auditID) }, ) - if err != nil { return nil, err } @@ -135,7 +135,6 @@ func (s AuditService) GetByReportID( return audit.LoadByReportID(ctx, conn, s.svc.scope, reportID) }, ) - if err != nil { return nil, err } @@ -193,7 +192,6 @@ func (s *AuditService) Create( return nil }, ) - if err != nil { return nil, err } @@ -210,6 +208,7 @@ func (s *AuditService) Update( } audit := &coredata.Audit{} + err := s.svc.pg.WithTx( ctx, func(ctx context.Context, conn pg.Tx) error { @@ -220,15 +219,19 @@ func (s *AuditService) Update( if req.Name != nil { audit.Name = *req.Name } + if req.ValidFrom != nil { audit.ValidFrom = req.ValidFrom } + if req.ValidUntil != nil { audit.ValidUntil = req.ValidUntil } + if req.State != nil { audit.State = *req.State } + if req.TrustCenterVisibility != nil { audit.TrustCenterVisibility = *req.TrustCenterVisibility } @@ -242,7 +245,6 @@ func (s *AuditService) Update( return nil }, ) - if err != nil { return nil, err } @@ -255,6 +257,7 @@ func (s AuditService) Delete( auditID gid.GID, ) error { audit := coredata.Audit{ID: auditID} + return s.svc.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { @@ -262,6 +265,7 @@ func (s AuditService) Delete( if err != nil { return fmt.Errorf("cannot delete audit: %w", err) } + return nil }, ) @@ -278,6 +282,7 @@ func (s AuditService) ListForOrganizationID( ctx, func(ctx context.Context, conn pg.Querier) error { filter := coredata.NewAuditFilter() + err := audits.LoadByOrganizationID(ctx, conn, s.svc.scope, organizationID, cursor, filter) if err != nil { return fmt.Errorf("cannot load audits: %w", err) @@ -286,7 +291,6 @@ func (s AuditService) ListForOrganizationID( return nil }, ) - if err != nil { return nil, err } @@ -304,6 +308,7 @@ func (s AuditService) CountForOrganizationID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { audits := coredata.Audits{} + count, err = audits.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID) if err != nil { return fmt.Errorf("cannot count audits: %w", err) @@ -312,7 +317,6 @@ func (s AuditService) CountForOrganizationID( return nil }, ) - if err != nil { return 0, err } @@ -386,7 +390,6 @@ func (s AuditService) UploadReport( return nil }, ) - if err != nil { return nil, err } @@ -447,7 +450,6 @@ func (s AuditService) DeleteReport( return nil }, ) - if err != nil { return nil, err } @@ -461,6 +463,7 @@ func (s AuditService) ListForControlID( cursor *page.Cursor[coredata.AuditOrderField], ) (*page.Page[*coredata.Audit, coredata.AuditOrderField], error) { var audits coredata.Audits + control := &coredata.Control{} err := s.svc.pg.WithConn( @@ -478,7 +481,6 @@ func (s AuditService) ListForControlID( return nil }, ) - if err != nil { return nil, err } @@ -496,6 +498,7 @@ func (s AuditService) CountForControlID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { audits := coredata.Audits{} + count, err = audits.CountByControlID(ctx, conn, s.svc.scope, controlID) if err != nil { return fmt.Errorf("cannot count audits: %w", err) @@ -504,7 +507,6 @@ func (s AuditService) CountForControlID( return nil }, ) - if err != nil { return 0, err } @@ -522,6 +524,7 @@ func (s AuditService) CountForFindingID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { audits := coredata.Audits{} + count, err = audits.CountByFindingID(ctx, conn, s.svc.scope, findingID) if err != nil { return fmt.Errorf("cannot count audits: %w", err) @@ -530,7 +533,6 @@ func (s AuditService) CountForFindingID( return nil }, ) - if err != nil { return 0, err } @@ -544,6 +546,7 @@ func (s AuditService) ListForFindingID( cursor *page.Cursor[coredata.AuditOrderField], ) (*page.Page[*coredata.Audit, coredata.AuditOrderField], error) { var audits coredata.Audits + finding := &coredata.Finding{} err := s.svc.pg.WithConn( @@ -561,7 +564,6 @@ func (s AuditService) ListForFindingID( return nil }, ) - if err != nil { return nil, err } diff --git a/pkg/probo/compliance_external_url_service.go b/pkg/probo/compliance_external_url_service.go index 249a1c6c7..2bc42cc7c 100644 --- a/pkg/probo/compliance_external_url_service.go +++ b/pkg/probo/compliance_external_url_service.go @@ -53,6 +53,7 @@ func (r *CreateComplianceExternalURLRequest) Validate() error { v := validator.New() v.Check(r.TrustCenterID, "trust_center_id", validator.Required(), validator.GID(coredata.TrustCenterEntityType)) v.Check(r.URL, "url", validator.Required(), validator.URL()) + return v.Error() } @@ -61,12 +62,14 @@ func (r *UpdateComplianceExternalURLRequest) Validate() error { v.Check(r.ID, "id", validator.Required(), validator.GID(coredata.ComplianceExternalURLEntityType)) v.Check(r.URL, "url", validator.Required(), validator.URL()) v.Check(r.Rank, "rank", validator.Min(1)) + return v.Error() } func (r *DeleteComplianceExternalURLRequest) Validate() error { v := validator.New() v.Check(r.ID, "id", validator.Required(), validator.GID(coredata.ComplianceExternalURLEntityType)) + return v.Error() } @@ -83,6 +86,7 @@ func (s ComplianceExternalURLService) List( if err := items.LoadByTrustCenterID(ctx, conn, s.svc.scope, trustCenterID, cursor); err != nil { return fmt.Errorf("cannot load compliance external URLs: %w", err) } + return nil }, ) diff --git a/pkg/probo/compliance_framework_service.go b/pkg/probo/compliance_framework_service.go index d3b24dfc0..b369a8a50 100644 --- a/pkg/probo/compliance_framework_service.go +++ b/pkg/probo/compliance_framework_service.go @@ -84,6 +84,7 @@ func (s ComplianceFrameworkService) ListWithHiddenForTrustCenterID( if err := cfs.LoadWithHiddenByTrustCenterID(ctx, conn, s.svc.scope, trustCenterID, cursor); err != nil { return fmt.Errorf("cannot load compliance frameworks with hidden: %w", err) } + return nil }, ) @@ -132,7 +133,6 @@ func (s ComplianceFrameworkService) Create( return nil }, ) - if err != nil { return nil, err } @@ -169,7 +169,6 @@ func (s ComplianceFrameworkService) Update( return nil }, ) - if err != nil { return nil, err } diff --git a/pkg/probo/connector_service.go b/pkg/probo/connector_service.go index 795b818eb..873c93049 100644 --- a/pkg/probo/connector_service.go +++ b/pkg/probo/connector_service.go @@ -77,6 +77,7 @@ func (car *CreateConnectorRequest) Validate() error { v.Check(car.Provider, "provider", validator.Required(), validator.OneOfSlice(coredata.ConnectorProviders())) v.Check(car.Protocol, "protocol", validator.Required(), validator.OneOfSlice(coredata.ConnectorProtocols())) v.Check(car.Connection, "connection", validator.Required()) + return v.Error() } @@ -86,6 +87,7 @@ func (rcr *ReconnectConnectorRequest) Validate() error { v.Check(rcr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType)) v.Check(rcr.Provider, "provider", validator.Required(), validator.OneOfSlice(coredata.ConnectorProviders())) v.Check(rcr.Connection, "connection", validator.Required()) + return v.Error() } @@ -110,7 +112,6 @@ func (s *ConnectorService) ListForOrganizationID( ) }, ) - if err != nil { return nil, fmt.Errorf("cannot list connectors: %w", err) } @@ -325,7 +326,6 @@ func (s *ConnectorService) Create( return nil }, ) - if err != nil { return nil, err } @@ -359,9 +359,11 @@ func (s *ConnectorService) Reconnect( if cnnctr.OrganizationID != req.OrganizationID { return fmt.Errorf("cannot reconnect connector: organization mismatch") } + if cnnctr.Provider != req.Provider { return fmt.Errorf("cannot reconnect connector: provider mismatch") } + if cnnctr.Protocol != coredata.ConnectorProtocolOAuth2 { return fmt.Errorf("cannot reconnect connector: not an OAuth2 connector") } @@ -397,6 +399,7 @@ func preserveConnectionFields(newConn, oldConn connector.Connection) { if n.RefreshToken == "" { n.RefreshToken = o.RefreshToken } + if n.Settings.WebhookURL == "" { n.Settings.WebhookURL = o.Settings.WebhookURL n.Settings.Channel = o.Settings.Channel diff --git a/pkg/probo/control_service.go b/pkg/probo/control_service.go index 03c7f2e7f..332dd3e03 100644 --- a/pkg/probo/control_service.go +++ b/pkg/probo/control_service.go @@ -103,6 +103,7 @@ func (s ControlService) CountForDocumentID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { controls := &coredata.Controls{} + count, err = controls.CountByDocumentID(ctx, conn, s.svc.scope, documentID, filter) if err != nil { return fmt.Errorf("cannot count controls: %w", err) @@ -111,7 +112,6 @@ func (s ControlService) CountForDocumentID( return nil }, ) - if err != nil { return 0, fmt.Errorf("cannot count controls: %w", err) } @@ -126,6 +126,7 @@ func (s ControlService) ListForDocumentID( filter *coredata.ControlFilter, ) (*page.Page[*coredata.Control, coredata.ControlOrderField], error) { var controls coredata.Controls + document := &coredata.Document{} err := s.svc.pg.WithConn( @@ -138,7 +139,6 @@ func (s ControlService) ListForDocumentID( return controls.LoadByDocumentID(ctx, conn, s.svc.scope, documentID, cursor, filter) }, ) - if err != nil { return nil, fmt.Errorf("cannot list controls: %w", err) } @@ -157,6 +157,7 @@ func (s ControlService) CountForMeasureID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { controls := &coredata.Controls{} + count, err = controls.CountByMeasureID(ctx, conn, s.svc.scope, measureID, filter) if err != nil { return fmt.Errorf("cannot count controls: %w", err) @@ -165,7 +166,6 @@ func (s ControlService) CountForMeasureID( return nil }, ) - if err != nil { return 0, fmt.Errorf("cannot count controls: %w", err) } @@ -180,6 +180,7 @@ func (s ControlService) ListForMeasureID( filter *coredata.ControlFilter, ) (*page.Page[*coredata.Control, coredata.ControlOrderField], error) { var controls coredata.Controls + measure := &coredata.Measure{} err := s.svc.pg.WithConn( @@ -192,7 +193,6 @@ func (s ControlService) ListForMeasureID( return controls.LoadByMeasureID(ctx, conn, s.svc.scope, measureID, cursor, filter) }, ) - if err != nil { return nil, fmt.Errorf("cannot list controls: %w", err) } @@ -211,6 +211,7 @@ func (s ControlService) CountForFrameworkID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { controls := &coredata.Controls{} + count, err = controls.CountByFrameworkID(ctx, conn, s.svc.scope, frameworkID, filter) if err != nil { return fmt.Errorf("cannot count controls: %w", err) @@ -219,7 +220,6 @@ func (s ControlService) CountForFrameworkID( return nil }, ) - if err != nil { return 0, fmt.Errorf("cannot count controls: %w", err) } @@ -234,6 +234,7 @@ func (s ControlService) ListForFrameworkID( filter *coredata.ControlFilter, ) (*page.Page[*coredata.Control, coredata.ControlOrderField], error) { var controls coredata.Controls + framework := &coredata.Framework{} err := s.svc.pg.WithConn( @@ -253,7 +254,6 @@ func (s ControlService) ListForFrameworkID( ) }, ) - if err != nil { return nil, fmt.Errorf("cannot list controls: %w", err) } @@ -272,6 +272,7 @@ func (s ControlService) CountForOrganizationID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { controls := &coredata.Controls{} + count, err = controls.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, filter) if err != nil { return fmt.Errorf("cannot count controls: %w", err) @@ -280,7 +281,6 @@ func (s ControlService) CountForOrganizationID( return nil }, ) - if err != nil { return 0, fmt.Errorf("cannot count controls: %w", err) } @@ -295,6 +295,7 @@ func (s ControlService) ListForOrganizationID( filter *coredata.ControlFilter, ) (*page.Page[*coredata.Control, coredata.ControlOrderField], error) { var controls coredata.Controls + organization := &coredata.Organization{} err := s.svc.pg.WithConn( @@ -314,7 +315,6 @@ func (s ControlService) ListForOrganizationID( ) }, ) - if err != nil { return nil, fmt.Errorf("cannot list controls: %w", err) } @@ -333,6 +333,7 @@ func (s ControlService) CountForRiskID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { controls := &coredata.Controls{} + count, err = controls.CountByRiskID(ctx, conn, s.svc.scope, riskID, filter) if err != nil { return fmt.Errorf("cannot count controls: %w", err) @@ -341,7 +342,6 @@ func (s ControlService) CountForRiskID( return nil }, ) - if err != nil { return 0, fmt.Errorf("cannot count controls: %w", err) } @@ -356,6 +356,7 @@ func (s ControlService) ListForRiskID( filter *coredata.ControlFilter, ) (*page.Page[*coredata.Control, coredata.ControlOrderField], error) { var controls coredata.Controls + risk := &coredata.Risk{} err := s.svc.pg.WithConn( @@ -368,7 +369,6 @@ func (s ControlService) ListForRiskID( return controls.LoadByRiskID(ctx, conn, s.svc.scope, risk.ID, cursor, filter) }, ) - if err != nil { return nil, fmt.Errorf("cannot list controls: %w", err) } @@ -406,7 +406,6 @@ func (s ControlService) CreateMeasureMapping( return controlMeasure.Upsert(ctx, conn, s.svc.scope) }, ) - if err != nil { return nil, nil, fmt.Errorf("cannot create control measure mapping: %w", err) } @@ -441,7 +440,6 @@ func (s ControlService) DeleteMeasureMapping( return nil }, ) - if err != nil { return nil, nil, fmt.Errorf("cannot delete control measure mapping: %w", err) } @@ -483,7 +481,6 @@ func (s ControlService) CreateDocumentMapping( return nil }, ) - if err != nil { return nil, nil, fmt.Errorf("cannot create control document mapping: %w", err) } @@ -518,7 +515,6 @@ func (s ControlService) DeleteDocumentMapping( return nil }, ) - if err != nil { return nil, nil, fmt.Errorf("cannot delete control document mapping: %w", err) } @@ -559,7 +555,6 @@ func (s ControlService) CreateAuditMapping( return nil }, ) - if err != nil { return nil, nil, err } @@ -594,7 +589,6 @@ func (s ControlService) DeleteAuditMapping( return nil }, ) - if err != nil { return nil, nil, fmt.Errorf("cannot delete control audit mapping: %w", err) } @@ -634,7 +628,6 @@ func (s ControlService) CreateObligationMapping( return nil }, ) - if err != nil { return nil, nil, err } @@ -669,7 +662,6 @@ func (s ControlService) DeleteObligationMapping( return nil }, ) - if err != nil { return nil, nil, fmt.Errorf("cannot delete control obligation mapping: %w", err) } @@ -684,6 +676,7 @@ func (s ControlService) ListForAuditID( filter *coredata.ControlFilter, ) (*page.Page[*coredata.Control, coredata.ControlOrderField], error) { var controls coredata.Controls + audit := &coredata.Audit{} err := s.svc.pg.WithConn( @@ -692,6 +685,7 @@ func (s ControlService) ListForAuditID( if err := audit.LoadByID(ctx, conn, s.svc.scope, auditID); err != nil { return fmt.Errorf("cannot load audit: %w", err) } + if err := controls.LoadByAuditID(ctx, conn, s.svc.scope, auditID, cursor, filter); err != nil { return fmt.Errorf("cannot load controls: %w", err) } @@ -699,7 +693,6 @@ func (s ControlService) ListForAuditID( return nil }, ) - if err != nil { return nil, err } @@ -718,6 +711,7 @@ func (s ControlService) CountForStatementOfApplicabilityID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { controls := &coredata.Controls{} + count, err = controls.CountByStatementOfApplicabilityID(ctx, conn, s.svc.scope, statementOfApplicabilityID, filter) if err != nil { return fmt.Errorf("cannot count controls: %w", err) @@ -726,7 +720,6 @@ func (s ControlService) CountForStatementOfApplicabilityID( return nil }, ) - if err != nil { return 0, fmt.Errorf("cannot count controls: %w", err) } @@ -776,7 +769,6 @@ func (s ControlService) Create( return control.Insert(ctx, conn, s.svc.scope) }, ) - if err != nil { return nil, fmt.Errorf("cannot create control: %w", err) } @@ -796,7 +788,6 @@ func (s ControlService) Get( return control.LoadByID(ctx, conn, s.svc.scope, controlID) }, ) - if err != nil { return nil, fmt.Errorf("cannot get control: %w", err) } @@ -915,11 +906,14 @@ func (s ControlService) HasRegulatoryObligation( ctx, func(ctx context.Context, conn pg.Querier) error { var controlObligations coredata.ControlObligations + count, err := controlObligations.CountByControlID(ctx, conn, s.svc.scope, controlID, filter) if err != nil { return fmt.Errorf("cannot count regulatory obligations: %w", err) } + hasRegulatory = count > 0 + return nil }, ) @@ -940,11 +934,14 @@ func (s ControlService) HasContractualObligation( ctx, func(ctx context.Context, conn pg.Querier) error { var controlObligations coredata.ControlObligations + count, err := controlObligations.CountByControlID(ctx, conn, s.svc.scope, controlID, filter) if err != nil { return fmt.Errorf("cannot count contractual obligations: %w", err) } + hasContractual = count > 0 + return nil }, ) @@ -967,6 +964,7 @@ func (s ControlService) HasRiskAssessment( } hasRisk = len(controlsWithRisk) > 0 + return nil }, ) diff --git a/pkg/probo/custom_domain_service.go b/pkg/probo/custom_domain_service.go index 398bfb31a..08f70e36d 100644 --- a/pkg/probo/custom_domain_service.go +++ b/pkg/probo/custom_domain_service.go @@ -83,7 +83,6 @@ func (s *CustomDomainService) CreateCustomDomain( return nil }, ) - if err != nil { return nil, err } @@ -152,7 +151,6 @@ func (s *CustomDomainService) GetOrganizationCustomDomain( return nil }, ) - if err != nil { return nil, err } diff --git a/pkg/probo/data_protection_impact_assessment_service.go b/pkg/probo/data_protection_impact_assessment_service.go index a06859f36..d8b450335 100644 --- a/pkg/probo/data_protection_impact_assessment_service.go +++ b/pkg/probo/data_protection_impact_assessment_service.go @@ -92,7 +92,6 @@ func (s DataProtectionImpactAssessmentService) Get( return nil }, ) - if err != nil { return nil, err } @@ -116,7 +115,6 @@ func (s DataProtectionImpactAssessmentService) GetByProcessingActivityID( return nil }, ) - if err != nil { return nil, err } @@ -142,7 +140,6 @@ func (s DataProtectionImpactAssessmentService) ListForOrganizationID( return nil }, ) - if err != nil { return nil, err } @@ -161,10 +158,10 @@ func (s DataProtectionImpactAssessmentService) CountForOrganizationID( func(ctx context.Context, conn pg.Querier) (err error) { dpias := coredata.DataProtectionImpactAssessments{} count, err = dpias.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID) + return err }, ) - if err != nil { return 0, err } @@ -211,7 +208,6 @@ func (s *DataProtectionImpactAssessmentService) Create( return nil }, ) - if err != nil { return nil, err } @@ -265,7 +261,6 @@ func (s *DataProtectionImpactAssessmentService) Update( return nil }, ) - if err != nil { return nil, err } diff --git a/pkg/probo/datum_service.go b/pkg/probo/datum_service.go index 4993b5dbd..d979a88b6 100644 --- a/pkg/probo/datum_service.go +++ b/pkg/probo/datum_service.go @@ -88,7 +88,6 @@ func (s DatumService) Get( return datum.LoadByID(ctx, conn, s.svc.scope, datumID) }, ) - if err != nil { return nil, err } @@ -108,7 +107,6 @@ func (s DatumService) GetByOwnerID( return datum.LoadByOwnerID(ctx, conn, s.svc.scope) }, ) - if err != nil { return nil, err } @@ -126,6 +124,7 @@ func (s DatumService) CountForOrganizationID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { data := coredata.Data{} + count, err = data.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID) if err != nil { return fmt.Errorf("cannot count data: %w", err) @@ -134,7 +133,6 @@ func (s DatumService) CountForOrganizationID( return nil }, ) - if err != nil { return 0, err } @@ -161,7 +159,6 @@ func (s DatumService) ListForOrganizationID( ) }, ) - if err != nil { return nil, err } @@ -189,16 +186,20 @@ func (s DatumService) Update( if req.Name != nil { datum.Name = *req.Name } + if req.DataClassification != nil { datum.DataClassification = *req.DataClassification } + if req.OwnerID != nil { owner := &coredata.MembershipProfile{} if err := owner.LoadByID(ctx, conn, s.svc.scope, *req.OwnerID); err != nil { return fmt.Errorf("cannot load owner profile: %w", err) } + datum.OwnerID = *req.OwnerID } + datum.UpdatedAt = now if err := datum.Update(ctx, conn, s.svc.scope); err != nil { @@ -213,7 +214,6 @@ func (s DatumService) Update( return nil }) - if err != nil { return nil, err } @@ -264,7 +264,6 @@ func (s DatumService) Create( return nil }, ) - if err != nil { return nil, err } @@ -299,7 +298,6 @@ func (s DatumService) ListThirdParties( return thirdParties.LoadByDatumID(ctx, conn, s.svc.scope, datumID, cursor) }, ) - if err != nil { return nil, err } diff --git a/pkg/probo/document_approval_service.go b/pkg/probo/document_approval_service.go index d5fb682f7..6597e2e6c 100644 --- a/pkg/probo/document_approval_service.go +++ b/pkg/probo/document_approval_service.go @@ -102,6 +102,7 @@ func (s *DocumentApprovalService) RequestApprovalInTx( } else { documentVersion.Major = 1 } + documentVersion.Minor = 0 documentVersion.UpdatedAt = now @@ -143,8 +144,10 @@ func (s *DocumentApprovalService) BulkPublishVersions( ctx context.Context, req BulkPublishVersionsRequest, ) ([]*coredata.DocumentVersion, []*coredata.Document, error) { - var publishedVersions []*coredata.DocumentVersion - var updatedDocuments []*coredata.Document + var ( + publishedVersions []*coredata.DocumentVersion + updatedDocuments []*coredata.Document + ) err := s.svc.pg.WithTx( ctx, @@ -174,6 +177,7 @@ func (s *DocumentApprovalService) BulkPublishVersions( if req.Minor && dv.Status == coredata.DocumentVersionStatusPublished { publishedVersions = append(publishedVersions, dv) updatedDocuments = append(updatedDocuments, document) + continue } @@ -183,6 +187,7 @@ func (s *DocumentApprovalService) BulkPublishVersions( if req.Minor { var err error + document, dv, err = s.svc.Documents.publishMinorVersionInTx(ctx, tx, documentID, &req.Changelog, true) if err != nil { return fmt.Errorf("cannot publish document %q: %w", documentID, err) @@ -204,6 +209,7 @@ func (s *DocumentApprovalService) BulkPublishVersions( } } else { var err error + document, dv, err = s.svc.Documents.publishMajorVersionInTx(ctx, tx, documentID, &req.Changelog, true) if err != nil { return fmt.Errorf("cannot publish document %q: %w", documentID, err) @@ -218,7 +224,6 @@ func (s *DocumentApprovalService) BulkPublishVersions( return nil }, ) - if err != nil { return nil, nil, err } @@ -254,8 +259,11 @@ func (s *DocumentApprovalService) Approve( return &ErrDocumentArchived{} } - var profile *coredata.MembershipProfile - var err error + var ( + profile *coredata.MembershipProfile + err error + ) + quorum, profile, err = s.loadQuorumAndProfile(ctx, conn, req.DocumentVersionID, req.IdentityID, documentVersion.OrganizationID) if err != nil { return fmt.Errorf("cannot load quorum and profile: %w", err) @@ -277,7 +285,6 @@ func (s *DocumentApprovalService) Approve( return nil }, ) - if err != nil { return nil, err } @@ -381,7 +388,6 @@ func (s *DocumentApprovalService) Approve( return nil }, ) - if err != nil { return nil, err } @@ -457,6 +463,7 @@ func (s *DocumentApprovalService) Reject( documentVersion.Major = 0 documentVersion.Minor = 1 } + documentVersion.UpdatedAt = now if err := documentVersion.Update(ctx, tx, s.svc.scope); err != nil { @@ -466,7 +473,6 @@ func (s *DocumentApprovalService) Reject( return nil }, ) - if err != nil { return nil, err } @@ -535,6 +541,7 @@ func (s *DocumentApprovalService) VoidApproval( documentVersion.Major = 0 documentVersion.Minor = 1 } + documentVersion.UpdatedAt = now if err := documentVersion.Update(ctx, tx, s.svc.scope); err != nil { @@ -544,7 +551,6 @@ func (s *DocumentApprovalService) VoidApproval( return nil }, ) - if err != nil { return nil, nil, err } @@ -564,10 +570,10 @@ func (s *DocumentApprovalService) GetQuorum( if err := quorum.LoadByID(ctx, conn, s.svc.scope, quorumID); err != nil { return fmt.Errorf("cannot load approval quorum: %w", err) } + return nil }, ) - if err != nil { return nil, err } @@ -588,10 +594,10 @@ func (s *DocumentApprovalService) ListQuorums( if err := quorums.LoadAllByDocumentVersionID(ctx, conn, s.svc.scope, documentVersionID, cursor); err != nil { return fmt.Errorf("cannot list approval quorums: %w", err) } + return nil }, ) - if err != nil { return nil, err } @@ -609,14 +615,15 @@ func (s *DocumentApprovalService) CountQuorums( ctx, func(ctx context.Context, conn pg.Querier) (err error) { quorums := &coredata.DocumentVersionApprovalQuorums{} + count, err = quorums.CountByDocumentVersionID(ctx, conn, s.svc.scope, documentVersionID) if err != nil { return fmt.Errorf("cannot count approval quorums: %w", err) } + return nil }, ) - if err != nil { return 0, err } @@ -638,10 +645,10 @@ func (s *DocumentApprovalService) ListDecisions( if err := decisions.LoadByQuorumID(ctx, conn, s.svc.scope, quorumID, cursor, filter); err != nil { return fmt.Errorf("cannot list approval decisions: %w", err) } + return nil }, ) - if err != nil { return nil, err } @@ -660,14 +667,15 @@ func (s *DocumentApprovalService) CountDecisions( ctx, func(ctx context.Context, conn pg.Querier) (err error) { decisions := &coredata.DocumentVersionApprovalDecisions{} + count, err = decisions.CountByQuorumID(ctx, conn, s.svc.scope, quorumID, filter) if err != nil { return fmt.Errorf("cannot count approval decisions: %w", err) } + return nil }, ) - if err != nil { return 0, err } @@ -687,10 +695,10 @@ func (s *DocumentApprovalService) GetDecision( if err := decision.LoadByID(ctx, conn, s.svc.scope, decisionID); err != nil { return fmt.Errorf("cannot load approval decision: %w", err) } + return nil }, ) - if err != nil { return nil, err } @@ -735,10 +743,10 @@ func (s *DocumentApprovalService) GetViewerDecision( } decision = d + return nil }, ) - if err != nil { return nil, err } @@ -758,6 +766,7 @@ func (s *DocumentApprovalService) loadQuorumAndProfile( if errors.Is(err, coredata.ErrResourceNotFound) { return nil, nil, &ErrDocumentVersionNotPendingApproval{} } + return nil, nil, fmt.Errorf("cannot load last approval quorum: %w", err) } @@ -816,6 +825,7 @@ func (s *DocumentApprovalService) sendApprovalEmails( emailLinkURLPath = approvalURLPath query = make(url.Values) ) + if profile.State != coredata.ProfileStateActive { if profile.Source != coredata.ProfileSourceSCIM { invitation := &coredata.Invitation{ @@ -842,6 +852,7 @@ func (s *DocumentApprovalService) sendApprovalEmails( emailLinkURLPath = "/auth/activate-account" continueURL := baseurl.MustParse(s.svc.baseURL).AppendPath(approvalURLPath).MustString() + query.Add("token", invitationToken) query.Add("continue", continueURL) } @@ -887,6 +898,7 @@ func (s *DocumentApprovalService) generateApprovalPDF( ctx, func(ctx context.Context, conn pg.Querier) error { var err error + pdfData, err = exportDocumentPDF( ctx, s.svc, @@ -896,6 +908,7 @@ func (s *DocumentApprovalService) generateApprovalPDF( documentVersionID, ExportPDFOptions{}, ) + return err }, ) @@ -909,6 +922,7 @@ func (s *DocumentApprovalService) countDecisions( quorumID gid.GID, ) (int, error) { decisions := &coredata.DocumentVersionApprovalDecisions{} + count, err := decisions.CountByQuorumID( ctx, conn, @@ -938,6 +952,7 @@ func (s *DocumentApprovalService) maybeApproveQuorum( } decisions := &coredata.DocumentVersionApprovalDecisions{} + approvedCount, err := decisions.CountApprovedByQuorumID(ctx, tx, s.svc.scope, quorumID) if err != nil { return fmt.Errorf("cannot count approved decisions: %w", err) diff --git a/pkg/probo/document_pdf_worker.go b/pkg/probo/document_pdf_worker.go index 0cefdf046..a4524a50d 100644 --- a/pkg/probo/document_pdf_worker.go +++ b/pkg/probo/document_pdf_worker.go @@ -61,6 +61,7 @@ func (h *documentPDFHandler) Claim(ctx context.Context) (coredata.DocumentVersio if errors.Is(err, coredata.ErrNoDocumentPDFJobAvailable) { return coredata.DocumentVersion{}, worker.ErrNoTask } + return coredata.DocumentVersion{}, err } @@ -78,6 +79,7 @@ func (h *documentPDFHandler) Process(ctx context.Context, version coredata.Docum log.String("document_version_id", version.ID.String()), log.Int("attempt", version.PdfAttemptCount), ) + return err } diff --git a/pkg/probo/document_service.go b/pkg/probo/document_service.go index 5e11cac51..cae4306e6 100644 --- a/pkg/probo/document_service.go +++ b/pkg/probo/document_service.go @@ -195,6 +195,7 @@ func (udr *UpdateDocumentRequest) Validate() error { v.Check(udr.DocumentID, "document_id", validator.Required(), validator.GID(coredata.DocumentEntityType)) v.Check(udr.TrustCenterVisibility, "trust_center_visibility", validator.OneOfSlice(coredata.TrustCenterVisibilities())) + if udr.DefaultApproverIDs != nil { v.Check(len(*udr.DefaultApproverIDs), "default_approver_ids", validator.Max(100)) v.Check(*udr.DefaultApproverIDs, "default_approver_ids", validator.NoDuplicates()) @@ -202,6 +203,7 @@ func (udr *UpdateDocumentRequest) Validate() error { v.Check(item, "default_approver_ids", validator.GID(coredata.MembershipProfileEntityType)) }) } + v.Check(udr.Title, "title", validator.SafeTextNoNewLine(TitleMaxLength)) v.Check(udr.Classification, "classification", validator.OneOfSlice(coredata.DocumentClassifications())) v.Check( @@ -290,7 +292,6 @@ func (s *DocumentService) Get( return document.LoadByID(ctx, conn, s.svc.scope, documentID) }, ) - if err != nil { return nil, err } @@ -310,7 +311,6 @@ func (s *DocumentService) GetDefaultApprovers( return approvers.LoadByDocumentID(ctx, conn, s.svc.scope, documentID) }, ) - if err != nil { return nil, fmt.Errorf("cannot load default approvers: %w", err) } @@ -332,7 +332,6 @@ func (s *DocumentService) GetDefaultApprovers( return profiles.LoadByIDs(ctx, conn, s.svc.scope, profileIDs) }, ) - if err != nil { return nil, fmt.Errorf("cannot load approver profiles: %w", err) } @@ -385,7 +384,6 @@ func (s *DocumentService) ListVersionApprovers( return nil }, ) - if err != nil { return nil, err } @@ -403,6 +401,7 @@ func (s *DocumentService) CountVersionApprovers( ctx, func(ctx context.Context, conn pg.Querier) (err error) { profiles := coredata.MembershipProfiles{} + count, err = profiles.CountByDocumentVersionID(ctx, conn, s.svc.scope, documentVersionID) if err != nil { return fmt.Errorf("cannot count document version approvers: %w", err) @@ -411,7 +410,6 @@ func (s *DocumentService) CountVersionApprovers( return nil }, ) - if err != nil { return 0, err } @@ -437,7 +435,6 @@ func (s *DocumentService) GetWithFilter( return nil }, ) - if err != nil { return nil, err } @@ -450,6 +447,7 @@ func (s DocumentService) GenerateChangelog( documentID gid.GID, ) (*string, error) { var changelog *string + draftVersion := &coredata.DocumentVersion{} publishedVersion := &coredata.DocumentVersion{} @@ -485,7 +483,6 @@ func (s DocumentService) GenerateChangelog( return nil }, ) - if err != nil { return nil, err } @@ -538,6 +535,7 @@ func (s DocumentService) generateChangelog( } text := result.FinalMessage().Text() + return &text, nil } @@ -578,8 +576,10 @@ func (s *DocumentService) PublishVersion( if err != nil { return fmt.Errorf("cannot publish minor version: %w", err) } + result.Document = document result.Version = version + return nil } @@ -588,8 +588,10 @@ func (s *DocumentService) PublishVersion( if err != nil { return fmt.Errorf("cannot publish major version: %w", err) } + result.Document = document result.Version = version + return nil } @@ -631,10 +633,10 @@ func (s *DocumentService) PublishVersion( result.Document = document result.Version = dv result.Quorum = quorum + return nil }, ) - if err != nil { return nil, err } @@ -672,6 +674,7 @@ func (s *DocumentService) Create( content := req.Content if strings.TrimSpace(content) != "" { var sanitizeErr error + content, sanitizeErr = prosemirror.SanitizeDocumentJSON(content) if sanitizeErr != nil { return nil, nil, fmt.Errorf("cannot sanitize document content: %w", sanitizeErr) @@ -722,7 +725,6 @@ func (s *DocumentService) Create( return nil }, ) - if err != nil { return nil, nil, err } @@ -757,6 +759,7 @@ func (s *DocumentService) SendSigningNotifications( emailLinkURLPath = employeeDocumentsURLPath query = make(url.Values) ) + if signatory.State != coredata.ProfileStateActive { if signatory.Source != coredata.ProfileSourceSCIM { invitation := &coredata.Invitation{ @@ -783,6 +786,7 @@ func (s *DocumentService) SendSigningNotifications( emailLinkURLPath = "/auth/activate-account" continueURL := baseurl.MustParse(s.svc.baseURL).AppendPath(employeeDocumentsURLPath).MustString() + query.Add("token", invitationToken) query.Add("continue", continueURL) } @@ -817,7 +821,6 @@ func (s *DocumentService) SendSigningNotifications( return nil }, ) - if err != nil { return fmt.Errorf("cannot send signing notifications: %w", err) } @@ -847,11 +850,12 @@ func (s *DocumentService) SignDocumentVersionByIdentity( } var signErr error + documentVersionSignature, signErr = s.signDocumentVersionInTx(ctx, conn, documentVersionID, profile.ID) + return signErr }, ) - if err != nil { return nil, fmt.Errorf("cannot sign document version: %w", err) } @@ -914,18 +918,22 @@ func (s *DocumentService) updateVersionInTx( if err != nil { return fmt.Errorf("cannot sanitize document content: %w", err) } + draftVersion.Content = sanitized } if title != nil { draftVersion.Title = *title } + if classification != nil { draftVersion.Classification = *classification } + if documentType != nil { draftVersion.DocumentType = *documentType } + draftVersion.UpdatedAt = time.Now() if err := draftVersion.Update(ctx, tx, s.svc.scope); err != nil { @@ -947,7 +955,6 @@ func (s *DocumentService) GetVersionSignature( return documentVersionSignature.LoadByID(ctx, conn, s.svc.scope, signatureID) }, ) - if err != nil { return nil, err } @@ -991,13 +998,14 @@ func (s *DocumentService) BulkRequestSignatures( if err != nil { return fmt.Errorf("cannot create signature request for document %q and signatory %q: %w", documentID, signatoryID, err) } + signatures = append(signatures, signature) } } + return nil }, ) - if err != nil { return nil, err } @@ -1024,6 +1032,7 @@ func (s *DocumentService) createSignatureRequestInTx( } existingSignature := &coredata.DocumentVersionSignature{} + err := existingSignature.LoadByDocumentVersionIDAndSignatory(ctx, tx, s.svc.scope, documentVersionID, signatoryID) if err == nil && ignoreExisting { return existingSignature, nil @@ -1087,6 +1096,7 @@ func (s *DocumentService) RequestSignature( } var err error + signature, err = s.createSignatureRequestInTx(ctx, tx, req.DocumentVersionID, req.Signatory, false) if err != nil { return fmt.Errorf("cannot create signature request: %w", err) @@ -1095,7 +1105,6 @@ func (s *DocumentService) RequestSignature( return nil }, ) - if err != nil { return nil, err } @@ -1117,7 +1126,6 @@ func (s *DocumentService) ListSignatures( return documentVersionSignatures.LoadByDocumentVersionID(ctx, conn, s.svc.scope, documentVersionID, cursor, filter) }, ) - if err != nil { return nil, err } @@ -1133,10 +1141,12 @@ func (s *DocumentService) IsVersionSignedByUserEmail( documentVersionSignature := &coredata.DocumentVersionSignature{} var signed bool + err := s.svc.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { var err error + signed, err = documentVersionSignature.IsSignedByUserEmail( ctx, conn, @@ -1144,10 +1154,10 @@ func (s *DocumentService) IsVersionSignedByUserEmail( documentVersionID, userEmail, ) + return err }, ) - if err != nil { return false, fmt.Errorf("cannot check if document version is signed: %w", err) } @@ -1338,6 +1348,7 @@ func (s *DocumentService) RequestExport( options ExportPDFOptions, ) (*coredata.ExportJob, error) { var exportJobID gid.GID + exportJob := &coredata.ExportJob{} if options.WithWatermark { @@ -1348,6 +1359,7 @@ func (s *DocumentService) RequestExport( err := s.svc.pg.WithTx(ctx, func(ctx context.Context, conn pg.Tx) error { var organizationID gid.GID + for _, documentID := range documentIDs { document := &coredata.Document{} if err := document.LoadByID(ctx, conn, s.svc.scope, documentID); err != nil { @@ -1366,6 +1378,7 @@ func (s *DocumentService) RequestExport( WatermarkEmail: options.WatermarkEmail, WithSignatures: options.WithSignatures, } + argsJSON, err := json.Marshal(args) if err != nil { return fmt.Errorf("cannot marshal document export arguments: %w", err) @@ -1388,7 +1401,6 @@ func (s *DocumentService) RequestExport( return nil }) - if err != nil { return nil, err } @@ -1412,7 +1424,6 @@ func (s *DocumentService) CountVersionsForDocumentID( return err }, ) - if err != nil { return 0, err } @@ -1436,7 +1447,6 @@ func (s *DocumentService) CountSignaturesForVersionID( return err }, ) - if err != nil { return 0, err } @@ -1455,7 +1465,6 @@ func (s *DocumentService) ListVersions( err := s.svc.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { - err := documentVersions.LoadByDocumentID(ctx, conn, s.svc.scope, documentID, cursor, filter) if err != nil { return fmt.Errorf("cannot load document versions: %w", err) @@ -1464,7 +1473,6 @@ func (s *DocumentService) ListVersions( return nil }, ) - if err != nil { return nil, err } @@ -1484,7 +1492,6 @@ func (s *DocumentService) GetVersion( return documentVersion.LoadByID(ctx, conn, s.svc.scope, documentVersionID) }, ) - if err != nil { return nil, err } @@ -1500,10 +1507,12 @@ func (s *DocumentService) IsSigned( document := &coredata.Document{} var signed bool + err := s.svc.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { var err error + signed, err = document.IsLastSignableVersionSignedByUserEmail( ctx, conn, @@ -1511,10 +1520,10 @@ func (s *DocumentService) IsSigned( documentID, userEmail, ) + return err }, ) - if err != nil { return false, fmt.Errorf("cannot check if document is signed: %w", err) } @@ -1530,10 +1539,12 @@ func (s *DocumentService) GetViewerApprovalState( document := &coredata.Document{} var state coredata.DocumentVersionApprovalDecisionState + err := s.svc.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { var err error + state, err = document.GetViewerApprovalStateForLastVersion( ctx, conn, @@ -1541,10 +1552,10 @@ func (s *DocumentService) GetViewerApprovalState( documentID, identityID, ) + return err }, ) - if err != nil { return "", fmt.Errorf("cannot get viewer approval state: %w", err) } @@ -1563,6 +1574,7 @@ func (s *DocumentService) CountForOrganizationID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { documents := &coredata.Documents{} + count, err = documents.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, filter) if err != nil { return fmt.Errorf("cannot count documents: %w", err) @@ -1571,7 +1583,6 @@ func (s *DocumentService) CountForOrganizationID( return nil }, ) - if err != nil { return 0, fmt.Errorf("cannot count documents: %w", err) } @@ -1600,7 +1611,6 @@ func (s *DocumentService) ListByOrganizationID( ) }, ) - if err != nil { return nil, err } @@ -1619,6 +1629,7 @@ func (s *DocumentService) CountForControlID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { documents := &coredata.Documents{} + count, err = documents.CountByControlID(ctx, conn, s.svc.scope, controlID, filter) if err != nil { return fmt.Errorf("cannot count documents: %w", err) @@ -1627,7 +1638,6 @@ func (s *DocumentService) CountForControlID( return nil }, ) - if err != nil { return 0, fmt.Errorf("cannot count documents: %w", err) } @@ -1649,7 +1659,6 @@ func (s *DocumentService) ListForControlID( return documents.LoadByControlID(ctx, conn, s.svc.scope, controlID, cursor, filter) }, ) - if err != nil { return nil, err } @@ -1668,6 +1677,7 @@ func (s *DocumentService) CountForRiskID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { documents := &coredata.Documents{} + count, err = documents.CountByRiskID(ctx, conn, s.svc.scope, riskID, filter) if err != nil { return fmt.Errorf("cannot count documents: %w", err) @@ -1676,7 +1686,6 @@ func (s *DocumentService) CountForRiskID( return nil }, ) - if err != nil { return 0, fmt.Errorf("cannot count documents: %w", err) } @@ -1698,7 +1707,6 @@ func (s *DocumentService) ListForRiskID( return documents.LoadByRiskID(ctx, conn, s.svc.scope, riskID, cursor, filter) }, ) - if err != nil { return nil, err } @@ -1717,6 +1725,7 @@ func (s *DocumentService) CountForMeasureID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { documents := &coredata.Documents{} + count, err = documents.CountByMeasureID(ctx, conn, s.svc.scope, measureID, filter) if err != nil { return fmt.Errorf("cannot count documents: %w", err) @@ -1725,7 +1734,6 @@ func (s *DocumentService) CountForMeasureID( return nil }, ) - if err != nil { return 0, err } @@ -1747,10 +1755,10 @@ func (s *DocumentService) ListForMeasureID( if err := documents.LoadByMeasureID(ctx, conn, s.svc.scope, measureID, cursor, filter); err != nil { return fmt.Errorf("cannot list documents for measure: %w", err) } + return nil }, ) - if err != nil { return nil, err } @@ -1767,8 +1775,12 @@ func (s *DocumentService) Update( } document := &coredata.Document{} - var resultVersion *coredata.DocumentVersion - var draftCreated bool + + var ( + resultVersion *coredata.DocumentVersion + draftCreated bool + ) + now := time.Now() err := s.svc.pg.WithTx( @@ -1811,6 +1823,7 @@ func (s *DocumentService) Update( return fmt.Errorf("cannot update default approvers: %w", err) } } + return nil } @@ -1842,7 +1855,9 @@ func (s *DocumentService) Update( if err := s.deleteDraftInTx(ctx, tx, latestVersion); err != nil { return err } + resultVersion = nil + return nil } } @@ -1873,7 +1888,6 @@ func (s *DocumentService) Update( return nil }, ) - if err != nil { return nil, nil, false, err } @@ -1914,7 +1928,6 @@ func (s *DocumentService) DeleteDraft( return s.deleteDraftInTx(ctx, tx, latestVersion) }, ) - if err != nil { return nil, err } @@ -1971,7 +1984,6 @@ func (s *DocumentService) Archive( return nil }, ) - if err != nil { return nil, err } @@ -2008,7 +2020,6 @@ func (s *DocumentService) Unarchive( return nil }, ) - if err != nil { return nil, err } @@ -2083,7 +2094,6 @@ func (s *DocumentService) ExportPDF( return nil }, ) - if err != nil { return nil, err } @@ -2093,6 +2103,7 @@ func (s *DocumentService) ExportPDF( func (s *DocumentService) BuildAndUploadExport(ctx context.Context, exportJobID gid.GID) (*coredata.ExportJob, error) { exportJob := &coredata.ExportJob{} + err := s.svc.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { @@ -2110,17 +2121,21 @@ func (s *DocumentService) BuildAndUploadExport(ctx context.Context, exportJobID } var organizationID gid.GID + firstDocument := &coredata.Document{} if err := firstDocument.LoadByID(ctx, tx, s.svc.scope, documentIDs[0]); err != nil { return fmt.Errorf("cannot load document for organization ID: %w", err) } + organizationID = firstDocument.OrganizationID tempDir := os.TempDir() + tempFile, err := os.CreateTemp(tempDir, "probo-document-export-*.zip") if err != nil { return fmt.Errorf("cannot create temp file: %w", err) } + defer func() { _ = tempFile.Close() }() defer func() { _ = os.Remove(tempFile.Name()) }() @@ -2405,6 +2420,7 @@ func generateDocumentPDF( } } else if lastQuorum.Status == coredata.DocumentVersionApprovalQuorumStatusApproved { approvedDecisions := &coredata.DocumentVersionApprovalDecisions{} + approvedFilter := coredata.NewDocumentVersionApprovalDecisionFilter( coredata.DocumentVersionApprovalDecisionStates{coredata.DocumentVersionApprovalDecisionStateApproved}, ) @@ -2450,6 +2466,7 @@ func generateDocumentPDF( } classification := docgen.ClassificationSecret + switch version.Classification { case coredata.DocumentClassificationPublic: classification = docgen.ClassificationPublic @@ -2460,8 +2477,10 @@ func generateDocumentPDF( } horizontalLogoBase64 := "" + if organization.HorizontalLogoFileID != nil { fileRecord := &coredata.File{} + fileErr := fileRecord.LoadByID(ctx, conn, scope, *organization.HorizontalLogoFileID) if fileErr == nil { base64Data, mimeType, logoErr := svc.fileManager.GetFileBase64(ctx, fileRecord) @@ -2526,6 +2545,7 @@ func generateDocumentPDF( if err != nil { return nil, fmt.Errorf("cannot add watermark to PDF: %w", err) } + return watermarkedPDF, nil } @@ -2539,6 +2559,7 @@ func (s *DocumentService) Export( options ExportPDFOptions, ) (err error) { archive := zip.NewWriter(file) + defer func() { if closeErr := archive.Close(); closeErr != nil && err == nil { err = fmt.Errorf("cannot close archive: %w", closeErr) @@ -2573,6 +2594,7 @@ func (s *DocumentService) Export( } filename := fmt.Sprintf("%d_%s.pdf", i+1, sanitizeFilename(document.Title)) + w, err := archive.Create(filename) if err != nil { return fmt.Errorf("cannot create document in archive: %w", err) @@ -2655,7 +2677,6 @@ func (s *DocumentService) GenerateDocumentExportDownloadURL( opts.Expires = documentExportEmailExpiresIn }, ) - if err != nil { return "", fmt.Errorf("cannot presign GetObject request: %w", err) } @@ -2829,6 +2850,7 @@ func (s *DocumentService) generateAndUploadPublicationPDF( ctx, func(ctx context.Context, conn pg.Querier) error { var err error + pdfData, err = exportDocumentPDF( ctx, s.svc, @@ -2838,6 +2860,7 @@ func (s *DocumentService) generateAndUploadPublicationPDF( documentVersion.ID, ExportPDFOptions{}, ) + return err }, ) @@ -2882,6 +2905,7 @@ func (s *DocumentService) generateAndUploadPublicationPDF( } documentVersion.FileID = &fileRecord.ID + documentVersion.UpdatedAt = now if err := documentVersion.Update(ctx, tx, s.svc.scope); err != nil { return fmt.Errorf("cannot update document version with file ID: %w", err) diff --git a/pkg/probo/evidence_description_worker.go b/pkg/probo/evidence_description_worker.go index 3c8922bcc..85db1a001 100644 --- a/pkg/probo/evidence_description_worker.go +++ b/pkg/probo/evidence_description_worker.go @@ -84,6 +84,7 @@ func (h *evidenceDescriptionHandler) Claim(ctx context.Context) (coredata.Eviden now := time.Now() evidence.DescriptionStatus = coredata.EvidenceDescriptionStatusProcessing evidence.DescriptionProcessingStartedAt = &now + evidence.UpdatedAt = now if err := evidence.Update(ctx, tx, coredata.NewNoScope()); err != nil { return fmt.Errorf("cannot update evidence: %w", err) @@ -95,6 +96,7 @@ func (h *evidenceDescriptionHandler) Claim(ctx context.Context) (coredata.Eviden if errors.Is(err, coredata.ErrResourceNotFound) { return coredata.Evidence{}, worker.ErrNoTask } + return coredata.Evidence{}, err } @@ -127,6 +129,7 @@ func (h *evidenceDescriptionHandler) RecoverStale(ctx context.Context) error { if err := coredata.ResetStaleDescriptionProcessing(ctx, conn, h.staleAfter); err != nil { return fmt.Errorf("cannot reset stale description processing: %w", err) } + return nil }, ) @@ -143,12 +146,14 @@ func (h *evidenceDescriptionHandler) describeAndCommit( scope := coredata.NewScopeFromObjectID(evidence.ID) var file coredata.File + if err := h.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { if err := file.LoadByID(ctx, conn, scope, *evidence.EvidenceFileId); err != nil { return fmt.Errorf("cannot load file: %w", err) } + return nil }, ); err != nil { @@ -171,6 +176,7 @@ func (h *evidenceDescriptionHandler) describeAndCommit( evidence.Description = description evidence.DescriptionStatus = coredata.EvidenceDescriptionStatusCompleted evidence.DescriptionProcessingStartedAt = nil + evidence.UpdatedAt = time.Now() if err := evidence.Update(ctx, tx, scope); err != nil { return fmt.Errorf("cannot update evidence: %w", err) @@ -192,6 +198,7 @@ func (h *evidenceDescriptionHandler) failEvidence( func(ctx context.Context, tx pg.Tx) error { evidence.DescriptionStatus = coredata.EvidenceDescriptionStatusFailed evidence.DescriptionProcessingStartedAt = nil + evidence.UpdatedAt = time.Now() if err := evidence.Update(ctx, tx, scope); err != nil { return fmt.Errorf("cannot update evidence: %w", err) diff --git a/pkg/probo/evidence_service.go b/pkg/probo/evidence_service.go index 224a8b5e2..97cadea3f 100644 --- a/pkg/probo/evidence_service.go +++ b/pkg/probo/evidence_service.go @@ -67,7 +67,6 @@ func (s EvidenceService) Get( return nil }, ) - if err != nil { return nil, fmt.Errorf("cannot load evidence: %w", err) } @@ -106,8 +105,11 @@ func (s EvidenceService) UploadMeasureEvidence( ctx, func(ctx context.Context, conn pg.Tx) error { measure := &coredata.Measure{} - var file *coredata.File - var err error + + var ( + file *coredata.File + err error + ) if err := measure.LoadByID(ctx, conn, s.svc.scope, req.MeasureID); err != nil { return fmt.Errorf("cannot load measure %q: %w", req.MeasureID, err) @@ -122,7 +124,6 @@ func (s EvidenceService) UploadMeasureEvidence( "organization-id": measure.OrganizationID.String(), }, &req.File) - if err != nil { return fmt.Errorf("cannot upload or file: %w", err) } @@ -138,7 +139,6 @@ func (s EvidenceService) UploadMeasureEvidence( return nil }, ) - if err != nil { // TODO try do delete file from s3 if it's a file type return nil, err @@ -157,6 +157,7 @@ func (s EvidenceService) CountForMeasureID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { evidences := coredata.Evidences{} + count, err = evidences.CountByMeasureID(ctx, conn, s.svc.scope, measureID) if err != nil { return fmt.Errorf("cannot count evidences: %w", err) @@ -165,7 +166,6 @@ func (s EvidenceService) CountForMeasureID( return nil }, ) - if err != nil { return 0, err } @@ -192,7 +192,6 @@ func (s EvidenceService) ListForMeasureID( ) }, ) - if err != nil { return nil, err } @@ -210,6 +209,7 @@ func (s EvidenceService) CountForTaskID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { evidences := coredata.Evidences{} + count, err = evidences.CountByTaskID(ctx, conn, s.svc.scope, taskID) if err != nil { return fmt.Errorf("cannot count evidences: %w", err) @@ -218,7 +218,6 @@ func (s EvidenceService) CountForTaskID( return nil }, ) - if err != nil { return 0, err } @@ -245,7 +244,6 @@ func (s EvidenceService) ListForTaskID( ) }, ) - if err != nil { return nil, err } diff --git a/pkg/probo/file_service.go b/pkg/probo/file_service.go index 0b1b95f9d..35e4d958f 100644 --- a/pkg/probo/file_service.go +++ b/pkg/probo/file_service.go @@ -65,7 +65,6 @@ func (s FileService) Get( return nil }, ) - if err != nil { return nil, fmt.Errorf("cannot load file: %w", err) } @@ -146,13 +145,17 @@ func (s FileService) UploadAndSaveFile( now := time.Now() fileID := gid.New(s.svc.scope.GetTenantID(), coredata.FileEntityType) + var file *coredata.File // Extract organization ID from S3 metadata organizationIDStr, hasOrgID := s3Metadata["organization-id"] + var organizationID gid.GID + if hasOrgID { var err error + organizationID, err = gid.ParseGID(organizationIDStr) if err != nil { return nil, fmt.Errorf("invalid organization-id in metadata: %w", err) @@ -182,7 +185,6 @@ func (s FileService) UploadAndSaveFile( return nil }, ) - if err != nil { return nil, err } diff --git a/pkg/probo/finding_service.go b/pkg/probo/finding_service.go index f08a70b3d..87d591252 100644 --- a/pkg/probo/finding_service.go +++ b/pkg/probo/finding_service.go @@ -114,7 +114,6 @@ func (s FindingService) Get( return finding.LoadByID(ctx, conn, s.svc.scope, findingID) }, ) - if err != nil { return nil, fmt.Errorf("cannot get finding: %w", err) } @@ -181,7 +180,6 @@ func (s *FindingService) Create( return nil }, ) - if err != nil { return nil, err } @@ -209,37 +207,48 @@ func (s *FindingService) Update( if req.Description != nil { finding.Description = *req.Description } + if req.Source != nil { finding.Source = *req.Source } + if req.IdentifiedOn != nil { finding.IdentifiedOn = *req.IdentifiedOn } + if req.RootCause != nil { finding.RootCause = *req.RootCause } + if req.CorrectiveAction != nil { finding.CorrectiveAction = *req.CorrectiveAction } + if req.OwnerID != nil { owner := &coredata.MembershipProfile{} if err := owner.LoadByID(ctx, conn, s.svc.scope, *req.OwnerID); err != nil { return fmt.Errorf("cannot load owner profile: %w", err) } + finding.OwnerID = req.OwnerID } + if req.DueDate != nil { finding.DueDate = *req.DueDate } + if req.Status != nil { finding.Status = *req.Status } + if req.Priority != nil { finding.Priority = *req.Priority } + if req.RiskID != nil { finding.RiskID = *req.RiskID } + if req.EffectivenessCheck != nil { finding.EffectivenessCheck = *req.EffectivenessCheck } @@ -257,7 +266,6 @@ func (s *FindingService) Update( return nil }, ) - if err != nil { return nil, err } @@ -270,6 +278,7 @@ func (s FindingService) Delete( findingID gid.GID, ) error { finding := coredata.Finding{ID: findingID} + return s.svc.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { @@ -277,6 +286,7 @@ func (s FindingService) Delete( if err != nil { return fmt.Errorf("cannot delete finding: %w", err) } + return nil }, ) @@ -301,7 +311,6 @@ func (s FindingService) ListForOrganizationID( return nil }, ) - if err != nil { return nil, err } @@ -320,6 +329,7 @@ func (s FindingService) CountForOrganizationID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { findings := coredata.Findings{} + count, err = findings.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, filter) if err != nil { return fmt.Errorf("cannot count findings: %w", err) @@ -328,7 +338,6 @@ func (s FindingService) CountForOrganizationID( return nil }, ) - if err != nil { return 0, err } @@ -375,7 +384,6 @@ func (s FindingService) CreateAuditMapping( return nil }, ) - if err != nil { return nil, nil, err } @@ -410,7 +418,6 @@ func (s FindingService) DeleteAuditMapping( return nil }, ) - if err != nil { return nil, nil, fmt.Errorf("cannot delete finding audit mapping: %w", err) } @@ -425,6 +432,7 @@ func (s FindingService) ListForAuditID( filter *coredata.FindingFilter, ) (*page.Page[*coredata.Finding, coredata.FindingOrderField], error) { var findings coredata.Findings + audit := &coredata.Audit{} err := s.svc.pg.WithConn( @@ -433,6 +441,7 @@ func (s FindingService) ListForAuditID( if err := audit.LoadByID(ctx, conn, s.svc.scope, auditID); err != nil { return fmt.Errorf("cannot load audit: %w", err) } + if err := findings.LoadByAuditID(ctx, conn, s.svc.scope, auditID, cursor, filter); err != nil { return fmt.Errorf("cannot load findings: %w", err) } @@ -440,7 +449,6 @@ func (s FindingService) ListForAuditID( return nil }, ) - if err != nil { return nil, err } @@ -459,6 +467,7 @@ func (s FindingService) CountForAuditID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { findings := coredata.Findings{} + count, err = findings.CountByAuditID(ctx, conn, s.svc.scope, auditID, filter) if err != nil { return fmt.Errorf("cannot count findings: %w", err) @@ -467,7 +476,6 @@ func (s FindingService) CountForAuditID( return nil }, ) - if err != nil { return 0, err } diff --git a/pkg/probo/framework_service.go b/pkg/probo/framework_service.go index 30ff7248e..46cb61d3a 100644 --- a/pkg/probo/framework_service.go +++ b/pkg/probo/framework_service.go @@ -106,6 +106,7 @@ func (s FrameworkService) RequestExport( recipientName string, ) (*coredata.ExportJob, error) { var exportJobID gid.GID + exportJob := &coredata.ExportJob{} err := s.svc.pg.WithTx(ctx, func(ctx context.Context, conn pg.Tx) error { @@ -120,6 +121,7 @@ func (s FrameworkService) RequestExport( args := coredata.FrameworkExportArguments{ FrameworkID: frameworkID, } + argsJSON, err := json.Marshal(args) if err != nil { return fmt.Errorf("cannot marshal framework export arguments: %w", err) @@ -142,7 +144,6 @@ func (s FrameworkService) RequestExport( return nil }) - if err != nil { return nil, err } @@ -156,6 +157,7 @@ func (s FrameworkService) Export( file io.Writer, ) error { archive := zip.NewWriter(file) + defer func() { _ = archive.Close() }() return s.svc.pg.WithTx( @@ -167,6 +169,7 @@ func (s FrameworkService) Export( } controls := coredata.Controls{} + err := controls.LoadByFrameworkID( ctx, conn, @@ -194,6 +197,7 @@ func (s FrameworkService) Export( } measures := coredata.Measures{} + err = measures.LoadByControlID( ctx, conn, @@ -221,6 +225,7 @@ func (s FrameworkService) Export( } evidences := coredata.Evidences{} + err = evidences.LoadByMeasureID( ctx, conn, @@ -262,6 +267,7 @@ func (s FrameworkService) Export( if err != nil { return fmt.Errorf("cannot download evidence: %w", err) } + defer func() { _ = object.Body.Close() }() w, err := archive.Create(fmt.Sprintf("%s/%s/%s/%s", framework.Name, control.SectionTitle, measure.Name, evidence_file.FileName)) @@ -277,6 +283,7 @@ func (s FrameworkService) Export( } documents := coredata.Documents{} + err = documents.LoadByControlID( ctx, conn, @@ -366,7 +373,6 @@ func (s FrameworkService) Create( return nil }) - if err != nil { return nil, err } @@ -382,13 +388,14 @@ func (s FrameworkService) CountForOrganizationID( err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) (err error) { frameworks := &coredata.Frameworks{} + count, err = frameworks.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID) if err != nil { return fmt.Errorf("cannot count frameworks: %w", err) } + return nil }) - if err != nil { return 0, fmt.Errorf("cannot count frameworks: %w", err) } @@ -402,6 +409,7 @@ func (s FrameworkService) ListForOrganizationID( cursor *page.Cursor[coredata.FrameworkOrderField], ) (*page.Page[*coredata.Framework, coredata.FrameworkOrderField], error) { var frameworks coredata.Frameworks + organization := &coredata.Organization{} err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { @@ -422,7 +430,6 @@ func (s FrameworkService) ListForOrganizationID( return nil }) - if err != nil { return nil, err } @@ -439,7 +446,6 @@ func (s FrameworkService) Get( err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { return framework.LoadByID(ctx, conn, s.svc.scope, frameworkID) }) - if err != nil { return nil, err } @@ -524,6 +530,7 @@ func (s FrameworkService) Import( req ImportFrameworkRequest, ) (*coredata.Framework, error) { var framework *coredata.Framework + frameworkID := gid.New(organizationID.TenantID(), coredata.FrameworkEntityType) now := time.Now() @@ -548,6 +555,7 @@ func (s FrameworkService) Import( "dark": req.Framework.Logo.Dark, } { fileID := gid.New(s.svc.scope.GetTenantID(), coredata.FileEntityType) + objectKey, err := uuid.NewV7() if err != nil { return fmt.Errorf("cannot generate object key: %w", err) @@ -600,21 +608,26 @@ func (s FrameworkService) Import( now := time.Now() description := control.Description + bestPractice := true if control.BestPractice != nil { bestPractice = *control.BestPractice } + maturityLevel := coredata.ControlMaturityLevelInitial + if control.MaturityLevel != nil { ml := coredata.ControlMaturityLevel(*control.MaturityLevel) if ml.IsValid() { maturityLevel = ml } } + var notImplementedJustification *string if maturityLevel == coredata.ControlMaturityLevelNone { notImplementedJustification = control.NotImplementedJustification } + control := &coredata.Control{ ID: controlID, FrameworkID: frameworkID, @@ -636,7 +649,6 @@ func (s FrameworkService) Import( return nil }) - if err != nil { return nil, err } @@ -710,7 +722,6 @@ func (s FrameworkService) GenerateFrameworkExportDownloadURL( opts.Expires = frameworkExportEmailExpiresIn }, ) - if err != nil { return "", fmt.Errorf("cannot presign GetObject request: %w", err) } @@ -720,6 +731,7 @@ func (s FrameworkService) GenerateFrameworkExportDownloadURL( func (s *FrameworkService) BuildAndUploadExport(ctx context.Context, exportJobID gid.GID) (*coredata.ExportJob, error) { exportJob := &coredata.ExportJob{} + err := s.svc.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { @@ -738,10 +750,12 @@ func (s *FrameworkService) BuildAndUploadExport(ctx context.Context, exportJobID } tempDir := os.TempDir() + tempFile, err := os.CreateTemp(tempDir, "probo-framework-export-*.zip") if err != nil { return fmt.Errorf("cannot create temp file: %w", err) } + defer func() { _ = tempFile.Close() }() defer func() { _ = os.Remove(tempFile.Name()) }() diff --git a/pkg/probo/generated_document_service.go b/pkg/probo/generated_document_service.go index dc8f4901d..e9137bca7 100644 --- a/pkg/probo/generated_document_service.go +++ b/pkg/probo/generated_document_service.go @@ -66,8 +66,10 @@ func (s *GeneratedDocumentService) PublishStatementOfApplicability( now := time.Now() var existingDoc *coredata.Document + if soa.DocumentID != nil { doc := &coredata.Document{} + err = doc.LoadByID(ctx, tx, s.svc.scope, *soa.DocumentID) if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) { return fmt.Errorf("cannot load statement of applicability document: %w", err) @@ -77,6 +79,7 @@ func (s *GeneratedDocumentService) PublishStatementOfApplicability( existingDoc = doc } else { soa.DocumentID = nil + soa.UpdatedAt = now if err := soa.Update(ctx, tx, s.svc.scope); err != nil { return fmt.Errorf("cannot clear document reference: %w", err) @@ -102,6 +105,7 @@ func (s *GeneratedDocumentService) PublishStatementOfApplicability( } soa.DocumentID = &documentID + soa.UpdatedAt = now if err := soa.Update(ctx, tx, s.svc.scope); err != nil { return fmt.Errorf("cannot update document reference: %w", err) @@ -127,7 +131,6 @@ func (s *GeneratedDocumentService) PublishStatementOfApplicability( return s.publishOrRequestApproval(ctx, tx, document, documentVersion, soa.OrganizationID, approverIDs, minor, now) }, ) - if err != nil { return nil, nil, err } @@ -171,6 +174,7 @@ func (s *GeneratedDocumentService) buildStatementOfApplicabilityDocumentData( controlMap := make(map[gid.GID]*coredata.Control, len(controls)) frameworkIDSet := make(map[gid.GID]struct{}) + for _, c := range controls { controlMap[c.ID] = c frameworkIDSet[c.FrameworkID] = struct{}{} @@ -200,6 +204,7 @@ func (s *GeneratedDocumentService) buildStatementOfApplicabilityDocumentData( controlID gid.GID oblType coredata.ObligationType } + oblSet := make(map[obligationKey]struct{}, len(controlOblTypes)) for _, co := range controlOblTypes { oblSet[obligationKey{co.ControlID, co.ObligationType}] = struct{}{} @@ -222,6 +227,7 @@ func (s *GeneratedDocumentService) buildStatementOfApplicabilityDocumentData( if control == nil { continue } + framework := frameworkMap[control.FrameworkID] if framework == nil { continue @@ -243,6 +249,7 @@ func (s *GeneratedDocumentService) buildStatementOfApplicabilityDocumentData( contractual := "-" bestPractice := "-" riskAssessment := "-" + if applicable { _, hasLegal := oblSet[obligationKey{stmt.ControlID, coredata.ObligationTypeLegal}] regulatory = docgen.BoolLabel(hasLegal) @@ -314,14 +321,17 @@ func (s *GeneratedDocumentService) PublishDataList( now := time.Now() datum := coredata.Datum{} + dataDocumentID, err := datum.GetGeneratedDocumentID(ctx, tx, organizationID) if err != nil { return fmt.Errorf("cannot query generated documents: %w", err) } var existingDoc *coredata.Document + if dataDocumentID != nil { doc := &coredata.Document{} + err = doc.LoadByID(ctx, tx, s.svc.scope, *dataDocumentID) if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) { return fmt.Errorf("cannot load data list document: %w", err) @@ -377,7 +387,6 @@ func (s *GeneratedDocumentService) PublishDataList( return s.publishOrRequestApproval(ctx, tx, document, documentVersion, organizationID, approverIDs, minor, now) }, ) - if err != nil { return nil, nil, err } @@ -393,8 +402,11 @@ func (s *GeneratedDocumentService) GetDataListDocumentID( err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { datum := coredata.Datum{} + var err error + dataDocumentID, err = datum.GetGeneratedDocumentID(ctx, conn, organizationID) + return err }) if err != nil { @@ -424,6 +436,7 @@ func (s *GeneratedDocumentService) buildDataListDocumentData( } ownerIDs := make([]gid.GID, 0, len(data)) + ownerIDSet := make(map[gid.GID]struct{}) for _, d := range data { if _, ok := ownerIDSet[d.OwnerID]; !ok { @@ -504,6 +517,7 @@ var dataListTemplate = template.Must( if err != nil { return "", err } + return string(b), nil }, }). @@ -515,6 +529,7 @@ func BuildDataListDocument(data docgen.DataListData) (string, error) { if err := dataListTemplate.Execute(&buf, data); err != nil { return "", fmt.Errorf("cannot execute data list template: %w", err) } + return buf.String(), nil } @@ -550,14 +565,17 @@ func (s *GeneratedDocumentService) PublishAssetList( now := time.Now() asset := coredata.Asset{} + assetDocumentID, err := asset.GetGeneratedDocumentID(ctx, tx, organizationID) if err != nil { return fmt.Errorf("cannot query generated documents: %w", err) } var existingDoc *coredata.Document + if assetDocumentID != nil { doc := &coredata.Document{} + err = doc.LoadByID(ctx, tx, s.svc.scope, *assetDocumentID) if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) { return fmt.Errorf("cannot load asset list document: %w", err) @@ -613,7 +631,6 @@ func (s *GeneratedDocumentService) PublishAssetList( return s.publishOrRequestApproval(ctx, tx, document, documentVersion, organizationID, approverIDs, minor, now) }, ) - if err != nil { return nil, nil, err } @@ -629,8 +646,11 @@ func (s *GeneratedDocumentService) GetAssetListDocumentID( err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { asset := coredata.Asset{} + var err error + assetDocumentID, err = asset.GetGeneratedDocumentID(ctx, conn, organizationID) + return err }) if err != nil { @@ -660,6 +680,7 @@ func (s *GeneratedDocumentService) buildAssetListDocumentData( } ownerIDs := make([]gid.GID, 0, len(assets)) + ownerIDSet := make(map[gid.GID]struct{}) for _, a := range assets { if _, ok := ownerIDSet[a.OwnerID]; !ok { @@ -738,6 +759,7 @@ var assetListTemplate = template.Must( if err != nil { return "", err } + return string(b), nil }, "printf": fmt.Sprintf, @@ -750,6 +772,7 @@ func BuildAssetListDocument(data docgen.AssetListData) (string, error) { if err := assetListTemplate.Execute(&buf, data); err != nil { return "", fmt.Errorf("cannot execute asset list template: %w", err) } + return buf.String(), nil } @@ -761,6 +784,7 @@ var soaTemplate = template.Must( if err != nil { return "", err } + return string(b), nil }, }). @@ -772,6 +796,7 @@ func BuildStatementOfApplicabilityDocument(data docgen.StatementOfApplicabilityD if err := soaTemplate.Execute(&buf, data); err != nil { return "", fmt.Errorf("cannot execute soa template: %w", err) } + return buf.String(), nil } @@ -807,14 +832,17 @@ func (s *GeneratedDocumentService) PublishFindingList( now := time.Now() finding := coredata.Finding{} + findingDocumentID, err := finding.GetGeneratedDocumentID(ctx, tx, organizationID) if err != nil { return fmt.Errorf("cannot query generated documents: %w", err) } var existingDoc *coredata.Document + if findingDocumentID != nil { doc := &coredata.Document{} + err = doc.LoadByID(ctx, tx, s.svc.scope, *findingDocumentID) if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) { return fmt.Errorf("cannot load finding list document: %w", err) @@ -870,7 +898,6 @@ func (s *GeneratedDocumentService) PublishFindingList( return s.publishOrRequestApproval(ctx, tx, document, documentVersion, organizationID, approverIDs, minor, now) }, ) - if err != nil { return nil, nil, err } @@ -886,8 +913,11 @@ func (s *GeneratedDocumentService) GetFindingsDocumentID( err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { finding := coredata.Finding{} + var err error + findingDocumentID, err = finding.GetGeneratedDocumentID(ctx, conn, organizationID) + return err }) if err != nil { @@ -918,6 +948,7 @@ func (s *GeneratedDocumentService) buildFindingListDocumentData( ownerIDs := make([]gid.GID, 0, len(findings)) ownerIDSet := make(map[gid.GID]struct{}) + for _, f := range findings { if f.OwnerID != nil { if _, ok := ownerIDSet[*f.OwnerID]; !ok { @@ -928,6 +959,7 @@ func (s *GeneratedDocumentService) buildFindingListDocumentData( } profileMap := make(map[gid.GID]*coredata.MembershipProfile) + if len(ownerIDs) > 0 { var profiles coredata.MembershipProfiles if err := profiles.LoadByIDs(ctx, conn, s.svc.scope, ownerIDs); err != nil { @@ -942,6 +974,7 @@ func (s *GeneratedDocumentService) buildFindingListDocumentData( rows := make([]docgen.FindingListRow, 0, len(findings)) for _, f := range findings { ownerName := "-" + if f.OwnerID != nil { if p, ok := profileMap[*f.OwnerID]; ok && p.FullName != "" { ownerName = p.FullName @@ -1063,6 +1096,7 @@ var findingListTemplate = template.Must( if err != nil { return "", err } + return string(b), nil }, }). @@ -1074,6 +1108,7 @@ func BuildFindingListDocument(data docgen.FindingListData) (string, error) { if err := findingListTemplate.Execute(&buf, data); err != nil { return "", fmt.Errorf("cannot execute finding list template: %w", err) } + return buf.String(), nil } @@ -1109,14 +1144,17 @@ func (s *GeneratedDocumentService) PublishObligationList( now := time.Now() obligation := coredata.Obligation{} + obligationDocumentID, err := obligation.GetGeneratedDocumentID(ctx, tx, organizationID) if err != nil { return fmt.Errorf("cannot query generated documents: %w", err) } var existingDoc *coredata.Document + if obligationDocumentID != nil { doc := &coredata.Document{} + err = doc.LoadByID(ctx, tx, s.svc.scope, *obligationDocumentID) if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) { return fmt.Errorf("cannot load obligation list document: %w", err) @@ -1172,7 +1210,6 @@ func (s *GeneratedDocumentService) PublishObligationList( return s.publishOrRequestApproval(ctx, tx, document, documentVersion, organizationID, approverIDs, minor, now) }, ) - if err != nil { return nil, nil, err } @@ -1188,8 +1225,11 @@ func (s *GeneratedDocumentService) GetObligationsDocumentID( err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { obligation := coredata.Obligation{} + var err error + obligationDocumentID, err = obligation.GetGeneratedDocumentID(ctx, conn, organizationID) + return err }) if err != nil { @@ -1220,10 +1260,12 @@ func (s *GeneratedDocumentService) buildObligationListDocumentData( ownerIDs := make([]gid.GID, 0, len(obligations)) ownerIDSet := make(map[gid.GID]struct{}) + for _, o := range obligations { if o.OwnerID == gid.Nil { continue } + if _, ok := ownerIDSet[o.OwnerID]; !ok { ownerIDs = append(ownerIDs, o.OwnerID) ownerIDSet[o.OwnerID] = struct{}{} @@ -1231,6 +1273,7 @@ func (s *GeneratedDocumentService) buildObligationListDocumentData( } profileMap := make(map[gid.GID]*coredata.MembershipProfile) + if len(ownerIDs) > 0 { var profiles coredata.MembershipProfiles if err := profiles.LoadByIDs(ctx, conn, s.svc.scope, ownerIDs); err != nil { @@ -1333,6 +1376,7 @@ var obligationListTemplate = template.Must( if err != nil { return "", err } + return string(b), nil }, }). @@ -1344,6 +1388,7 @@ func BuildObligationListDocument(data docgen.ObligationListData) (string, error) if err := obligationListTemplate.Execute(&buf, data); err != nil { return "", fmt.Errorf("cannot execute obligation list template: %w", err) } + return buf.String(), nil } @@ -1379,14 +1424,17 @@ func (s *GeneratedDocumentService) PublishProcessingActivityList( now := time.Now() processingActivity := coredata.ProcessingActivity{} + processingActivityDocumentID, err := processingActivity.GetGeneratedDocumentID(ctx, tx, organizationID) if err != nil { return fmt.Errorf("cannot query generated documents: %w", err) } var existingDoc *coredata.Document + if processingActivityDocumentID != nil { doc := &coredata.Document{} + err = doc.LoadByID(ctx, tx, s.svc.scope, *processingActivityDocumentID) if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) { return fmt.Errorf("cannot load processing activity list document: %w", err) @@ -1442,7 +1490,6 @@ func (s *GeneratedDocumentService) PublishProcessingActivityList( return s.publishOrRequestApproval(ctx, tx, document, documentVersion, organizationID, approverIDs, minor, now) }, ) - if err != nil { return nil, nil, err } @@ -1458,8 +1505,11 @@ func (s *GeneratedDocumentService) GetProcessingActivitiesDocumentID( err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { processingActivity := coredata.ProcessingActivity{} + var err error + documentID, err = processingActivity.GetGeneratedDocumentID(ctx, conn, organizationID) + return err }) if err != nil { @@ -1489,6 +1539,7 @@ func (s *GeneratedDocumentService) buildProcessingActivityListDocumentData( } var thirdParties coredata.ThirdParties + thirdPartyMap, err := thirdParties.LoadAllByProcessingActivities(ctx, conn, s.svc.scope, organization.ID) if err != nil { return docgen.ProcessingActivityListData{}, fmt.Errorf("cannot load thirdParties: %w", err) @@ -1496,6 +1547,7 @@ func (s *GeneratedDocumentService) buildProcessingActivityListDocumentData( dpoIDs := make([]gid.GID, 0, len(processingActivities)) dpoIDSet := make(map[gid.GID]struct{}) + for _, pa := range processingActivities { if pa.DataProtectionOfficerID != nil { if _, ok := dpoIDSet[*pa.DataProtectionOfficerID]; !ok { @@ -1506,6 +1558,7 @@ func (s *GeneratedDocumentService) buildProcessingActivityListDocumentData( } dpoMap := make(map[gid.GID]*coredata.MembershipProfile) + if len(dpoIDs) > 0 { var profiles coredata.MembershipProfiles if err := profiles.LoadByIDs(ctx, conn, s.svc.scope, dpoIDs); err != nil { @@ -1520,6 +1573,7 @@ func (s *GeneratedDocumentService) buildProcessingActivityListDocumentData( rows := make([]docgen.ProcessingActivityListRow, 0, len(processingActivities)) for _, pa := range processingActivities { dpoName := "Not assigned" + if pa.DataProtectionOfficerID != nil { if p, ok := dpoMap[*pa.DataProtectionOfficerID]; ok && p.FullName != "" { dpoName = p.FullName @@ -1568,6 +1622,7 @@ func derefStringOrNotSpecified(s *string) string { if s == nil || *s == "" { return "Not specified" } + return *s } @@ -1575,6 +1630,7 @@ func formatDateOrNotSpecified(t *time.Time) string { if t == nil { return "Not specified" } + return t.Format("January 2, 2006") } @@ -1582,6 +1638,7 @@ func yesNoLabel(b bool) string { if b { return "Yes" } + return "No" } @@ -1632,6 +1689,7 @@ func formatTransferSafeguard(safeguard *coredata.ProcessingActivityTransferSafeg if safeguard == nil { return "Not specified" } + switch *safeguard { case coredata.ProcessingActivityTransferSafeguardStandardContractualClauses: return "Standard Contractual Clauses" @@ -1676,6 +1734,7 @@ func formatResidualRisk(risk *coredata.DataProtectionImpactAssessmentResidualRis if risk == nil { return "Not specified" } + switch *risk { case coredata.DataProtectionImpactAssessmentResidualRiskLow: return "Low" @@ -1696,6 +1755,7 @@ var processingActivityListTemplate = template.Must( if err != nil { return "", err } + return string(b), nil }, "printf": fmt.Sprintf, @@ -1709,6 +1769,7 @@ func BuildProcessingActivityListDocument(data docgen.ProcessingActivityListData) if err := processingActivityListTemplate.Execute(&buf, data); err != nil { return "", fmt.Errorf("cannot execute processing activity list template: %w", err) } + return buf.String(), nil } @@ -1744,14 +1805,17 @@ func (s *GeneratedDocumentService) PublishDataProtectionImpactAssessmentList( now := time.Now() dpia := coredata.DataProtectionImpactAssessment{} + dpiaDocumentID, err := dpia.GetGeneratedDocumentID(ctx, tx, organizationID) if err != nil { return fmt.Errorf("cannot query generated documents: %w", err) } var existingDoc *coredata.Document + if dpiaDocumentID != nil { doc := &coredata.Document{} + err = doc.LoadByID(ctx, tx, s.svc.scope, *dpiaDocumentID) if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) { return fmt.Errorf("cannot load DPIA list document: %w", err) @@ -1807,7 +1871,6 @@ func (s *GeneratedDocumentService) PublishDataProtectionImpactAssessmentList( return s.publishOrRequestApproval(ctx, tx, document, documentVersion, organizationID, approverIDs, minor, now) }, ) - if err != nil { return nil, nil, err } @@ -1823,8 +1886,11 @@ func (s *GeneratedDocumentService) GetDataProtectionImpactAssessmentsDocumentID( err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { dpia := coredata.DataProtectionImpactAssessment{} + var err error + documentID, err = dpia.GetGeneratedDocumentID(ctx, conn, organizationID) + return err }) if err != nil { @@ -1854,6 +1920,7 @@ func (s *GeneratedDocumentService) buildDataProtectionImpactAssessmentListDocume } processingActivityIDs := make([]gid.GID, 0, len(assessments)) + processingActivityIDSet := make(map[gid.GID]struct{}, len(assessments)) for _, a := range assessments { if _, ok := processingActivityIDSet[a.ProcessingActivityID]; !ok { @@ -1906,6 +1973,7 @@ var dataProtectionImpactAssessmentListTemplate = template.Must( if err != nil { return "", err } + return string(b), nil }, "printf": fmt.Sprintf, @@ -1919,6 +1987,7 @@ func BuildDataProtectionImpactAssessmentListDocument(data docgen.DataProtectionI if err := dataProtectionImpactAssessmentListTemplate.Execute(&buf, data); err != nil { return "", fmt.Errorf("cannot execute DPIA list template: %w", err) } + return buf.String(), nil } @@ -1954,14 +2023,17 @@ func (s *GeneratedDocumentService) PublishTransferImpactAssessmentList( now := time.Now() tia := coredata.TransferImpactAssessment{} + tiaDocumentID, err := tia.GetGeneratedDocumentID(ctx, tx, organizationID) if err != nil { return fmt.Errorf("cannot query generated documents: %w", err) } var existingDoc *coredata.Document + if tiaDocumentID != nil { doc := &coredata.Document{} + err = doc.LoadByID(ctx, tx, s.svc.scope, *tiaDocumentID) if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) { return fmt.Errorf("cannot load TIA list document: %w", err) @@ -2017,7 +2089,6 @@ func (s *GeneratedDocumentService) PublishTransferImpactAssessmentList( return s.publishOrRequestApproval(ctx, tx, document, documentVersion, organizationID, approverIDs, minor, now) }, ) - if err != nil { return nil, nil, err } @@ -2033,8 +2104,11 @@ func (s *GeneratedDocumentService) GetTransferImpactAssessmentsDocumentID( err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { tia := coredata.TransferImpactAssessment{} + var err error + documentID, err = tia.GetGeneratedDocumentID(ctx, conn, organizationID) + return err }) if err != nil { @@ -2064,6 +2138,7 @@ func (s *GeneratedDocumentService) buildTransferImpactAssessmentListDocumentData } processingActivityIDs := make([]gid.GID, 0, len(assessments)) + processingActivityIDSet := make(map[gid.GID]struct{}, len(assessments)) for _, a := range assessments { if _, ok := processingActivityIDSet[a.ProcessingActivityID]; !ok { @@ -2116,6 +2191,7 @@ var transferImpactAssessmentListTemplate = template.Must( if err != nil { return "", err } + return string(b), nil }, "printf": fmt.Sprintf, @@ -2129,6 +2205,7 @@ func BuildTransferImpactAssessmentListDocument(data docgen.TransferImpactAssessm if err := transferImpactAssessmentListTemplate.Execute(&buf, data); err != nil { return "", fmt.Errorf("cannot execute TIA list template: %w", err) } + return buf.String(), nil } @@ -2143,6 +2220,7 @@ func (s *GeneratedDocumentService) PublishThirdPartyList( // JSON template rendering are slow enough that holding write locks across // them would needlessly block other writers. var documentData docgen.ThirdPartyListData + err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { organization := &coredata.Organization{} if err := organization.LoadByID(ctx, conn, s.svc.scope, organizationID); err != nil { @@ -2150,10 +2228,12 @@ func (s *GeneratedDocumentService) PublishThirdPartyList( } var err error + documentData, err = s.buildThirdPartyListDocumentData(ctx, conn, organization) if err != nil { return fmt.Errorf("cannot build document data: %w", err) } + return nil }) if err != nil { @@ -2177,14 +2257,17 @@ func (s *GeneratedDocumentService) PublishThirdPartyList( now := time.Now() thirdParty := coredata.ThirdParty{} + thirdPartyDocumentID, err := thirdParty.GetGeneratedDocumentID(ctx, tx, organizationID) if err != nil { return fmt.Errorf("cannot query generated documents: %w", err) } var existingDoc *coredata.Document + if thirdPartyDocumentID != nil { doc := &coredata.Document{} + err = doc.LoadByID(ctx, tx, s.svc.scope, *thirdPartyDocumentID) if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) { return fmt.Errorf("cannot load thirdParty list document: %w", err) @@ -2240,7 +2323,6 @@ func (s *GeneratedDocumentService) PublishThirdPartyList( return s.publishOrRequestApproval(ctx, tx, document, documentVersion, organizationID, approverIDs, minor, now) }, ) - if err != nil { return nil, nil, err } @@ -2256,8 +2338,11 @@ func (s *GeneratedDocumentService) GetThirdPartiesDocumentID( err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { thirdParty := coredata.ThirdParty{} + var err error + documentID, err = thirdParty.GetGeneratedDocumentID(ctx, conn, organizationID) + return err }) if err != nil { @@ -2288,6 +2373,7 @@ func (s *GeneratedDocumentService) buildThirdPartyListDocumentData( ownerIDSet := make(map[gid.GID]struct{}) ownerIDs := make([]gid.GID, 0) + for _, v := range thirdParties { if v.BusinessOwnerID != nil { if _, ok := ownerIDSet[*v.BusinessOwnerID]; !ok { @@ -2295,6 +2381,7 @@ func (s *GeneratedDocumentService) buildThirdPartyListDocumentData( ownerIDSet[*v.BusinessOwnerID] = struct{}{} } } + if v.SecurityOwnerID != nil { if _, ok := ownerIDSet[*v.SecurityOwnerID]; !ok { ownerIDs = append(ownerIDs, *v.SecurityOwnerID) @@ -2304,11 +2391,13 @@ func (s *GeneratedDocumentService) buildThirdPartyListDocumentData( } profileMap := make(map[gid.GID]*coredata.MembershipProfile) + if len(ownerIDs) > 0 { var profiles coredata.MembershipProfiles if err := profiles.LoadByIDs(ctx, conn, s.svc.scope, ownerIDs); err != nil { return docgen.ThirdPartyListData{}, fmt.Errorf("cannot load owner profiles: %w", err) } + for _, p := range profiles { profileMap[p.ID] = p } @@ -2323,6 +2412,7 @@ func (s *GeneratedDocumentService) buildThirdPartyListDocumentData( if err := allServices.LoadByThirdPartyIDs(ctx, conn, s.svc.scope, thirdPartyIDs); err != nil { return docgen.ThirdPartyListData{}, fmt.Errorf("cannot load thirdParty services: %w", err) } + servicesByThirdParty := make(map[gid.GID]coredata.ThirdPartyServices, len(thirdParties)) for _, vs := range allServices { servicesByThirdParty[vs.ThirdPartyID] = append(servicesByThirdParty[vs.ThirdPartyID], vs) @@ -2332,6 +2422,7 @@ func (s *GeneratedDocumentService) buildThirdPartyListDocumentData( if err := allContacts.LoadByThirdPartyIDs(ctx, conn, s.svc.scope, thirdPartyIDs); err != nil { return docgen.ThirdPartyListData{}, fmt.Errorf("cannot load thirdParty contacts: %w", err) } + contactsByThirdParty := make(map[gid.GID]coredata.ThirdPartyContacts, len(thirdParties)) for _, c := range allContacts { contactsByThirdParty[c.ThirdPartyID] = append(contactsByThirdParty[c.ThirdPartyID], c) @@ -2341,6 +2432,7 @@ func (s *GeneratedDocumentService) buildThirdPartyListDocumentData( if err := allAssessments.LoadByThirdPartyIDs(ctx, conn, s.svc.scope, thirdPartyIDs); err != nil { return docgen.ThirdPartyListData{}, fmt.Errorf("cannot load thirdParty risk assessments: %w", err) } + assessmentsByThirdParty := make(map[gid.GID]coredata.ThirdPartyRiskAssessments, len(thirdParties)) for _, ra := range allAssessments { assessmentsByThirdParty[ra.ThirdPartyID] = append(assessmentsByThirdParty[ra.ThirdPartyID], ra) @@ -2350,6 +2442,7 @@ func (s *GeneratedDocumentService) buildThirdPartyListDocumentData( if err := allReports.LoadByThirdPartyIDs(ctx, conn, s.svc.scope, thirdPartyIDs); err != nil { return docgen.ThirdPartyListData{}, fmt.Errorf("cannot load thirdParty compliance reports: %w", err) } + reportsByThirdParty := make(map[gid.GID]coredata.ThirdPartyComplianceReports, len(thirdParties)) for _, r := range allReports { reportsByThirdParty[r.ThirdPartyID] = append(reportsByThirdParty[r.ThirdPartyID], r) @@ -2359,6 +2452,7 @@ func (s *GeneratedDocumentService) buildThirdPartyListDocumentData( if err := allBAAs.LoadByThirdPartyIDs(ctx, conn, s.svc.scope, thirdPartyIDs); err != nil { return docgen.ThirdPartyListData{}, fmt.Errorf("cannot load thirdParty business associate agreements: %w", err) } + baaByThirdParty := make(map[gid.GID]*coredata.ThirdPartyBusinessAssociateAgreement, len(allBAAs)) for _, b := range allBAAs { baaByThirdParty[b.ThirdPartyID] = b @@ -2368,6 +2462,7 @@ func (s *GeneratedDocumentService) buildThirdPartyListDocumentData( if err := allDPAs.LoadByThirdPartyIDs(ctx, conn, s.svc.scope, thirdPartyIDs); err != nil { return docgen.ThirdPartyListData{}, fmt.Errorf("cannot load thirdParty data privacy agreements: %w", err) } + dpaByThirdParty := make(map[gid.GID]*coredata.ThirdPartyDataPrivacyAgreement, len(allDPAs)) for _, d := range allDPAs { dpaByThirdParty[d.ThirdPartyID] = d @@ -2409,6 +2504,7 @@ func (s *GeneratedDocumentService) buildThirdPartyListDocumentData( if c.Email != nil { email = c.Email.String() } + row.Contacts = append(row.Contacts, docgen.ThirdPartyListContact{ FullName: derefStringOrNotSpecified(c.FullName), Email: stringOrNotSpecified(email), @@ -2465,6 +2561,7 @@ func stringOrNotSpecified(s string) string { if s == "" { return "Not specified" } + return s } @@ -2472,6 +2569,7 @@ func formatTimeOrNotSpecified(t *time.Time) string { if t == nil { return "Not specified" } + return t.Format("2006-01-02") } @@ -2479,6 +2577,7 @@ func joinOrNotSpecified(items []string) string { if len(items) == 0 { return "Not specified" } + return strings.Join(items, ", ") } @@ -2486,10 +2585,12 @@ func formatCountries(c coredata.CountryCodes) string { if len(c) == 0 { return "Not specified" } + parts := make([]string, len(c)) for i, cc := range c { parts[i] = string(cc) } + return strings.Join(parts, ", ") } @@ -2497,9 +2598,11 @@ func lookupProfileName(profiles map[gid.GID]*coredata.MembershipProfile, id *gid if id == nil { return "Not assigned" } + if p, ok := profiles[*id]; ok && p.FullName != "" { return p.FullName } + return "Not assigned" } @@ -2594,6 +2697,7 @@ var thirdPartyListTemplate = template.Must( if err != nil { return "", err } + return string(b), nil }, "printf": fmt.Sprintf, @@ -2607,6 +2711,7 @@ func BuildThirdPartyListDocument(data docgen.ThirdPartyListData) (string, error) if err := thirdPartyListTemplate.Execute(&buf, data); err != nil { return "", fmt.Errorf("cannot execute thirdParty list template: %w", err) } + return buf.String(), nil } @@ -2618,6 +2723,7 @@ var riskListTemplate = template.Must( if err != nil { return "", err } + return string(b), nil }, "printf": fmt.Sprintf, @@ -2631,6 +2737,7 @@ func BuildRiskListDocument(data docgen.RiskListData) (string, error) { if err := riskListTemplate.Execute(&buf, data); err != nil { return "", fmt.Errorf("cannot execute risk list template: %w", err) } + return buf.String(), nil } @@ -2666,14 +2773,17 @@ func (s *GeneratedDocumentService) PublishRiskList( now := time.Now() risk := coredata.Risk{} + riskDocumentID, err := risk.GetGeneratedDocumentID(ctx, tx, organizationID) if err != nil { return fmt.Errorf("cannot query generated documents: %w", err) } var existingDoc *coredata.Document + if riskDocumentID != nil { doc := &coredata.Document{} + err = doc.LoadByID(ctx, tx, s.svc.scope, *riskDocumentID) if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) { return fmt.Errorf("cannot load risk list document: %w", err) @@ -2729,7 +2839,6 @@ func (s *GeneratedDocumentService) PublishRiskList( return s.publishOrRequestApproval(ctx, tx, document, documentVersion, organizationID, approverIDs, minor, now) }, ) - if err != nil { return nil, nil, err } @@ -2745,8 +2854,11 @@ func (s *GeneratedDocumentService) GetRisksDocumentID( err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { risk := coredata.Risk{} + var err error + riskDocumentID, err = risk.GetGeneratedDocumentID(ctx, conn, organizationID) + return err }) if err != nil { @@ -2777,6 +2889,7 @@ func (s *GeneratedDocumentService) buildRiskListDocumentData( ownerIDs := make([]gid.GID, 0, len(risks)) ownerIDSet := make(map[gid.GID]struct{}) + for _, r := range risks { if r.OwnerID != nil { if _, ok := ownerIDSet[*r.OwnerID]; !ok { @@ -2787,6 +2900,7 @@ func (s *GeneratedDocumentService) buildRiskListDocumentData( } profileMap := make(map[gid.GID]*coredata.MembershipProfile) + if len(ownerIDs) > 0 { var profiles coredata.MembershipProfiles if err := profiles.LoadByIDs(ctx, conn, s.svc.scope, ownerIDs); err != nil { @@ -2910,6 +3024,7 @@ func (s *GeneratedDocumentService) publishOrRequestApproval( now time.Time, ) error { previousVersion := &coredata.DocumentVersion{} + err := previousVersion.LoadLatestVersion(ctx, tx, s.svc.scope, document.ID) switch { case err == nil: @@ -2926,6 +3041,7 @@ func (s *GeneratedDocumentService) publishOrRequestApproval( if document.CurrentPublishedMajor == nil || document.CurrentPublishedMinor == nil { return &ErrCannotPublishMinorWithoutMajor{} } + version.Major = *document.CurrentPublishedMajor version.Minor = *document.CurrentPublishedMinor + 1 version.Status = coredata.DocumentVersionStatusPublished @@ -2937,6 +3053,7 @@ func (s *GeneratedDocumentService) publishOrRequestApproval( } else { version.Major = 1 } + version.Minor = 0 if len(approverIDs) > 0 { version.Status = coredata.DocumentVersionStatusDraft @@ -2958,6 +3075,7 @@ func (s *GeneratedDocumentService) publishOrRequestApproval( return fmt.Errorf("a version already exists at this number: %w", err) } } + return fmt.Errorf("cannot insert document version: %w", err) } @@ -2966,9 +3084,11 @@ func (s *GeneratedDocumentService) publishOrRequestApproval( if err := defaultApprovers.MergeByDocumentID(ctx, tx, s.svc.scope, document.ID, organizationID, approverIDs); err != nil { return fmt.Errorf("cannot save default approvers: %w", err) } + if _, err := s.svc.DocumentApprovals.RequestApprovalInTx(ctx, tx, document, version, approverIDs, nil); err != nil { return fmt.Errorf("cannot request approval: %w", err) } + return nil } @@ -2979,5 +3099,6 @@ func (s *GeneratedDocumentService) publishOrRequestApproval( if err := document.Update(ctx, tx, s.svc.scope); err != nil { return fmt.Errorf("cannot update document: %w", err) } + return nil } diff --git a/pkg/probo/measure_service.go b/pkg/probo/measure_service.go index 8bc9f493c..8a7303c29 100644 --- a/pkg/probo/measure_service.go +++ b/pkg/probo/measure_service.go @@ -104,6 +104,7 @@ func (s MeasureService) CountForRiskID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { measures := &coredata.Measures{} + count, err = measures.CountByRiskID(ctx, conn, s.svc.scope, riskID, filter) if err != nil { return fmt.Errorf("cannot count measures: %w", err) @@ -112,7 +113,6 @@ func (s MeasureService) CountForRiskID( return nil }, ) - if err != nil { return 0, err } @@ -126,6 +126,7 @@ func (s MeasureService) ListForRiskID( filter *coredata.MeasureFilter, ) (*page.Page[*coredata.Measure, coredata.MeasureOrderField], error) { var measures coredata.Measures + risk := &coredata.Risk{} err := s.svc.pg.WithConn( @@ -143,7 +144,6 @@ func (s MeasureService) ListForRiskID( return nil }, ) - if err != nil { return nil, err } @@ -162,6 +162,7 @@ func (s MeasureService) CountForControlID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { measures := &coredata.Measures{} + count, err = measures.CountByControlID(ctx, conn, s.svc.scope, controlID, filter) if err != nil { return fmt.Errorf("cannot count measures: %w", err) @@ -170,7 +171,6 @@ func (s MeasureService) CountForControlID( return nil }, ) - if err != nil { return 0, err } @@ -185,6 +185,7 @@ func (s MeasureService) ListForControlID( filter *coredata.MeasureFilter, ) (*page.Page[*coredata.Measure, coredata.MeasureOrderField], error) { var measures coredata.Measures + control := &coredata.Control{} err := s.svc.pg.WithConn( @@ -202,7 +203,6 @@ func (s MeasureService) ListForControlID( return nil }, ) - if err != nil { return nil, err } @@ -221,6 +221,7 @@ func (s MeasureService) CountForOrganizationID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { measures := &coredata.Measures{} + count, err = measures.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, filter) if err != nil { return fmt.Errorf("cannot count measures: %w", err) @@ -229,7 +230,6 @@ func (s MeasureService) CountForOrganizationID( return nil }, ) - if err != nil { return 0, err } @@ -251,8 +251,11 @@ func (s MeasureService) ListDistinctCategoriesForOrganizationID( return fmt.Errorf("cannot load organization: %w", err) } - var measures coredata.Measures - var err error + var ( + measures coredata.Measures + err error + ) + categories, err = measures.LoadDistinctCategoriesByOrganizationID( ctx, conn, @@ -266,7 +269,6 @@ func (s MeasureService) ListDistinctCategoriesForOrganizationID( return nil }, ) - if err != nil { return nil, err } @@ -281,6 +283,7 @@ func (s MeasureService) ListForOrganizationID( filter *coredata.MeasureFilter, ) (*page.Page[*coredata.Measure, coredata.MeasureOrderField], error) { var measures coredata.Measures + organization := &coredata.Organization{} err := s.svc.pg.WithConn( @@ -305,7 +308,6 @@ func (s MeasureService) ListForOrganizationID( return nil }, ) - if err != nil { return nil, err } @@ -325,7 +327,6 @@ func (s MeasureService) Get( return measure.LoadByID(ctx, conn, s.svc.scope, measureID) }, ) - if err != nil { return nil, err } @@ -469,7 +470,6 @@ func (s MeasureService) Import( return nil }, ) - if err != nil { return nil, fmt.Errorf("cannot import measures: %w", err) } @@ -545,7 +545,9 @@ func (s MeasureService) Create( } now := time.Now() + var measure *coredata.Measure + organization := &coredata.Organization{} referenceID, err := uuid.NewV4() @@ -579,7 +581,6 @@ func (s MeasureService) Create( return nil }, ) - if err != nil { return nil, err } @@ -636,7 +637,6 @@ func (s MeasureService) CreateDocumentMapping( return nil }, ) - if err != nil { return nil, nil, err } @@ -671,7 +671,6 @@ func (s MeasureService) DeleteDocumentMapping( return nil }, ) - if err != nil { return nil, nil, err } diff --git a/pkg/probo/obligation_service.go b/pkg/probo/obligation_service.go index 4e02791b1..dfeba3e53 100644 --- a/pkg/probo/obligation_service.go +++ b/pkg/probo/obligation_service.go @@ -110,7 +110,6 @@ func (s ObligationService) Get( return nil }, ) - if err != nil { return nil, err } @@ -169,7 +168,6 @@ func (s *ObligationService) Create( return nil }, ) - if err != nil { return nil, err } @@ -219,6 +217,7 @@ func (s *ObligationService) Update( if err := owner.LoadByID(ctx, conn, s.svc.scope, *req.OwnerID); err != nil { return fmt.Errorf("cannot load owner profile: %w", err) } + obligation.OwnerID = *req.OwnerID } @@ -251,7 +250,6 @@ func (s *ObligationService) Update( return nil }, ) - if err != nil { return nil, err } @@ -296,6 +294,7 @@ func (s ObligationService) CountForOrganizationID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { obligations := coredata.Obligations{} + count, err = obligations.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID) if err != nil { return fmt.Errorf("cannot count obligations: %w", err) @@ -304,7 +303,6 @@ func (s ObligationService) CountForOrganizationID( return nil }, ) - if err != nil { return 0, err } @@ -318,6 +316,7 @@ func (s ObligationService) ListForControlID( cursor *page.Cursor[coredata.ObligationOrderField], ) (*page.Page[*coredata.Obligation, coredata.ObligationOrderField], error) { var obligations coredata.Obligations + control := &coredata.Control{} err := s.svc.pg.WithConn( @@ -335,7 +334,6 @@ func (s ObligationService) ListForControlID( return nil }, ) - if err != nil { return nil, err } @@ -361,7 +359,6 @@ func (s ObligationService) ListForOrganizationID( return nil }, ) - if err != nil { return nil, err } @@ -379,6 +376,7 @@ func (s ObligationService) CountForRiskID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { obligations := &coredata.Obligations{} + count, err = obligations.CountByRiskID(ctx, conn, s.svc.scope, riskID) if err != nil { return fmt.Errorf("cannot count obligations: %w", err) @@ -387,7 +385,6 @@ func (s ObligationService) CountForRiskID( return nil }, ) - if err != nil { return 0, err } @@ -413,7 +410,6 @@ func (s ObligationService) ListForRiskID( return nil }, ) - if err != nil { return nil, err } diff --git a/pkg/probo/organization_service.go b/pkg/probo/organization_service.go index 9d326e402..6bc1b7051 100644 --- a/pkg/probo/organization_service.go +++ b/pkg/probo/organization_service.go @@ -102,7 +102,6 @@ func (s OrganizationService) Get( ) }, ) - if err != nil { return nil, err } @@ -160,7 +159,6 @@ func (s OrganizationService) GetContext( return nil }, ) - if err != nil { return nil, err } @@ -219,7 +217,6 @@ func (s OrganizationService) UpdateContext( return nil }, ) - if err != nil { return nil, err } @@ -265,6 +262,7 @@ func (s OrganizationService) Update( return fmt.Errorf("invalid email address: %w", err) } } + organization.Email = *req.Email } @@ -278,6 +276,7 @@ func (s OrganizationService) Update( if req.File != nil { fileID := gid.New(s.svc.scope.GetTenantID(), coredata.FileEntityType) + objectKey, err := uuid.NewV7() if err != nil { return fmt.Errorf("cannot generate object key: %w", err) @@ -288,6 +287,7 @@ func (s OrganizationService) Update( if contentType == "" { contentType = "application/octet-stream" + if filename != "" { if detectedType := mime.TypeByExtension(filepath.Ext(filename)); detectedType != "" { contentType = detectedType @@ -339,6 +339,7 @@ func (s OrganizationService) Update( if req.HorizontalLogoFile != nil { fileID := gid.New(s.svc.scope.GetTenantID(), coredata.FileEntityType) + objectKey, err := uuid.NewV7() if err != nil { return fmt.Errorf("cannot generate object key: %w", err) @@ -349,6 +350,7 @@ func (s OrganizationService) Update( if contentType == "" { contentType = "application/octet-stream" + if filename != "" { if detectedType := mime.TypeByExtension(filepath.Ext(filename)); detectedType != "" { contentType = detectedType @@ -405,7 +407,6 @@ func (s OrganizationService) Update( return nil }, ) - if err != nil { return nil, err } @@ -520,7 +521,6 @@ func (s OrganizationService) DeleteHorizontalLogo( return nil }, ) - if err != nil { return nil, err } diff --git a/pkg/probo/processing_activity_service.go b/pkg/probo/processing_activity_service.go index 06bcbc54c..71cae80d7 100644 --- a/pkg/probo/processing_activity_service.go +++ b/pkg/probo/processing_activity_service.go @@ -147,7 +147,6 @@ func (s ProcessingActivityService) Get( return processingActivity.LoadByID(ctx, conn, s.svc.scope, processingActivityID) }, ) - if err != nil { return nil, err } @@ -209,7 +208,6 @@ func (s *ProcessingActivityService) Create( return nil }, ) - if err != nil { return nil, err } @@ -234,57 +232,75 @@ func (s *ProcessingActivityService) Update( if req.Name != nil { processingActivity.Name = *req.Name } + if req.Purpose != nil { processingActivity.Purpose = *req.Purpose } + if req.DataSubjectCategory != nil { processingActivity.DataSubjectCategory = *req.DataSubjectCategory } + if req.PersonalDataCategory != nil { processingActivity.PersonalDataCategory = *req.PersonalDataCategory } + if req.SpecialOrCriminalData != nil { processingActivity.SpecialOrCriminalData = *req.SpecialOrCriminalData } + if req.ConsentEvidenceLink != nil { processingActivity.ConsentEvidenceLink = *req.ConsentEvidenceLink } + if req.LawfulBasis != nil { processingActivity.LawfulBasis = *req.LawfulBasis } + if req.Recipients != nil { processingActivity.Recipients = *req.Recipients } + if req.Location != nil { processingActivity.Location = *req.Location } + if req.InternationalTransfers != nil { processingActivity.InternationalTransfers = *req.InternationalTransfers } + if req.TransferSafeguard != nil { processingActivity.TransferSafeguard = *req.TransferSafeguard } + if req.RetentionPeriod != nil { processingActivity.RetentionPeriod = *req.RetentionPeriod } + if req.SecurityMeasures != nil { processingActivity.SecurityMeasures = *req.SecurityMeasures } + if req.DataProtectionImpactAssessmentNeeded != nil { processingActivity.DataProtectionImpactAssessmentNeeded = *req.DataProtectionImpactAssessmentNeeded } + if req.TransferImpactAssessmentNeeded != nil { processingActivity.TransferImpactAssessmentNeeded = *req.TransferImpactAssessmentNeeded } + if req.LastReviewDate != nil { processingActivity.LastReviewDate = *req.LastReviewDate } + if req.NextReviewDate != nil { processingActivity.NextReviewDate = *req.NextReviewDate } + if req.Role != nil { processingActivity.Role = *req.Role } + if req.DataProtectionOfficerID != nil { processingActivity.DataProtectionOfficerID = *req.DataProtectionOfficerID } @@ -304,7 +320,6 @@ func (s *ProcessingActivityService) Update( return nil }, ) - if err != nil { return nil, err } @@ -317,6 +332,7 @@ func (s ProcessingActivityService) Delete( processingActivityID gid.GID, ) error { processingActivity := coredata.ProcessingActivity{ID: processingActivityID} + return s.svc.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { @@ -324,6 +340,7 @@ func (s ProcessingActivityService) Delete( if err != nil { return fmt.Errorf("cannot delete processing activity: %w", err) } + return nil }, ) @@ -347,7 +364,6 @@ func (s ProcessingActivityService) ListForOrganizationID( return nil }, ) - if err != nil { return nil, err } @@ -365,6 +381,7 @@ func (s ProcessingActivityService) CountForOrganizationID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { processingActivities := coredata.ProcessingActivities{} + count, err = processingActivities.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID) if err != nil { return fmt.Errorf("cannot count processing activities: %w", err) @@ -373,7 +390,6 @@ func (s ProcessingActivityService) CountForOrganizationID( return nil }, ) - if err != nil { return 0, err } diff --git a/pkg/probo/report_service.go b/pkg/probo/report_service.go index ac07af143..be211d37a 100644 --- a/pkg/probo/report_service.go +++ b/pkg/probo/report_service.go @@ -46,7 +46,6 @@ func (s ReportService) Get( return nil }, ) - if err != nil { return nil, err } @@ -88,6 +87,7 @@ func (s ReportService) Delete( ) error { return s.svc.pg.WithTx(ctx, func(ctx context.Context, conn pg.Tx) error { report := &coredata.Report{} + err := report.LoadByID(ctx, conn, s.svc.scope, reportID) if err != nil { return fmt.Errorf("cannot get report: %w", err) diff --git a/pkg/probo/rights_request_service.go b/pkg/probo/rights_request_service.go index d37bdecfe..6c65df2a5 100644 --- a/pkg/probo/rights_request_service.go +++ b/pkg/probo/rights_request_service.go @@ -98,7 +98,6 @@ func (s RightsRequestService) Get( return nil }, ) - if err != nil { return nil, err } @@ -145,7 +144,6 @@ func (s *RightsRequestService) Create( return nil }, ) - if err != nil { return nil, err } @@ -207,7 +205,6 @@ func (s *RightsRequestService) Update( return nil }, ) - if err != nil { return nil, err } @@ -248,6 +245,7 @@ func (s RightsRequestService) CountByOrganizationID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { requests := coredata.RightsRequests{} + count, err = requests.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID) if err != nil { return fmt.Errorf("cannot count rights requests: %w", err) @@ -256,7 +254,6 @@ func (s RightsRequestService) CountByOrganizationID( return nil }, ) - if err != nil { return 0, err } @@ -282,7 +279,6 @@ func (s RightsRequestService) ListForOrganizationID( return nil }, ) - if err != nil { return nil, err } diff --git a/pkg/probo/risk_service.go b/pkg/probo/risk_service.go index 83b004122..0ef10ef44 100644 --- a/pkg/probo/risk_service.go +++ b/pkg/probo/risk_service.go @@ -107,6 +107,7 @@ func (s RiskService) CountForMeasureID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { risks := &coredata.Risks{} + count, err = risks.CountByMeasureID(ctx, conn, s.svc.scope, measureID, filter) if err != nil { return fmt.Errorf("cannot count risks: %w", err) @@ -115,7 +116,6 @@ func (s RiskService) CountForMeasureID( return nil }, ) - if err != nil { return 0, fmt.Errorf("cannot count risks: %w", err) } @@ -137,7 +137,6 @@ func (s RiskService) ListForMeasureID( return risks.LoadByMeasureID(ctx, conn, s.svc.scope, measureID, cursor, filter) }, ) - if err != nil { return nil, fmt.Errorf("cannot list risks: %w", err) } @@ -156,6 +155,7 @@ func (s RiskService) CountForOrganizationID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { risks := &coredata.Risks{} + count, err = risks.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, filter) if err != nil { return fmt.Errorf("cannot count risks: %w", err) @@ -164,7 +164,6 @@ func (s RiskService) CountForOrganizationID( return nil }, ) - if err != nil { return 0, fmt.Errorf("cannot count risks: %w", err) } @@ -193,7 +192,6 @@ func (s RiskService) ListForOrganizationID( ) }, ) - if err != nil { return nil, fmt.Errorf("cannot list risks: %w", err) } @@ -230,7 +228,6 @@ func (s RiskService) CreateDocumentMapping( return riskDocument.Insert(ctx, tx, s.svc.scope) }, ) - if err != nil { return nil, nil, fmt.Errorf("cannot create risk document mapping: %w", err) } @@ -261,7 +258,6 @@ func (s RiskService) DeleteDocumentMapping( return riskDocument.Delete(ctx, tx, s.svc.scope, risk.ID, document.ID) }, ) - if err != nil { return nil, nil, fmt.Errorf("cannot delete risk document mapping: %w", err) } @@ -298,7 +294,6 @@ func (s RiskService) CreateMeasureMapping( return riskMeasure.Insert(ctx, tx, s.svc.scope) }, ) - if err != nil { return nil, nil, fmt.Errorf("cannot create risk measure mapping: %w", err) } @@ -335,7 +330,6 @@ func (s RiskService) DeleteMeasureMapping( return riskMeasure.Delete(ctx, tx, s.svc.scope, risk.ID, measure.ID) }, ) - if err != nil { return nil, nil, fmt.Errorf("cannot delete risk measure mapping: %w", err) } @@ -372,7 +366,6 @@ func (s RiskService) CreateObligationMapping( return riskObligation.Insert(ctx, tx, s.svc.scope) }, ) - if err != nil { return nil, nil, fmt.Errorf("cannot create risk obligation mapping: %w", err) } @@ -406,7 +399,6 @@ func (s RiskService) DeleteObligationMapping( return riskObligation.Delete(ctx, tx, s.svc.scope) }, ) - if err != nil { return nil, nil, fmt.Errorf("cannot delete risk obligation mapping: %w", err) } @@ -470,7 +462,6 @@ func (s RiskService) Create( return risk.Insert(ctx, tx, s.svc.scope) }, ) - if err != nil { return nil, fmt.Errorf("cannot create risk: %w", err) } @@ -490,7 +481,6 @@ func (s RiskService) Get( return risk.LoadByID(ctx, conn, s.svc.scope, riskID) }, ) - if err != nil { return nil, fmt.Errorf("cannot get risk: %w", err) } @@ -577,6 +567,7 @@ func (s RiskService) Update( if err := owner.LoadByID(ctx, conn, s.svc.scope, **req.OwnerID); err != nil { return fmt.Errorf("cannot load owner profile: %w", err) } + risk.OwnerID = *req.OwnerID } else { risk.OwnerID = nil diff --git a/pkg/probo/service.go b/pkg/probo/service.go index 53e856d16..450bbded6 100644 --- a/pkg/probo/service.go +++ b/pkg/probo/service.go @@ -325,6 +325,7 @@ func (s *Service) ExportJob(ctx context.Context) error { if err := s.commitFailedExport(ctx, exportJob, unknownTypeErr); err != nil { return fmt.Errorf("unknown export job type %q, and cannot commit failed export: %w", exportJob.Type, err) } + return unknownTypeErr } @@ -338,8 +339,10 @@ func (s *Service) ExportJob(ctx context.Context) error { err, ) } + return fmt.Errorf("cannot build and upload %s export: %w", exportJob.Type, buildErr) } + exportJob = updatedExportJob if emailErr := exportService.SendExportEmail(ctx, *exportJob.FileID, exportJob.RecipientName, exportJob.RecipientEmail); emailErr != nil { @@ -350,6 +353,7 @@ func (s *Service) ExportJob(ctx context.Context) error { err, ) } + return fmt.Errorf("cannot send completion email: %w", emailErr) } @@ -362,6 +366,7 @@ func (s *Service) ExportJob(ctx context.Context) error { func (s *Service) lockExportJob(ctx context.Context) (*coredata.ExportJob, error) { exportJob := &coredata.ExportJob{} + var scope coredata.Scoper err := s.pg.WithTx( @@ -374,6 +379,7 @@ func (s *Service) lockExportJob(ctx context.Context) (*coredata.ExportJob, error scope = coredata.NewScope(exportJob.ID.TenantID()) exportJob.Status = coredata.ExportJobStatusProcessing + exportJob.StartedAt = new(time.Now()) if err := exportJob.Update(ctx, tx, scope); err != nil { return fmt.Errorf("cannot update %s export job: %w", exportJob.Type, err) diff --git a/pkg/probo/statement_of_applicability_service.go b/pkg/probo/statement_of_applicability_service.go index 1641e74f1..bb16d064a 100644 --- a/pkg/probo/statement_of_applicability_service.go +++ b/pkg/probo/statement_of_applicability_service.go @@ -66,6 +66,7 @@ func (s StatementOfApplicabilityService) ListForOrganizationID( cursor *page.Cursor[coredata.StatementOfApplicabilityOrderField], ) (*page.Page[*coredata.StatementOfApplicability, coredata.StatementOfApplicabilityOrderField], error) { var statementsOfApplicability coredata.StatementsOfApplicability + organization := &coredata.Organization{} err := s.svc.pg.WithConn( @@ -89,7 +90,6 @@ func (s StatementOfApplicabilityService) ListForOrganizationID( return nil }, ) - if err != nil { return nil, err } @@ -107,6 +107,7 @@ func (s StatementOfApplicabilityService) CountForOrganizationID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { statementsOfApplicability := &coredata.StatementsOfApplicability{} + count, err = statementsOfApplicability.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID) if err != nil { return fmt.Errorf("cannot count statements_of_applicability: %w", err) @@ -115,7 +116,6 @@ func (s StatementOfApplicabilityService) CountForOrganizationID( return nil }, ) - if err != nil { return 0, err } @@ -135,7 +135,6 @@ func (s StatementOfApplicabilityService) Get( return statementOfApplicability.LoadByID(ctx, conn, s.svc.scope, statementOfApplicabilityID) }, ) - if err != nil { return nil, err } @@ -183,7 +182,6 @@ func (s StatementOfApplicabilityService) Create( return nil }, ) - if err != nil { return nil, err } @@ -221,7 +219,6 @@ func (s StatementOfApplicabilityService) Update( return nil }, ) - if err != nil { return nil, err } @@ -249,7 +246,6 @@ func (s StatementOfApplicabilityService) Delete( return nil }, ) - if err != nil { return err } @@ -289,10 +285,10 @@ func (s StatementOfApplicabilityService) ListApplicabilityStatements( if err := statements.LoadByStatementOfApplicabilityID(ctx, conn, s.svc.scope, statementOfApplicabilityID, cursor); err != nil { return fmt.Errorf("cannot load applicability statements: %w", err) } + return nil }, ) - if err != nil { return nil, err } @@ -310,14 +306,15 @@ func (s StatementOfApplicabilityService) CountApplicabilityStatements( ctx, func(ctx context.Context, conn pg.Querier) (err error) { statements := &coredata.ApplicabilityStatements{} + count, err = statements.CountByStatementOfApplicabilityID(ctx, conn, s.svc.scope, statementOfApplicabilityID) if err != nil { return fmt.Errorf("cannot count applicability statements: %w", err) } + return nil }, ) - if err != nil { return 0, err } diff --git a/pkg/probo/task_service.go b/pkg/probo/task_service.go index fc6b53b6b..def9d76fa 100644 --- a/pkg/probo/task_service.go +++ b/pkg/probo/task_service.go @@ -311,6 +311,7 @@ func (s TaskService) Update( if err := assignee.LoadByID(ctx, conn, s.svc.scope, **req.AssignedToID); err != nil { return fmt.Errorf("cannot load assignee profile: %w", err) } + task.AssignedToID = *req.AssignedToID } } @@ -323,6 +324,7 @@ func (s TaskService) Update( if err := measure.LoadByID(ctx, conn, s.svc.scope, **req.MeasureID); err != nil { return fmt.Errorf("cannot load measure: %w", err) } + task.MeasureID = *req.MeasureID } } @@ -393,6 +395,7 @@ func (s TaskService) CountForOrganizationID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { tasks := coredata.Tasks{} + count, err = tasks.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID) if err != nil { return fmt.Errorf("cannot count tasks: %w", err) @@ -438,6 +441,7 @@ func (s TaskService) CountForMeasureID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { tasks := coredata.Tasks{} + count, err = tasks.CountByMeasureID(ctx, conn, s.svc.scope, measureID) if err != nil { return fmt.Errorf("cannot count tasks: %w", err) diff --git a/pkg/probo/third_party_business_associate_agreement_service.go b/pkg/probo/third_party_business_associate_agreement_service.go index 24a763a27..3e903d32e 100644 --- a/pkg/probo/third_party_business_associate_agreement_service.go +++ b/pkg/probo/third_party_business_associate_agreement_service.go @@ -70,8 +70,10 @@ func (s ThirdPartyBusinessAssociateAgreementService) GetByThirdPartyID( ctx context.Context, thirdPartyID gid.GID, ) (*coredata.ThirdPartyBusinessAssociateAgreement, *coredata.File, error) { - var thirdPartyBusinessAssociateAgreement *coredata.ThirdPartyBusinessAssociateAgreement - var file *coredata.File + var ( + thirdPartyBusinessAssociateAgreement *coredata.ThirdPartyBusinessAssociateAgreement + file *coredata.File + ) err := s.svc.pg.WithConn( ctx, @@ -89,7 +91,6 @@ func (s ThirdPartyBusinessAssociateAgreementService) GetByThirdPartyID( return nil }, ) - if err != nil { return nil, nil, err } @@ -111,8 +112,10 @@ func (s ThirdPartyBusinessAssociateAgreementService) Upload( return nil, nil, fmt.Errorf("cannot generate object key: %w", err) } - var thirdPartyBusinessAssociateAgreement *coredata.ThirdPartyBusinessAssociateAgreement - var file *coredata.File + var ( + thirdPartyBusinessAssociateAgreement *coredata.ThirdPartyBusinessAssociateAgreement + file *coredata.File + ) err = s.svc.pg.WithTx( ctx, @@ -186,7 +189,6 @@ func (s ThirdPartyBusinessAssociateAgreementService) Upload( return nil }, ) - if err != nil { return nil, nil, err } @@ -198,8 +200,10 @@ func (s ThirdPartyBusinessAssociateAgreementService) Get( ctx context.Context, thirdPartyBusinessAssociateAgreementID gid.GID, ) (*coredata.ThirdPartyBusinessAssociateAgreement, *coredata.File, error) { - var thirdPartyBusinessAssociateAgreement *coredata.ThirdPartyBusinessAssociateAgreement - var file *coredata.File + var ( + thirdPartyBusinessAssociateAgreement *coredata.ThirdPartyBusinessAssociateAgreement + file *coredata.File + ) err := s.svc.pg.WithConn( ctx, @@ -217,7 +221,6 @@ func (s ThirdPartyBusinessAssociateAgreementService) Get( return nil }, ) - if err != nil { return nil, nil, fmt.Errorf("cannot load thirdParty business associate agreement: %w", err) } @@ -293,9 +296,11 @@ func (s ThirdPartyBusinessAssociateAgreementService) Update( } now := time.Now() + if req.ValidFrom != nil { existingAgreement.ValidFrom = *req.ValidFrom } + if req.ValidUntil != nil { existingAgreement.ValidUntil = *req.ValidUntil } @@ -313,7 +318,6 @@ func (s ThirdPartyBusinessAssociateAgreementService) Update( return nil }, ) - if err != nil { return nil, nil, err } diff --git a/pkg/probo/third_party_compliance_report_service.go b/pkg/probo/third_party_compliance_report_service.go index 8bc9ac38b..8ffaded98 100644 --- a/pkg/probo/third_party_compliance_report_service.go +++ b/pkg/probo/third_party_compliance_report_service.go @@ -62,7 +62,6 @@ func (s ThirdPartyComplianceReportService) ListForThirdPartyID( return thirdPartyComplianceReports.LoadForThirdPartyID(ctx, conn, s.svc.scope, thirdPartyID, cursor) }, ) - if err != nil { return nil, err } @@ -93,7 +92,6 @@ func (s ThirdPartyComplianceReportService) Upload( "organization-id": thirdParty.OrganizationID.String(), }, &req.File) - if err != nil { return nil, err } @@ -120,7 +118,6 @@ func (s ThirdPartyComplianceReportService) Upload( return thirdPartyComplianceReport.Insert(ctx, tx, s.svc.scope) }, ) - if err != nil { return nil, err } @@ -140,7 +137,6 @@ func (s ThirdPartyComplianceReportService) Get( return thirdPartyComplianceReport.LoadByID(ctx, conn, s.svc.scope, thirdPartyComplianceReportID) }, ) - if err != nil { return nil, fmt.Errorf("cannot load thirdParty compliance report: %w", err) } @@ -164,7 +160,6 @@ func (s ThirdPartyComplianceReportService) Delete( return nil }, ) - if err != nil { return fmt.Errorf("cannot delete thirdParty compliance report: %w", err) } diff --git a/pkg/probo/third_party_contact_service.go b/pkg/probo/third_party_contact_service.go index 8e625195a..81c2e55c2 100644 --- a/pkg/probo/third_party_contact_service.go +++ b/pkg/probo/third_party_contact_service.go @@ -88,7 +88,6 @@ func (s ThirdPartyContactService) Get( return nil }, ) - if err != nil { return nil, err } @@ -114,7 +113,6 @@ func (s ThirdPartyContactService) List( return nil }, ) - if err != nil { return nil, err } @@ -159,7 +157,6 @@ func (s ThirdPartyContactService) Create( return nil }, ) - if err != nil { return nil, err } @@ -188,21 +185,24 @@ func (s ThirdPartyContactService) Update( if req.FullName != nil { thirdPartyContact.FullName = *req.FullName } + if req.Email != nil { thirdPartyContact.Email = *req.Email } + if req.Phone != nil { thirdPartyContact.Phone = *req.Phone } + if req.Role != nil { thirdPartyContact.Role = *req.Role } + thirdPartyContact.UpdatedAt = time.Now() return thirdPartyContact.Update(ctx, conn, s.svc.scope) }, ) - if err != nil { return nil, err } @@ -215,6 +215,7 @@ func (s ThirdPartyContactService) Delete( thirdPartyContactID gid.GID, ) error { thirdPartyContact := coredata.ThirdPartyContact{ID: thirdPartyContactID} + return s.svc.pg.WithTx( ctx, func(ctx context.Context, conn pg.Tx) error { diff --git a/pkg/probo/third_party_data_privacy_agreement_service.go b/pkg/probo/third_party_data_privacy_agreement_service.go index ec9c28095..29b2262a5 100644 --- a/pkg/probo/third_party_data_privacy_agreement_service.go +++ b/pkg/probo/third_party_data_privacy_agreement_service.go @@ -70,8 +70,10 @@ func (s ThirdPartyDataPrivacyAgreementService) GetByThirdPartyID( ctx context.Context, thirdPartyID gid.GID, ) (*coredata.ThirdPartyDataPrivacyAgreement, *coredata.File, error) { - var thirdPartyDataPrivacyAgreement *coredata.ThirdPartyDataPrivacyAgreement - var file *coredata.File + var ( + thirdPartyDataPrivacyAgreement *coredata.ThirdPartyDataPrivacyAgreement + file *coredata.File + ) err := s.svc.pg.WithConn( ctx, @@ -89,7 +91,6 @@ func (s ThirdPartyDataPrivacyAgreementService) GetByThirdPartyID( return nil }, ) - if err != nil { return nil, nil, err } @@ -111,9 +112,11 @@ func (s ThirdPartyDataPrivacyAgreementService) Upload( return nil, nil, fmt.Errorf("cannot generate object key: %w", err) } - var thirdPartyDataPrivacyAgreement *coredata.ThirdPartyDataPrivacyAgreement - var file *coredata.File - var thirdParty *coredata.ThirdParty + var ( + thirdPartyDataPrivacyAgreement *coredata.ThirdPartyDataPrivacyAgreement + file *coredata.File + thirdParty *coredata.ThirdParty + ) err = s.svc.pg.WithTx( ctx, @@ -124,6 +127,7 @@ func (s ThirdPartyDataPrivacyAgreementService) Upload( } mimeType := mime.TypeByExtension(filepath.Ext(req.FileName)) + _, err := s.svc.s3.PutObject(ctx, &s3.PutObjectInput{ Bucket: &s.svc.bucket, Key: new(objectKey.String()), @@ -139,6 +143,7 @@ func (s ThirdPartyDataPrivacyAgreementService) Upload( if err != nil { return fmt.Errorf("cannot upload file to S3: %w", err) } + headOutput, err := s.svc.s3.HeadObject(ctx, &s3.HeadObjectInput{ Bucket: new(s.svc.bucket), Key: new(objectKey.String()), @@ -184,7 +189,6 @@ func (s ThirdPartyDataPrivacyAgreementService) Upload( return nil }, ) - if err != nil { return nil, nil, err } @@ -196,8 +200,10 @@ func (s ThirdPartyDataPrivacyAgreementService) Get( ctx context.Context, thirdPartyDataPrivacyAgreementID gid.GID, ) (*coredata.ThirdPartyDataPrivacyAgreement, *coredata.File, error) { - var thirdPartyDataPrivacyAgreement *coredata.ThirdPartyDataPrivacyAgreement - var file *coredata.File + var ( + thirdPartyDataPrivacyAgreement *coredata.ThirdPartyDataPrivacyAgreement + file *coredata.File + ) err := s.svc.pg.WithConn( ctx, @@ -215,7 +221,6 @@ func (s ThirdPartyDataPrivacyAgreementService) Get( return nil }, ) - if err != nil { return nil, nil, fmt.Errorf("cannot load thirdParty data privacy agreement: %w", err) } @@ -291,9 +296,11 @@ func (s ThirdPartyDataPrivacyAgreementService) Update( } now := time.Now() + if req.ValidFrom != nil { existingAgreement.ValidFrom = *req.ValidFrom } + if req.ValidUntil != nil { existingAgreement.ValidUntil = *req.ValidUntil } @@ -311,7 +318,6 @@ func (s ThirdPartyDataPrivacyAgreementService) Update( return nil }, ) - if err != nil { return nil, nil, err } diff --git a/pkg/probo/third_party_service.go b/pkg/probo/third_party_service.go index 087a0c4d2..2827dd6ef 100644 --- a/pkg/probo/third_party_service.go +++ b/pkg/probo/third_party_service.go @@ -217,6 +217,7 @@ func (s ThirdPartyService) CountForOrganizationID( func(ctx context.Context, conn pg.Querier) (err error) { thirdParties := coredata.ThirdParties{} filter := &coredata.ThirdPartyFilter{} + count, err = thirdParties.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, filter) if err != nil { return fmt.Errorf("cannot count thirdParties: %w", err) @@ -225,7 +226,6 @@ func (s ThirdPartyService) CountForOrganizationID( return nil }, ) - if err != nil { return 0, err } @@ -240,6 +240,7 @@ func (s ThirdPartyService) ListForOrganizationID( filter *coredata.ThirdPartyFilter, ) (*page.Page[*coredata.ThirdParty, coredata.ThirdPartyOrderField], error) { var thirdParties coredata.ThirdParties + organization := &coredata.Organization{} err := s.svc.pg.WithConn( @@ -259,7 +260,6 @@ func (s ThirdPartyService) ListForOrganizationID( ) }, ) - if err != nil { return nil, err } @@ -277,6 +277,7 @@ func (s ThirdPartyService) CountForDatumID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { thirdParties := coredata.ThirdParties{} + count, err = thirdParties.CountByDatumID(ctx, conn, s.svc.scope, datumID) if err != nil { return fmt.Errorf("cannot count thirdParties: %w", err) @@ -285,7 +286,6 @@ func (s ThirdPartyService) CountForDatumID( return nil }, ) - if err != nil { return 0, err } @@ -312,7 +312,6 @@ func (s ThirdPartyService) ListForDatumID( ) }, ) - if err != nil { return nil, err } @@ -421,6 +420,7 @@ func (s ThirdPartyService) Update( if err := businessOwner.LoadByID(ctx, conn, s.svc.scope, **req.BusinessOwnerID); err != nil { return fmt.Errorf("cannot load business owner profile: %w", err) } + thirdParty.BusinessOwnerID = &businessOwner.ID } else { thirdParty.BusinessOwnerID = nil @@ -433,6 +433,7 @@ func (s ThirdPartyService) Update( if err := securityOwner.LoadByID(ctx, conn, s.svc.scope, **req.SecurityOwnerID); err != nil { return fmt.Errorf("cannot load security owner profile: %w", err) } + thirdParty.SecurityOwnerID = &securityOwner.ID } else { thirdParty.SecurityOwnerID = nil @@ -459,7 +460,6 @@ func (s ThirdPartyService) Update( return nil }, ) - if err != nil { return nil, err } @@ -479,7 +479,6 @@ func (s ThirdPartyService) Get( return thirdParty.LoadByID(ctx, conn, s.svc.scope, thirdPartyID) }, ) - if err != nil { return nil, err } @@ -591,6 +590,7 @@ func (s ThirdPartyService) Create( if err := businessOwner.LoadByID(ctx, conn, s.svc.scope, *req.BusinessOwnerID); err != nil { return fmt.Errorf("cannot load business owner profile: %w", err) } + thirdParty.BusinessOwnerID = &businessOwner.ID } @@ -599,6 +599,7 @@ func (s ThirdPartyService) Create( if err := securityOwner.LoadByID(ctx, conn, s.svc.scope, *req.SecurityOwnerID); err != nil { return fmt.Errorf("cannot load security owner profile: %w", err) } + thirdParty.SecurityOwnerID = &securityOwner.ID } @@ -626,7 +627,6 @@ func (s ThirdPartyService) Create( return nil }, ) - if err != nil { return nil, err } @@ -644,6 +644,7 @@ func (s ThirdPartyService) CountForAssetID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { thirdParties := coredata.ThirdParties{} + count, err = thirdParties.CountByAssetID(ctx, conn, s.svc.scope, assetID) if err != nil { return fmt.Errorf("cannot count thirdParties: %w", err) @@ -652,7 +653,6 @@ func (s ThirdPartyService) CountForAssetID( return nil }, ) - if err != nil { return 0, err } @@ -673,7 +673,6 @@ func (s ThirdPartyService) ListForAssetID( return thirdParties.LoadByAssetID(ctx, conn, s.svc.scope, assetID, cursor) }, ) - if err != nil { return nil, err } @@ -699,7 +698,6 @@ func (s ThirdPartyService) ListForProcessingActivityID( return nil }, ) - if err != nil { return nil, err } @@ -720,7 +718,6 @@ func (s ThirdPartyService) ListRiskAssessments( return thirdPartyRiskAssessments.LoadByThirdPartyID(ctx, conn, s.svc.scope, thirdPartyID, cursor) }, ) - if err != nil { return nil, err } @@ -772,10 +769,10 @@ func (s ThirdPartyService) CreateRiskAssessment( if err := thirdPartyRiskAssessment.Insert(ctx, tx, s.svc.scope); err != nil { return fmt.Errorf("cannot insert thirdParty risk assessment: %w", err) } + return nil }, ) - if err != nil { return nil, err } @@ -795,7 +792,6 @@ func (s ThirdPartyService) GetRiskAssessment( return thirdPartyRiskAssessment.LoadByID(ctx, conn, s.svc.scope, thirdPartyRiskAssessmentID) }, ) - if err != nil { return nil, err } @@ -824,7 +820,6 @@ func (s ThirdPartyService) GetByRiskAssessmentID( return nil }, ) - if err != nil { return nil, err } @@ -860,41 +855,53 @@ func (s ThirdPartyService) Assess( if info.Category != "" { thirdParty.Category = coredata.ThirdPartyCategory(info.Category) } + thirdParty.UpdatedAt = time.Now() if info.Description != "" { thirdParty.Description = &info.Description } + if info.HeadquarterAddress != "" { thirdParty.HeadquarterAddress = &info.HeadquarterAddress } + if info.LegalName != "" { thirdParty.LegalName = &info.LegalName } + if info.PrivacyPolicyURL != "" { thirdParty.PrivacyPolicyURL = &info.PrivacyPolicyURL } + if info.ServiceLevelAgreementURL != "" { thirdParty.ServiceLevelAgreementURL = &info.ServiceLevelAgreementURL } + if info.DataProcessingAgreementURL != "" { thirdParty.DataProcessingAgreementURL = &info.DataProcessingAgreementURL } + if info.BusinessAssociateAgreementURL != "" { thirdParty.BusinessAssociateAgreementURL = &info.BusinessAssociateAgreementURL } + if info.SubprocessorsListURL != "" { thirdParty.SubprocessorsListURL = &info.SubprocessorsListURL } + if info.SecurityPageURL != "" { thirdParty.SecurityPageURL = &info.SecurityPageURL } + if info.TrustPageURL != "" { thirdParty.TrustPageURL = &info.TrustPageURL } + if info.TermsOfServiceURL != "" { thirdParty.TermsOfServiceURL = &info.TermsOfServiceURL } + if info.StatusPageURL != "" { thirdParty.StatusPageURL = &info.StatusPageURL } diff --git a/pkg/probo/third_party_service_service.go b/pkg/probo/third_party_service_service.go index ebb6a8ff1..130890bce 100644 --- a/pkg/probo/third_party_service_service.go +++ b/pkg/probo/third_party_service_service.go @@ -81,7 +81,6 @@ func (s ThirdPartyServiceService) Get( return nil }, ) - if err != nil { return nil, err } @@ -107,7 +106,6 @@ func (s ThirdPartyServiceService) List( return nil }, ) - if err != nil { return nil, err } @@ -150,7 +148,6 @@ func (s ThirdPartyServiceService) Create( return nil }, ) - if err != nil { return nil, err } @@ -179,9 +176,11 @@ func (s ThirdPartyServiceService) Update( if req.Name != nil { thirdPartyService.Name = *req.Name } + if req.Description != nil { thirdPartyService.Description = *req.Description } + thirdPartyService.UpdatedAt = time.Now() if err := thirdPartyService.Update(ctx, conn, s.svc.scope); err != nil { @@ -191,7 +190,6 @@ func (s ThirdPartyServiceService) Update( return nil }, ) - if err != nil { return nil, err } @@ -204,6 +202,7 @@ func (s ThirdPartyServiceService) Delete( thirdPartyServiceID gid.GID, ) error { thirdPartyService := coredata.ThirdPartyService{ID: thirdPartyServiceID} + return s.svc.pg.WithTx( ctx, func(ctx context.Context, conn pg.Tx) error { diff --git a/pkg/probo/transfer_impact_assessment_service.go b/pkg/probo/transfer_impact_assessment_service.go index eb99b66c4..986833d00 100644 --- a/pkg/probo/transfer_impact_assessment_service.go +++ b/pkg/probo/transfer_impact_assessment_service.go @@ -92,7 +92,6 @@ func (s TransferImpactAssessmentService) Get( return nil }, ) - if err != nil { return nil, err } @@ -116,7 +115,6 @@ func (s TransferImpactAssessmentService) GetByProcessingActivityID( return nil }, ) - if err != nil { return nil, err } @@ -142,7 +140,6 @@ func (s TransferImpactAssessmentService) ListForOrganizationID( return nil }, ) - if err != nil { return nil, err } @@ -161,10 +158,10 @@ func (s TransferImpactAssessmentService) CountForOrganizationID( func(ctx context.Context, conn pg.Querier) (err error) { tias := coredata.TransferImpactAssessments{} count, err = tias.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID) + return err }, ) - if err != nil { return 0, err } @@ -211,7 +208,6 @@ func (s *TransferImpactAssessmentService) Create( return nil }, ) - if err != nil { return nil, err } @@ -265,7 +261,6 @@ func (s *TransferImpactAssessmentService) Update( return nil }, ) - if err != nil { return nil, err } diff --git a/pkg/probo/trust_center_access_service.go b/pkg/probo/trust_center_access_service.go index cbd025d3c..a0bccaf2b 100644 --- a/pkg/probo/trust_center_access_service.go +++ b/pkg/probo/trust_center_access_service.go @@ -62,12 +62,15 @@ func (utcar *UpdateTrustCenterAccessRequest) Validate() error { v := validator.New() v.Check(utcar.ID, "id", validator.Required(), validator.GID(coredata.TrustCenterAccessEntityType)) + for i, docAccess := range utcar.DocumentAccesses { v.Check(docAccess.ID, fmt.Sprintf("documentAccesses[%d].ID", i), validator.Required(), validator.GID(coredata.DocumentEntityType)) } + for i, reportAccess := range utcar.ReportAccesses { v.Check(reportAccess.ID, fmt.Sprintf("reportAccesses[%d].ID", i), validator.Required(), validator.GID(coredata.ReportEntityType)) } + for i, reportAccess := range utcar.TrustCenterFileAccesses { v.Check(reportAccess.ID, fmt.Sprintf("trustCenterFileAccesses[%d].ID", i), validator.Required(), validator.GID(coredata.TrustCenterFileEntityType)) } @@ -88,7 +91,6 @@ func (s TrustCenterAccessService) ListForTrustCenterID( return accesses.LoadByTrustCenterID(ctx, conn, s.svc.scope, trustCenterID, cursor) }, ) - if err != nil { return nil, err } @@ -109,7 +111,6 @@ func (s TrustCenterAccessService) ListAvailableDocumentAccesses( return documentAccesses.LoadAvailableByTrustCenterAccessID(ctx, conn, s.svc.scope, trustCenterAccessID, cursor) }, ) - if err != nil { return nil, err } @@ -129,7 +130,6 @@ func (s TrustCenterAccessService) Get( return access.LoadByID(ctx, conn, s.svc.scope, accessID) }, ) - if err != nil { return nil, err } @@ -142,16 +142,20 @@ func (s TrustCenterAccessService) CountDocumentAccesses( trustCenterAccessID gid.GID, ) (int, error) { var count int + err := s.svc.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { - var documentAccesses coredata.TrustCenterDocumentAccesses - var err error + var ( + documentAccesses coredata.TrustCenterDocumentAccesses + err error + ) + count, err = documentAccesses.CountByTrustCenterAccessID(ctx, conn, s.svc.scope, trustCenterAccessID) + return err }, ) - if err != nil { return 0, err } @@ -164,16 +168,20 @@ func (s TrustCenterAccessService) CountPendingRequestDocumentAccesses( trustCenterAccessID gid.GID, ) (int, error) { var count int + err := s.svc.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { - var documentAccesses coredata.TrustCenterDocumentAccesses - var err error + var ( + documentAccesses coredata.TrustCenterDocumentAccesses + err error + ) + count, err = documentAccesses.CountPendingRequestByTrustCenterAccessID(ctx, conn, s.svc.scope, trustCenterAccessID) + return err }, ) - if err != nil { return 0, err } @@ -186,16 +194,20 @@ func (s TrustCenterAccessService) CountActiveDocumentAccesses( trustCenterAccessID gid.GID, ) (int, error) { var count int + err := s.svc.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { - var documentAccesses coredata.TrustCenterDocumentAccesses - var err error + var ( + documentAccesses coredata.TrustCenterDocumentAccesses + err error + ) + count, err = documentAccesses.CountActiveByTrustCenterAccessID(ctx, conn, s.svc.scope, trustCenterAccessID) + return err }, ) - if err != nil { return 0, err } @@ -284,7 +296,6 @@ func (s TrustCenterAccessService) Update( return nil }, ) - if err != nil { return nil, err } @@ -374,5 +385,6 @@ func (s TrustCenterAccessService) sendAccessEmail(ctx context.Context, tx pg.Tx, if err := accessEmail.Insert(ctx, tx); err != nil { return fmt.Errorf("cannot insert access email: %w", err) } + return nil } diff --git a/pkg/probo/trust_center_file_service.go b/pkg/probo/trust_center_file_service.go index 303358b2f..dc68353d5 100644 --- a/pkg/probo/trust_center_file_service.go +++ b/pkg/probo/trust_center_file_service.go @@ -95,7 +95,6 @@ func (s TrustCenterFileService) ListForOrganizationID( return nil }) - if err != nil { return nil, err } @@ -113,6 +112,7 @@ func (s TrustCenterFileService) CountForOrganizationID( ctx, func(ctx context.Context, conn pg.Querier) error { var err error + count, err = (&coredata.TrustCenterFiles{}).CountByOrganizationID(ctx, conn, s.svc.scope, organizationID) if err != nil { return fmt.Errorf("cannot count trust center files: %w", err) @@ -120,7 +120,6 @@ func (s TrustCenterFileService) CountForOrganizationID( return nil }) - if err != nil { return 0, err } @@ -142,7 +141,6 @@ func (s TrustCenterFileService) Get( return nil }) - if err != nil { return nil, err } @@ -161,6 +159,7 @@ func (s TrustCenterFileService) Create( // Validate file filename := req.File.Filename contentType := req.File.ContentType + fileSize, err := s.svc.fileManager.GetFileSize(req.File.Content) if err != nil { return nil, fmt.Errorf("cannot get file size: %w", err) @@ -174,8 +173,10 @@ func (s TrustCenterFileService) Create( trustCenterFileID := gid.New(s.svc.scope.GetTenantID(), coredata.TrustCenterFileEntityType) - var file *coredata.TrustCenterFile - var s3Key string + var ( + file *coredata.TrustCenterFile + s3Key string + ) err = s.svc.pg.WithTx( ctx, @@ -184,6 +185,7 @@ func (s TrustCenterFileService) Create( if err != nil { return fmt.Errorf("cannot upload file: %w", err) } + s3Key = objectKey file = &coredata.TrustCenterFile{ @@ -204,7 +206,6 @@ func (s TrustCenterFileService) Create( return nil }, ) - if err != nil { s.cleanupS3Object(ctx, s3Key) return nil, err @@ -237,12 +238,15 @@ func (s TrustCenterFileService) Update( if req.Name != nil { file.Name = *req.Name } + if req.Category != nil { file.Category = *req.Category } + if req.TrustCenterVisibility != nil { file.TrustCenterVisibility = *req.TrustCenterVisibility } + file.UpdatedAt = now if err := file.Update(ctx, tx, s.svc.scope); err != nil { @@ -252,7 +256,6 @@ func (s TrustCenterFileService) Update( return nil }, ) - if err != nil { return nil, err } @@ -306,7 +309,6 @@ func (s TrustCenterFileService) GenerateFileURL( return nil }, ) - if err != nil { return "", err } @@ -334,8 +336,11 @@ func (s TrustCenterFileService) uploadFile( return gid.GID{}, "", fmt.Errorf("cannot generate object key: %w", err) } - var fileSize int64 - var fileContent io.ReadSeeker + var ( + fileSize int64 + fileContent io.ReadSeeker + ) + filename := file.Filename contentType := file.ContentType @@ -345,6 +350,7 @@ func (s TrustCenterFileService) uploadFile( if err != nil { return gid.GID{}, "", fmt.Errorf("cannot determine file size: %w", err) } + fileSize = size _, err = readSeeker.Seek(0, io.SeekStart) @@ -354,18 +360,21 @@ func (s TrustCenterFileService) uploadFile( } else { fileSize = file.Size } + fileContent = readSeeker } else { buf, err := io.ReadAll(file.Content) if err != nil { return gid.GID{}, "", fmt.Errorf("cannot read file: %w", err) } + fileSize = int64(len(buf)) fileContent = bytes.NewReader(buf) } if contentType == "" { contentType = "application/octet-stream" + if filename != "" { if detectedType := mime.TypeByExtension(filepath.Ext(filename)); detectedType != "" { contentType = detectedType diff --git a/pkg/probo/trust_center_reference_service.go b/pkg/probo/trust_center_reference_service.go index 8c23fa202..430972320 100644 --- a/pkg/probo/trust_center_reference_service.go +++ b/pkg/probo/trust_center_reference_service.go @@ -93,7 +93,6 @@ func (s TrustCenterReferenceService) ListForTrustCenterID( return nil }) - if err != nil { return nil, err } @@ -109,6 +108,7 @@ func (s TrustCenterReferenceService) CountForTrustCenterID( err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) (err error) { references := coredata.TrustCenterReferences{} + count, err = references.CountByTrustCenterID(ctx, conn, s.svc.scope, trustCenterID) if err != nil { return fmt.Errorf("cannot count trust center references: %w", err) @@ -116,7 +116,6 @@ func (s TrustCenterReferenceService) CountForTrustCenterID( return nil }) - if err != nil { return 0, err } @@ -138,7 +137,6 @@ func (s TrustCenterReferenceService) Get( return nil }) - if err != nil { return nil, err } @@ -172,6 +170,7 @@ func (s TrustCenterReferenceService) Create( if err != nil { return fmt.Errorf("cannot upload logo file: %w", err) } + logoKey = s3Key reference = &coredata.TrustCenterReference{ @@ -192,7 +191,6 @@ func (s TrustCenterReferenceService) Create( return nil }) - if err != nil { s.cleanupS3Object(ctx, logoKey) return nil, err @@ -211,9 +209,11 @@ func (s TrustCenterReferenceService) Update( now := time.Now() - var reference *coredata.TrustCenterReference - var newFileID *gid.GID - var logoKey string + var ( + reference *coredata.TrustCenterReference + newFileID *gid.GID + logoKey string + ) err := s.svc.pg.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { reference = &coredata.TrustCenterReference{} @@ -227,6 +227,7 @@ func (s TrustCenterReferenceService) Update( if err != nil { return fmt.Errorf("cannot upload logo file: %w", err) } + newFileID = &fileID logoKey = s3Key } @@ -234,15 +235,19 @@ func (s TrustCenterReferenceService) Update( if req.Name != nil { reference.Name = *req.Name } + if req.Description != nil { reference.Description = *req.Description } + if req.WebsiteURL != nil { reference.WebsiteURL = *req.WebsiteURL } + if newFileID != nil { reference.LogoFileID = *newFileID } + reference.UpdatedAt = now if req.Rank != nil { @@ -258,7 +263,6 @@ func (s TrustCenterReferenceService) Update( return nil }) - if err != nil { s.cleanupS3Object(ctx, logoKey) return nil, err @@ -295,6 +299,7 @@ func (s TrustCenterReferenceService) GenerateLogoURL( ) (string, error) { reference := &coredata.TrustCenterReference{} file := &coredata.File{} + err := s.svc.pg.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { err := reference.LoadByID(ctx, tx, s.svc.scope, referenceID) if err != nil { @@ -308,7 +313,6 @@ func (s TrustCenterReferenceService) GenerateLogoURL( return nil }) - if err != nil { return "", nil } @@ -354,8 +358,11 @@ func (s TrustCenterReferenceService) uploadLogoFile( return gid.GID{}, "", fmt.Errorf("cannot load trust center: %w", err) } - var fileSize int64 - var fileContent io.ReadSeeker + var ( + fileSize int64 + fileContent io.ReadSeeker + ) + filename := file.Filename contentType := file.ContentType @@ -365,6 +372,7 @@ func (s TrustCenterReferenceService) uploadLogoFile( if err != nil { return gid.GID{}, "", fmt.Errorf("cannot determine file size: %w", err) } + fileSize = size _, err = readSeeker.Seek(0, io.SeekStart) @@ -374,18 +382,21 @@ func (s TrustCenterReferenceService) uploadLogoFile( } else { fileSize = file.Size } + fileContent = readSeeker } else { buf, err := io.ReadAll(file.Content) if err != nil { return gid.GID{}, "", fmt.Errorf("cannot read file: %w", err) } + fileSize = int64(len(buf)) fileContent = bytes.NewReader(buf) } if contentType == "" { contentType = "application/octet-stream" + if filename != "" { if detectedType := mime.TypeByExtension(filepath.Ext(filename)); detectedType != "" { contentType = detectedType diff --git a/pkg/probo/trust_center_service.go b/pkg/probo/trust_center_service.go index af3332e35..4af5d5325 100644 --- a/pkg/probo/trust_center_service.go +++ b/pkg/probo/trust_center_service.go @@ -121,7 +121,6 @@ func (s TrustCenterService) Get( return nil }, ) - if err != nil { return nil, fmt.Errorf("cannot load trust center: %w", err) } @@ -146,7 +145,6 @@ func (s TrustCenterService) GetByOrganizationID( return nil }, ) - if err != nil { return nil, err } @@ -162,8 +160,10 @@ func (s TrustCenterService) Update( return nil, nil, err } - var trustCenter *coredata.TrustCenter - var file *coredata.File + var ( + trustCenter *coredata.TrustCenter + file *coredata.File + ) err := s.svc.pg.WithTx( ctx, @@ -176,12 +176,15 @@ func (s TrustCenterService) Update( if req.Active != nil { trustCenter.Active = *req.Active } + if req.Slug != nil { trustCenter.Slug = *req.Slug } + if req.SearchEngineIndexing != nil { trustCenter.SearchEngineIndexing = *req.SearchEngineIndexing } + trustCenter.UpdatedAt = time.Now() if err := trustCenter.Update(ctx, conn, s.svc.scope); err != nil { @@ -198,7 +201,6 @@ func (s TrustCenterService) Update( return nil }, ) - if err != nil { return nil, nil, err } @@ -219,8 +221,10 @@ func (s TrustCenterService) UploadNDA( return nil, nil, fmt.Errorf("cannot generate object key: %w", err) } - var trustCenter *coredata.TrustCenter - var file *coredata.File + var ( + trustCenter *coredata.TrustCenter + file *coredata.File + ) err = s.svc.pg.WithTx( ctx, @@ -285,7 +289,6 @@ func (s TrustCenterService) UploadNDA( return nil }, ) - if err != nil { return nil, nil, err } @@ -317,7 +320,6 @@ func (s TrustCenterService) DeleteNDA( return nil }, ) - if err != nil { return nil, nil, err } @@ -333,8 +335,10 @@ func (s TrustCenterService) UpdateTrustCenterBrand( return nil, nil, err } - var trustCenter *coredata.TrustCenter - var ndaFile *coredata.File + var ( + trustCenter *coredata.TrustCenter + ndaFile *coredata.File + ) err := s.svc.pg.WithTx( ctx, @@ -354,6 +358,7 @@ func (s TrustCenterService) UpdateTrustCenterBrand( if err != nil { return fmt.Errorf("cannot upload logo file: %w", err) } + trustCenter.LogoFileID = &file.ID } } @@ -366,6 +371,7 @@ func (s TrustCenterService) UpdateTrustCenterBrand( if err != nil { return fmt.Errorf("cannot upload dark logo file: %w", err) } + trustCenter.DarkLogoFileID = &file.ID } } @@ -386,7 +392,6 @@ func (s TrustCenterService) UpdateTrustCenterBrand( return nil }, ) - if err != nil { return nil, nil, err } @@ -464,6 +469,7 @@ func (s TrustCenterService) GenerateNDAFileURL( expiresIn time.Duration, ) (*string, error) { var file *coredata.File + trustCenter := &coredata.TrustCenter{} err := s.svc.pg.WithConn( diff --git a/pkg/probo/webhook_subscription_service.go b/pkg/probo/webhook_subscription_service.go index f9273f451..7b38e0f9b 100644 --- a/pkg/probo/webhook_subscription_service.go +++ b/pkg/probo/webhook_subscription_service.go @@ -68,6 +68,7 @@ func (s WebhookSubscriptionService) ListForOrganizationID( cursor *page.Cursor[coredata.WebhookSubscriptionOrderField], ) (*page.Page[*coredata.WebhookSubscription, coredata.WebhookSubscriptionOrderField], error) { var subscriptions coredata.WebhookSubscriptions + organization := &coredata.Organization{} err := s.svc.pg.WithConn( @@ -91,7 +92,6 @@ func (s WebhookSubscriptionService) ListForOrganizationID( return nil }, ) - if err != nil { return nil, err } @@ -109,6 +109,7 @@ func (s WebhookSubscriptionService) CountForOrganizationID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { subscriptions := &coredata.WebhookSubscriptions{} + count, err = subscriptions.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID) if err != nil { return fmt.Errorf("cannot count webhook subscriptions: %w", err) @@ -117,7 +118,6 @@ func (s WebhookSubscriptionService) CountForOrganizationID( return nil }, ) - if err != nil { return 0, err } @@ -141,7 +141,6 @@ func (s WebhookSubscriptionService) Get( return nil }, ) - if err != nil { return nil, err } @@ -158,7 +157,9 @@ func (s WebhookSubscriptionService) Create( } now := time.Now() + var wc *coredata.WebhookSubscription + organization := &coredata.Organization{} err := s.svc.pg.WithTx( @@ -188,7 +189,6 @@ func (s WebhookSubscriptionService) Create( return nil }, ) - if err != nil { return nil, err } @@ -216,6 +216,7 @@ func (s WebhookSubscriptionService) Update( if req.EndpointURL != nil { wc.EndpointURL = *req.EndpointURL } + if req.SelectedEvents != nil { wc.SelectedEvents = req.SelectedEvents } @@ -229,7 +230,6 @@ func (s WebhookSubscriptionService) Update( return nil }, ) - if err != nil { return nil, err } @@ -253,7 +253,6 @@ func (s WebhookSubscriptionService) GetSigningSecret( return nil }, ) - if err != nil { return "", err } @@ -278,7 +277,6 @@ func (s WebhookSubscriptionService) ListEventsForSubscriptionID( return nil }, ) - if err != nil { return nil, err } @@ -296,8 +294,8 @@ func (s WebhookSubscriptionService) CountEventsForSubscriptionID( ctx, func(ctx context.Context, conn pg.Querier) (err error) { events := &coredata.WebhookEvents{} - count, err = events.CountBySubscriptionID(ctx, conn, s.svc.scope, webhookSubscriptionID) + count, err = events.CountBySubscriptionID(ctx, conn, s.svc.scope, webhookSubscriptionID) if err != nil { return fmt.Errorf("cannot count webhook events: %w", err) } @@ -305,7 +303,6 @@ func (s WebhookSubscriptionService) CountEventsForSubscriptionID( return nil }, ) - if err != nil { return 0, err } @@ -333,7 +330,6 @@ func (s WebhookSubscriptionService) Delete( return nil }, ) - if err != nil { return err } diff --git a/pkg/proboctl/cmdutil/cmdutil.go b/pkg/proboctl/cmdutil/cmdutil.go index 2086695fa..235504380 100644 --- a/pkg/proboctl/cmdutil/cmdutil.go +++ b/pkg/proboctl/cmdutil/cmdutil.go @@ -32,5 +32,6 @@ func (f *Factory) PgClient() (*pg.Client, error) { if f.PgDSN == "" { return nil, fmt.Errorf("set --pg-dsn or DATABASE_URL") } + return pgconn.NewPgClientFromDSN(f.PgDSN) } diff --git a/pkg/proboctl/pgconn/pgconn.go b/pkg/proboctl/pgconn/pgconn.go index dbb26c1d5..e4537cb86 100644 --- a/pkg/proboctl/pgconn/pgconn.go +++ b/pkg/proboctl/pgconn/pgconn.go @@ -48,6 +48,7 @@ func NewPgClientFromDSN(dsn string) (*pg.Client, error) { if u.Port() == "" { host = net.JoinHostPort(u.Hostname(), "5432") } + opts = append(opts, pg.WithAddr(host)) } diff --git a/pkg/proboctl/seed/common-third-parties/common_third_parties.go b/pkg/proboctl/seed/common-third-parties/common_third_parties.go index 9e9cebb17..fb52ad5b7 100644 --- a/pkg/proboctl/seed/common-third-parties/common_third_parties.go +++ b/pkg/proboctl/seed/common-third-parties/common_third_parties.go @@ -118,6 +118,7 @@ func NewCmdCommonThirdParties(f *cmdutil.Factory) *cobra.Command { inserted++ } else { updated++ + if err := party.LoadByName(ctx, tx, tp.Name); err != nil { return fmt.Errorf("cannot reload common third party %q: %w", tp.Name, err) } diff --git a/pkg/proboctl/seed/common-tracker-patterns/common_tracker_patterns.go b/pkg/proboctl/seed/common-tracker-patterns/common_tracker_patterns.go index 23246bb92..3cd6caecd 100644 --- a/pkg/proboctl/seed/common-tracker-patterns/common_tracker_patterns.go +++ b/pkg/proboctl/seed/common-tracker-patterns/common_tracker_patterns.go @@ -103,8 +103,10 @@ func NewCmdCommonTrackerPatterns(f *cmdutil.Factory) *cobra.Command { _, _ = fmt.Fprintf(out, "seeding %d common tracker patterns from Open Cookie Database\n", len(patterns)) - var inserted, updated, skipped int - var partiesCreated int + var ( + inserted, updated, skipped int + partiesCreated int + ) if err := pgClient.WithTx( ctx, @@ -122,6 +124,7 @@ func NewCmdCommonTrackerPatterns(f *cmdutil.Factory) *cobra.Command { if err != nil { _, _ = fmt.Fprintf(errOut, "warning: %v, skipping pattern %q\n", err, p.Pattern) skipped++ + continue } @@ -129,6 +132,7 @@ func NewCmdCommonTrackerPatterns(f *cmdutil.Factory) *cobra.Command { if err != nil { _, _ = fmt.Fprintf(errOut, "warning: %v, skipping pattern %q\n", err, p.Pattern) skipped++ + continue } @@ -207,6 +211,7 @@ func loadPatternsFromOCD(dir string) ([]trackerPatternData, error) { if err != nil { return nil, fmt.Errorf("cannot open %s: %w", ocdJSONFile, err) } + defer func() { _ = f.Close() }() var db map[string][]ocdEntry @@ -218,9 +223,11 @@ func loadPatternsFromOCD(dir string) ([]trackerPatternData, error) { for k := range db { platforms = append(platforms, k) } + sort.Strings(platforms) var patterns []trackerPatternData + for _, platform := range platforms { for _, e := range db[platform] { if e.Cookie == "" { @@ -273,6 +280,7 @@ func resolveThirdParty( if platformSlug == "" { return nil, nil } + if cached, ok := cache[platformSlug]; ok { return cached, nil } @@ -298,7 +306,9 @@ func resolveThirdParty( if err := party.LoadByID(ctx, tx, domainRow.CommonThirdPartyID); err != nil { return nil, fmt.Errorf("cannot load common third party by ID %s: %w", domainRow.CommonThirdPartyID, err) } + cache[platformSlug] = &party.ID + return &party.ID, nil } } @@ -336,6 +346,7 @@ func resolveThirdParty( *created++ cache[platformSlug] = &party.ID + return &party.ID, nil } @@ -349,6 +360,7 @@ func normalizeDomain(s string) string { if r == '\u200b' || r == '\ufeff' { return -1 } + return r }, s, @@ -357,6 +369,7 @@ func normalizeDomain(s string) string { if idx := strings.Index(s, " or "); idx != -1 { s = s[:idx] } + s = strings.TrimPrefix(s, "or ") if idx := strings.IndexByte(s, '('); idx != -1 { @@ -374,6 +387,7 @@ func normalizeDomain(s string) string { if s == "" || !domainValidRe.MatchString(s) { return "" } + return s } @@ -419,6 +433,7 @@ func parseRetentionPeriod(s string) *int { } var multiplier int + switch strings.ToLower(m[2]) { case "second", "seconds", "sec", "secs": multiplier = 1 @@ -439,6 +454,7 @@ func parseRetentionPeriod(s string) *int { } result := min(n*multiplier, math.MaxInt32) + return &result } diff --git a/pkg/probod/llm.go b/pkg/probod/llm.go index 1eda4ba53..7da4888d9 100644 --- a/pkg/probod/llm.go +++ b/pkg/probod/llm.go @@ -37,14 +37,17 @@ func (impl *Implm) resolveAgentClient( r prometheus.Registerer, ) (LLMAgentConfig, *llm.Client, error) { resolved := impl.cfg.Agents.ResolveAgent(agent) + providerCfg, ok := impl.cfg.Agents.Providers[resolved.Provider] if !ok { return LLMAgentConfig{}, nil, fmt.Errorf("unknown LLM provider %q for %s agent", resolved.Provider, name) } + client, err := buildLLMClient(providerCfg, l.Named("llm."+name), tp, r) if err != nil { return LLMAgentConfig{}, nil, fmt.Errorf("cannot create %s LLM client: %w", name, err) } + return resolved, client, nil } @@ -66,6 +69,7 @@ func buildLLMClient(cfg LLMProviderConfig, l *log.Logger, tp trace.TracerProvide cfg.APIKey, llmopenai.WithHTTPClient(httpClient), ) + return llm.NewClient( p, "openai", @@ -77,6 +81,7 @@ func buildLLMClient(cfg LLMProviderConfig, l *log.Logger, tp trace.TracerProvide cfg.APIKey, llmanthropic.WithHTTPClient(httpClient), ) + return llm.NewClient( p, "anthropic", diff --git a/pkg/probod/probod.go b/pkg/probod/probod.go index 41cb533fb..9349708c4 100644 --- a/pkg/probod/probod.go +++ b/pkg/probod/probod.go @@ -191,6 +191,7 @@ func (impl *Implm) Run( tp trace.TracerProvider, ) error { tracer := tp.Tracer("probod") + ctx, rootSpan := tracer.Start(parentCtx, "probod.Run") defer rootSpan.End() @@ -208,6 +209,7 @@ func (impl *Implm) Run( } wg := sync.WaitGroup{} + ctx, cancel := context.WithCancelCause(ctx) defer cancel(context.Canceled) @@ -267,6 +269,7 @@ func (impl *Implm) Run( } geolocService := geoloc.NewService(pgClient) + populated, err := geolocService.IsPopulated(ctx) if err != nil { l.ErrorCtx(ctx, "cannot check geoloc table", log.Error(err)) @@ -281,10 +284,12 @@ func (impl *Implm) Run( redirectURI := baseURL.WithPath(connector.CallbackPath).MustString() defaultConnectorRegistry := connector.NewConnectorRegistry() + for _, connectorCfg := range impl.cfg.Connectors { if oauth2c, ok := connectorCfg.Config.(*connector.OAuth2Connector); ok { connector.ApplyProviderDefaults(connectorCfg.Provider, redirectURI, oauth2c) } + if err := defaultConnectorRegistry.Register(connectorCfg.Provider, connectorCfg.Config); err != nil { return fmt.Errorf("cannot register connector: %w", err) } @@ -312,15 +317,20 @@ func (impl *Implm) Run( fileManagerService := filemanager.NewService(s3Client) - var samlCert *x509.Certificate - var samlKey *rsa.PrivateKey + var ( + samlCert *x509.Certificate + samlKey *rsa.PrivateKey + ) + if impl.cfg.Auth.SAML.Certificate != "" && impl.cfg.Auth.SAML.PrivateKey != "" { // Decode certificate certBlock, _ := pem.Decode([]byte(impl.cfg.Auth.SAML.Certificate)) if certBlock == nil { return fmt.Errorf("cannot decode SAML certificate PEM block") } + var err error + samlCert, err = x509.ParseCertificate(certBlock.Bytes) if err != nil { return fmt.Errorf("cannot parse SAML certificate: %w", err) @@ -331,7 +341,9 @@ func (impl *Implm) Run( if err != nil { return fmt.Errorf("cannot decode SAML private key: %w", err) } + var ok bool + samlKey, ok = signer.(*rsa.PrivateKey) if !ok { return fmt.Errorf("SAML private key is not an RSA key") @@ -342,8 +354,11 @@ func (impl *Implm) Run( return fmt.Errorf("cannot configure OAuth2 server: at least one signing key is required") } - var oauth2SigningKeys oauth2server.SigningKeys - var hasActive bool + var ( + oauth2SigningKeys oauth2server.SigningKeys + hasActive bool + ) + for _, keyCfg := range impl.cfg.Auth.OAuth2Server.SigningKeys { signer, err := pemutil.DecodePrivateKey([]byte(keyCfg.PrivateKey)) if err != nil { @@ -432,6 +447,7 @@ func (impl *Implm) Run( if err != nil { return fmt.Errorf("cannot decode ACME account key: %w", err) } + l.Info("using configured ACME account key") } @@ -569,6 +585,7 @@ func (impl *Implm) Run( apiServerCtx, stopApiServer := context.WithCancel(context.Background()) defer stopApiServer() + wg.Go( func() { if err := impl.runApiServer(apiServerCtx, l, r, tp, serverHandler); err != nil { @@ -596,6 +613,7 @@ func (impl *Implm) Run( worker.WithInterval(time.Duration(impl.cfg.Notifications.Mailer.MailerInterval)*time.Second), worker.WithMaxConcurrency(20), ) + wg.Go( func() { if err := sendingWorker.Run(mailerCtx); err != nil { @@ -608,6 +626,7 @@ func (impl *Implm) Run( slackSender := slack.NewSender(pgClient, l.Named("slack-sender"), encryptionKey, slack.Config{ Interval: time.Duration(impl.cfg.Notifications.Slack.SenderInterval) * time.Second, }) + wg.Go( func() { if err := slackSender.Run(slackSenderCtx); err != nil { @@ -623,6 +642,7 @@ func (impl *Implm) Run( EncryptionKey: encryptionKey, Host: baseURL.String(), }) + wg.Go( func() { if err := webhookSender.Run(webhookSenderCtx); err != nil { @@ -632,6 +652,7 @@ func (impl *Implm) Run( ) exportJobExporterCtx, stopExportJobExporter := context.WithCancel(context.Background()) + wg.Go( func() { if err := impl.runExportJob(exportJobExporterCtx, proboService, l.Named("export-job-exporter")); err != nil { @@ -646,6 +667,7 @@ func (impl *Implm) Run( worker.WithInterval(30*time.Second), ) documentPDFWorkerCtx, stopDocumentPDFWorker := context.WithCancel(context.Background()) + wg.Go( func() { if err := documentPDFWorker.Run(documentPDFWorkerCtx); err != nil { @@ -655,6 +677,7 @@ func (impl *Implm) Run( ) accessReviewWorkerCtx, stopAccessReviewWorker := context.WithCancel(context.Background()) + wg.Go( func() { if err := accessReviewService.Run(accessReviewWorkerCtx); err != nil { @@ -664,6 +687,7 @@ func (impl *Implm) Run( ) iamServiceCtx, stopIAMService := context.WithCancel(context.Background()) + wg.Go( func() { if err := iamService.Run(iamServiceCtx); err != nil { @@ -673,6 +697,7 @@ func (impl *Implm) Run( ) esignServiceCtx, stopESignService := context.WithCancel(context.Background()) + wg.Go( func() { if err := esignService.Run(esignServiceCtx, trustService.EmailPresenterConfigByOrganizationID); err != nil { @@ -683,6 +708,7 @@ func (impl *Implm) Run( trackerPatternAnalysisWorker := cookiebanner.NewPatternAnalysisWorker(cookieBannerService, pgClient, l) trackerPatternAnalysisWorkerCtx, stopTrackerPatternAnalysisWorker := context.WithCancel(context.Background()) + wg.Go( func() { if err := trackerPatternAnalysisWorker.Run(trackerPatternAnalysisWorkerCtx); err != nil { @@ -693,6 +719,7 @@ func (impl *Implm) Run( trackerMappingWorker := cookiebanner.NewTrackerMappingWorker(pgClient, l, trackerMappingCfg) trackerMappingWorkerCtx, stopTrackerMappingWorker := context.WithCancel(context.Background()) + wg.Go( func() { if err := trackerMappingWorker.Run(trackerMappingWorkerCtx); err != nil { @@ -703,6 +730,7 @@ func (impl *Implm) Run( mailingListWorker := mailman.NewMailingListWorker(mailmanService, pgClient, l.Named("mailing-list-worker")) mailingListWorkerCtx, stopMailingListWorker := context.WithCancel(context.Background()) + wg.Go( func() { if err := mailingListWorker.Run(mailingListWorkerCtx); err != nil { @@ -731,6 +759,7 @@ func (impl *Implm) Run( worker.WithMaxConcurrency(impl.cfg.EvidenceDescriber.MaxConcurrency), ) evidenceDescriptionWorkerCtx, stopEvidenceDescriptionWorker := context.WithCancel(context.Background()) + wg.Go( func() { if err := evidenceDescriptionWorker.Run(evidenceDescriptionWorkerCtx); err != nil { @@ -741,6 +770,7 @@ func (impl *Implm) Run( trustCenterServerCtx, stopTrustCenterServer := context.WithCancel(context.Background()) defer stopTrustCenterServer() + wg.Go( func() { if err := impl.runTrustCenterServer( @@ -811,6 +841,7 @@ func (impl *Implm) runApiServer( handler http.Handler, ) error { tracer := tp.Tracer("go.probo.inc/probo/pkg/probod") + ctx, span := tracer.Start(ctx, "probod.runApiServer") defer span.End() @@ -819,6 +850,7 @@ func (impl *Implm) runApiServer( span.RecordError(err) return fmt.Errorf("cannot build trusted proxy middleware: %w", err) } + handler = trustedProxyMiddleware(handler) apiServer := httpserver.NewServer( @@ -853,14 +885,17 @@ func (impl *Implm) runApiServer( l.Info("using proxy protocol", log.Any("trusted-proxies", impl.cfg.Api.ProxyProtocol.TrustedProxies)) } + defer func() { _ = listener.Close() }() serverErrCh := make(chan error, 1) + go func() { err := apiServer.Serve(listener) if err != nil && !errors.Is(err, http.ErrServerClosed) { serverErrCh <- fmt.Errorf("cannot server http request: %w", err) } + close(serverErrCh) }() @@ -872,6 +907,7 @@ func (impl *Implm) runApiServer( if err != nil { span.RecordError(err) } + return err case <-ctx.Done(): } @@ -888,6 +924,7 @@ func (impl *Implm) runApiServer( } span.AddEvent("API server shutdown complete") + return ctx.Err() } @@ -946,6 +983,7 @@ func (impl *Implm) runTrustCenterServer( encryptionKey cipher.EncryptionKey, ) error { tracer := tp.Tracer("go.probo.inc/probo/pkg/probod") + ctx, span := tracer.Start(ctx, "probod.runTrustCenterServer") defer span.End() @@ -968,6 +1006,7 @@ func (impl *Implm) runTrustCenterServer( if certProvisioningInterval == 0 { certProvisioningInterval = 30 * time.Second } + certProvisioner := certmanager.NewProvisioner(pgClient, acmeService, encryptionKey, impl.cfg.CustomDomains.CnameTarget, impl.cfg.CustomDomains.CAAIssuerDomain, certProvisioningInterval, impl.cfg.CustomDomains.ResolverAddr, l) g, ctx := errgroup.WithContext(ctx) @@ -1013,6 +1052,7 @@ func (impl *Implm) runTrustCenterServer( if err != nil { return fmt.Errorf("cannot listen on %q: %w", httpServer.Addr, err) } + defer func() { _ = listener.Close() }() if len(impl.cfg.TrustCenter.ProxyProtocol.TrustedProxies) > 0 { @@ -1033,6 +1073,7 @@ func (impl *Implm) runTrustCenterServer( if err := httpServer.Serve(listener); err != nil && err != http.ErrServerClosed { return fmt.Errorf("cannot serve http requests: %w", err) } + return nil }, ) @@ -1075,10 +1116,12 @@ func (impl *Implm) runTrustCenterServer( if errors.As(err, &noSNIErr) { return nil, nil } + if errors.Is(err, coredata.ErrResourceNotFound) { return nil, nil } } + return cert, err }, MinVersion: tls.VersionTLS12, @@ -1101,6 +1144,7 @@ func (impl *Implm) runTrustCenterServer( if err != nil { return fmt.Errorf("cannot listen on %q: %w", httpsServer.Addr, err) } + defer func() { _ = listener.Close() }() if len(impl.cfg.TrustCenter.ProxyProtocol.TrustedProxies) > 0 { diff --git a/pkg/probodconfig/auth_config.go b/pkg/probodconfig/auth_config.go index 4a31b6f79..270a7e8b1 100644 --- a/pkg/probodconfig/auth_config.go +++ b/pkg/probodconfig/auth_config.go @@ -68,6 +68,7 @@ func (c AuthConfig) GetPepperBytes() ([]byte, error) { if len(decoded) < 32 { return nil, fmt.Errorf("decoded pepper must be at least 32 bytes long") } + return decoded, nil } @@ -87,6 +88,7 @@ func (c AuthConfig) GetCookieSecretBytes() ([]byte, error) { if len(decoded) < 32 { return nil, fmt.Errorf("decoded cookie secret must be at least 32 bytes long") } + return decoded, nil } diff --git a/pkg/probodconfig/connector_config.go b/pkg/probodconfig/connector_config.go index b7fad3360..d13ad279e 100644 --- a/pkg/probodconfig/connector_config.go +++ b/pkg/probodconfig/connector_config.go @@ -57,6 +57,7 @@ func (c *Config) GetSlackSigningSecret() string { } } } + return "" } @@ -80,6 +81,7 @@ func (c *ConnectorConfig) UnmarshalJSON(data []byte) error { if err := json.NewDecoder(bytes.NewReader(tmp.Settings)).Decode(&settings); err != nil { return fmt.Errorf("cannot unmarshal settings: %w", err) } + c.Settings = settings } diff --git a/pkg/probodconfig/llm_config.go b/pkg/probodconfig/llm_config.go index 3f7dfa912..fe2aa00ce 100644 --- a/pkg/probodconfig/llm_config.go +++ b/pkg/probodconfig/llm_config.go @@ -66,14 +66,18 @@ func (c *AgentsConfig) ResolveAgent(agent LLMAgentConfig) LLMAgentConfig { if agent.Provider == "" { agent.Provider = c.Default.Provider } + if agent.ModelName == "" { agent.ModelName = c.Default.ModelName } + if agent.Temperature == nil { agent.Temperature = c.Default.Temperature } + if agent.MaxTokens == nil { agent.MaxTokens = c.Default.MaxTokens } + return agent } diff --git a/pkg/probodconfig/pg_config.go b/pkg/probodconfig/pg_config.go index 03719dea7..f42c90f39 100644 --- a/pkg/probodconfig/pg_config.go +++ b/pkg/probodconfig/pg_config.go @@ -92,14 +92,17 @@ func (cfg PgConfig) Options(options ...pg.Option) []pg.Option { if cfg.CACertBundle != "" { var certs []*x509.Certificate + pemData := []byte(cfg.CACertBundle) for len(pemData) > 0 { var block *pem.Block + block, pemData = pem.Decode(pemData) if block == nil { break } + if block.Type != "CERTIFICATE" { continue } diff --git a/pkg/probodconfig/saml_config.go b/pkg/probodconfig/saml_config.go index 74b525944..3613dfbc9 100644 --- a/pkg/probodconfig/saml_config.go +++ b/pkg/probodconfig/saml_config.go @@ -31,6 +31,7 @@ func (c SAMLConfig) SessionDurationTime() time.Duration { if c.SessionDuration == 0 { return 7 * 24 * time.Hour } + return time.Duration(c.SessionDuration) * time.Second } diff --git a/pkg/prosemirror/html_converter.go b/pkg/prosemirror/html_converter.go index a9212c1dd..1d6faed6b 100644 --- a/pkg/prosemirror/html_converter.go +++ b/pkg/prosemirror/html_converter.go @@ -46,14 +46,18 @@ func convertProseMirrorFromInlineHTML(raw string) ([]Node, error) { } c := &htmlBlockConverter{} + var out []Node + for _, root := range roots { nodes, err := c.convertInlineNode(root) if err != nil { return nil, err } + out = append(out, nodes...) } + if len(out) > 0 { return out, nil } @@ -62,6 +66,7 @@ func convertProseMirrorFromInlineHTML(raw string) ([]Node, error) { if plain == "" { return nil, nil } + return []Node{{Type: NodeText, Text: &plain}}, nil } @@ -75,6 +80,7 @@ func convertProseMirrorFromHTMLBlock(raw string) ([]Node, error) { if err != nil { return nil, fmt.Errorf("cannot convert html block to prosemirror: %w", err) } + if len(nodes) > 0 { return nodes, nil } @@ -83,6 +89,7 @@ func convertProseMirrorFromHTMLBlock(raw string) ([]Node, error) { if plain == "" { return nil, nil } + return []Node{paragraphWithPlainText(plain)}, nil } @@ -109,12 +116,17 @@ func plainTextFromHTMLFragment(htmlStr string) string { if err != nil { return "" } - var b strings.Builder - var walk func(*html.Node) + + var ( + b strings.Builder + walk func(*html.Node) + ) + walk = func(n *html.Node) { if n.Type == html.TextNode { b.WriteString(n.Data) } + for c := n.FirstChild; c != nil; c = c.NextSibling { walk(c) } @@ -122,6 +134,7 @@ func plainTextFromHTMLFragment(htmlStr string) string { for _, root := range roots { walk(root) } + return b.String() } @@ -132,14 +145,18 @@ func htmlFragmentToProseMirrorBlocks(htmlStr string) ([]Node, error) { } c := &htmlBlockConverter{} + var out []Node + for _, root := range roots { nodes, err := c.convertTopLevel(root) if err != nil { return nil, err } + out = append(out, nodes...) } + return out, nil } @@ -154,6 +171,7 @@ func (c *htmlBlockConverter) convertTopLevel(n *html.Node) ([]Node, error) { if t == "" { return nil, nil } + return []Node{paragraphWithPlainText(t)}, nil case html.ElementNode: return c.convertBlockElement(n) @@ -169,17 +187,21 @@ func (c *htmlBlockConverter) convertBlockElement(n *html.Node) ([]Node, error) { if err != nil { return nil, err } + return []Node{{Type: NodeParagraph, Content: inlines}}, nil case "h1", "h2", "h3", "h4", "h5", "h6": level := int(n.Data[1] - '0') + inlines, err := c.convertInlineFragments(n) if err != nil { return nil, err } + attrs, err := json.Marshal(HeadingAttrs{Level: level}) if err != nil { return nil, fmt.Errorf("cannot marshal heading attrs: %w", err) } + return []Node{{ Type: NodeHeading, Attrs: attrs, @@ -207,9 +229,11 @@ func (c *htmlBlockConverter) convertBlockElement(n *html.Node) ([]Node, error) { if err != nil { return nil, err } + if img == nil { return nil, nil } + return []Node{{ Type: NodeParagraph, Content: []Node{*img}, @@ -228,23 +252,29 @@ func (c *htmlBlockConverter) unwrapBlockElement(n *html.Node) ([]Node, error) { if err != nil { return nil, err } + if len(inlines) == 0 { return nil, nil } + return []Node{{Type: NodeParagraph, Content: inlines}}, nil } + return c.convertBlockChildren(n) } func (c *htmlBlockConverter) convertBlockChildren(n *html.Node) ([]Node, error) { var out []Node + for ch := n.FirstChild; ch != nil; ch = ch.NextSibling { nodes, err := c.convertTopLevel(ch) if err != nil { return nil, err } + out = append(out, nodes...) } + return out, nil } @@ -254,44 +284,58 @@ func (c *htmlBlockConverter) convertBlockquote(n *html.Node) ([]Node, error) { if err != nil { return nil, err } + return []Node{{Type: NodeBlockquote, Content: inner}}, nil } + inlines, err := c.convertInlineFragments(n) if err != nil { return nil, err } + var content []Node if len(inlines) > 0 { content = []Node{{Type: NodeParagraph, Content: inlines}} } + return []Node{{Type: NodeBlockquote, Content: content}}, nil } func (c *htmlBlockConverter) convertPre(n *html.Node) ([]Node, error) { - var lang *string - var textBuf strings.Builder + var ( + lang *string + textBuf strings.Builder + ) + for ch := n.FirstChild; ch != nil; ch = ch.NextSibling { if ch.Type == html.ElementNode && ch.Data == "code" { lang = codeLanguageFromClass(attrVal(ch, "class")) + var walkText func(*html.Node) + walkText = func(x *html.Node) { if x.Type == html.TextNode { textBuf.WriteString(x.Data) } + for cc := x.FirstChild; cc != nil; cc = cc.NextSibling { walkText(cc) } } walkText(ch) + break } } + if textBuf.Len() == 0 { var walkText func(*html.Node) + walkText = func(x *html.Node) { if x.Type == html.TextNode { textBuf.WriteString(x.Data) } + for cc := x.FirstChild; cc != nil; cc = cc.NextSibling { walkText(cc) } @@ -300,6 +344,7 @@ func (c *htmlBlockConverter) convertPre(n *html.Node) ([]Node, error) { } content := textBuf.String() + attrs, err := json.Marshal(CodeBlockAttrs{Language: lang}) if err != nil { return nil, fmt.Errorf("cannot marshal code block attrs: %w", err) @@ -309,6 +354,7 @@ func (c *htmlBlockConverter) convertPre(n *html.Node) ([]Node, error) { if content != "" { textNodes = []Node{{Type: NodeText, Text: &content}} } + return []Node{{ Type: NodeCodeBlock, Attrs: attrs, @@ -326,39 +372,49 @@ func codeLanguageFromClass(class string) *string { } } } + return nil } func (c *htmlBlockConverter) convertList(n *html.Node, ordered bool) ([]Node, error) { var items []Node + for li := n.FirstChild; li != nil; li = li.NextSibling { if li.Type != html.ElementNode || li.Data != "li" { continue } + body, err := c.convertListItem(li) if err != nil { return nil, err } + if len(body) == 0 { continue } + items = append(items, Node{Type: NodeListItem, Content: body}) } + if len(items) == 0 { return nil, nil } + if ordered { start := parseOlStart(n) + attrs, err := json.Marshal(OrderedListAttrs{Start: start}) if err != nil { return nil, fmt.Errorf("cannot marshal ordered list attrs: %w", err) } + return []Node{{ Type: NodeOrderedList, Attrs: attrs, Content: items, }}, nil } + return []Node{{ Type: NodeBulletList, Content: items, @@ -370,10 +426,12 @@ func parseOlStart(n *html.Node) int { if s == "" { return 1 } + v, err := strconv.Atoi(strings.TrimSpace(s)) if err != nil || v < 1 { return 1 } + return v } @@ -381,13 +439,16 @@ func (c *htmlBlockConverter) convertListItem(li *html.Node) ([]Node, error) { if hasBlockElementChild(li) { return c.convertBlockChildren(li) } + inlines, err := c.convertInlineFragments(li) if err != nil { return nil, err } + if len(inlines) == 0 { return nil, nil } + return []Node{{Type: NodeParagraph, Content: inlines}}, nil } @@ -397,6 +458,7 @@ func hasBlockElementChild(n *html.Node) bool { return true } } + return false } @@ -419,10 +481,12 @@ func blockTagName(name string) bool { // tables cannot contribute rows to the outer table. func collectTableRows(table *html.Node) []*html.Node { var rows []*html.Node + for ch := table.FirstChild; ch != nil; ch = ch.NextSibling { if ch.Type != html.ElementNode { continue } + switch ch.Data { case "thead", "tbody", "tfoot": for tr := ch.FirstChild; tr != nil; tr = tr.NextSibling { @@ -438,6 +502,7 @@ func collectTableRows(table *html.Node) []*html.Node { // Ignore other direct children (e.g. invalid markup). } } + return rows } @@ -445,29 +510,38 @@ func (c *htmlBlockConverter) convertTable(n *html.Node) ([]Node, error) { rows := collectTableRows(n) var rowNodes []Node + for _, tr := range rows { row, err := c.convertTableRow(tr) if err != nil { return nil, err } + if row != nil { rowNodes = append(rowNodes, *row) } } + if len(rowNodes) == 0 { return nil, nil } + return []Node{{Type: NodeTable, Content: rowNodes}}, nil } func (c *htmlBlockConverter) convertTableRow(tr *html.Node) (*Node, error) { var cells []Node + for ch := tr.FirstChild; ch != nil; ch = ch.NextSibling { if ch.Type != html.ElementNode { continue } - var cell *Node - var err error + + var ( + cell *Node + err error + ) + switch ch.Data { case "th": cell, err = c.convertTableCell(ch, NodeTableHeader) @@ -476,16 +550,20 @@ func (c *htmlBlockConverter) convertTableRow(tr *html.Node) (*Node, error) { default: continue } + if err != nil { return nil, err } + if cell != nil { cells = append(cells, *cell) } } + if len(cells) == 0 { return nil, nil } + return &Node{Type: NodeTableRow, Content: cells}, nil } @@ -494,27 +572,34 @@ func (c *htmlBlockConverter) convertTableCell(n *html.Node, typ NodeType) (*Node Colspan: tableSpanFromHTML(n, "colspan"), Rowspan: tableSpanFromHTML(n, "rowspan"), } + attrs, err := json.Marshal(cellAttrs) if err != nil { return nil, fmt.Errorf("cannot marshal table cell attrs: %w", err) } + inlines, err := c.convertInlineFragments(n) if err != nil { return nil, err } + content := []Node{{Type: NodeParagraph, Content: inlines}} + return &Node{Type: typ, Attrs: attrs, Content: content}, nil } func (c *htmlBlockConverter) convertInlineFragments(parent *html.Node) ([]Node, error) { var out []Node + for ch := parent.FirstChild; ch != nil; ch = ch.NextSibling { nodes, err := c.convertInlineNode(ch) if err != nil { return nil, err } + out = append(out, nodes...) } + return out, nil } @@ -524,7 +609,9 @@ func (c *htmlBlockConverter) convertInlineNode(n *html.Node) ([]Node, error) { if n.Data == "" { return nil, nil } + t := n.Data + return []Node{{ Type: NodeText, Text: &t, @@ -562,6 +649,7 @@ func (c *htmlBlockConverter) convertInlineElement(n *html.Node) ([]Node, error) if err != nil || img == nil { return nil, err } + return []Node{*img}, nil case "span": return c.convertInlineFragments(n) @@ -574,9 +662,11 @@ func (c *htmlBlockConverter) withMark(m Mark, n *html.Node) ([]Node, error) { c.marks = append(c.marks, m) nodes, err := c.convertInlineFragments(n) c.marks = c.marks[:len(c.marks)-1] + if err != nil { return nil, err } + return nodes, nil } @@ -585,21 +675,26 @@ func (c *htmlBlockConverter) convertAnchor(n *html.Node) ([]Node, error) { if href == "" { return c.convertInlineFragments(n) } + var title *string if t := attrVal(n, "title"); t != "" { title = &t } + attrs, err := json.Marshal(LinkAttrs{Href: safeLinkHref(href), Title: title}) if err != nil { return nil, fmt.Errorf("cannot marshal link attrs: %w", err) } + m := Mark{Type: MarkLink, Attrs: attrs} c.marks = append(c.marks, m) nodes, err := c.convertInlineFragments(n) c.marks = c.marks[:len(c.marks)-1] + if err != nil { return nil, err } + return nodes, nil } @@ -608,17 +703,21 @@ func (c *htmlBlockConverter) convertImageElement(n *html.Node) (*Node, error) { if src == "" { return nil, nil } + var alt, title *string if a := attrVal(n, "alt"); a != "" { alt = &a } + if t := attrVal(n, "title"); t != "" { title = &t } + attrs, err := json.Marshal(ImageAttrs{Src: src, Alt: alt, Title: title}) if err != nil { return nil, fmt.Errorf("cannot marshal image attrs: %w", err) } + return &Node{Type: NodeImage, Attrs: attrs}, nil } @@ -628,6 +727,7 @@ func attrVal(n *html.Node, key string) string { return a.Val } } + return "" } @@ -639,9 +739,11 @@ func tableSpanFromHTML(n *html.Node, key string) int { if s == "" { return 1 } + v, err := strconv.Atoi(s) if err != nil || v < 1 { return 1 } + return v } diff --git a/pkg/prosemirror/html_converter_test.go b/pkg/prosemirror/html_converter_test.go index 625707da6..59ecfc6bf 100644 --- a/pkg/prosemirror/html_converter_test.go +++ b/pkg/prosemirror/html_converter_test.go @@ -45,17 +45,22 @@ func TestParseMarkdown_BlockHTMLDivPreservesInlineMarks(t *testing.T) { require.Equal(t, NodeParagraph, p.Type) var foundStrong bool + for _, ch := range p.Content { if ch.Type != NodeText || ch.Text == nil { continue } + if *ch.Text != "world" { continue } + require.Len(t, ch.Marks, 1) assert.Equal(t, MarkStrong, ch.Marks[0].Type) + foundStrong = true } + assert.True(t, foundStrong, "expected bold mark on 'world' inside a single paragraph") } @@ -230,11 +235,13 @@ func TestParseMarkdown_InlineRawHTML(t *testing.T) { require.Equal(t, NodeParagraph, p.Type) var joined strings.Builder + for _, ch := range p.Content { require.Equal(t, NodeText, ch.Type) require.NotNil(t, ch.Text) joined.WriteString(*ch.Text) } + // Sanitized HTML: span is unwrapped to plain text content. assert.Equal(t, "before x after", joined.String()) } @@ -261,12 +268,15 @@ func TestParseMarkdown_InlineRawHTMLScriptStripped(t *testing.T) { require.NoError(t, err) require.Len(t, doc.Content, 1) p := doc.Content[0] + var joined strings.Builder + for _, ch := range p.Content { if ch.Type == NodeText && ch.Text != nil { joined.WriteString(*ch.Text) } } + assert.NotContains(t, joined.String(), "script") assert.NotContains(t, joined.String(), "evil") assert.Contains(t, joined.String(), "hi") @@ -283,20 +293,24 @@ func TestParseMarkdown_InlineRawHTMLWithOuterBold(t *testing.T) { require.GreaterOrEqual(t, len(p.Content), 3) var joined strings.Builder + for _, ch := range p.Content { require.Equal(t, NodeText, ch.Type) require.NotNil(t, ch.Text) joined.WriteString(*ch.Text) } + assert.Equal(t, "a b c", joined.String()) var mid *Node + for i := range p.Content { if p.Content[i].Text != nil && *p.Content[i].Text == "b" { mid = &p.Content[i] break } } + require.NotNil(t, mid, "expected inner as text node b") require.GreaterOrEqual(t, len(mid.Marks), 2) assert.Equal(t, MarkStrong, mid.Marks[0].Type) diff --git a/pkg/prosemirror/html_renderer.go b/pkg/prosemirror/html_renderer.go index c59b0782e..43c53901a 100644 --- a/pkg/prosemirror/html_renderer.go +++ b/pkg/prosemirror/html_renderer.go @@ -29,6 +29,7 @@ func RenderHTML(node Node) (string, error) { if err := renderNode(&buf, node); err != nil { return "", err } + return buf.String(), nil } @@ -38,54 +39,70 @@ func renderNode(buf *bytes.Buffer, n Node) error { return renderChildren(buf, n.Content) case NodeParagraph: buf.WriteString("

") + if err := renderChildren(buf, n.Content); err != nil { return err } + buf.WriteString("

") case NodeHeading: attrs, err := n.HeadingAttrs() if err != nil { return fmt.Errorf("cannot render heading node: %w", err) } + if attrs.Level < 1 || attrs.Level > 6 { return fmt.Errorf("cannot render heading node: invalid level %d", attrs.Level) } + level := strconv.Itoa(attrs.Level) + buf.WriteString("') + if err := renderChildren(buf, n.Content); err != nil { return err } + buf.WriteString("') case NodeBlockquote: buf.WriteString("
") + if err := renderChildren(buf, n.Content); err != nil { return err } + buf.WriteString("
") case NodeCodeBlock: attrs, err := n.CodeBlockAttrs() if err != nil { return fmt.Errorf("cannot render code block node: %w", err) } + if attrs.Language != nil && *attrs.Language == "mermaid" { buf.WriteString(`
`)
+
 			if err := renderChildren(buf, n.Content); err != nil {
 				return err
 			}
+
 			buf.WriteString("
") } else { buf.WriteString("
')
+
 			if err := renderChildren(buf, n.Content); err != nil {
 				return err
 			}
+
 			buf.WriteString("
") } case NodeHorizontalRule: @@ -99,55 +116,73 @@ func renderNode(buf *bytes.Buffer, n Node) error { if err != nil { return fmt.Errorf("cannot render image node: %w", err) } + buf.WriteString("') case NodeBulletList: buf.WriteString("
    ") + if err := renderChildren(buf, n.Content); err != nil { return err } + buf.WriteString("
") case NodeOrderedList: attrs, err := n.OrderedListAttrs() if err != nil { return fmt.Errorf("cannot render ordered list node: %w", err) } + buf.WriteString("') + if err := renderChildren(buf, n.Content); err != nil { return err } + buf.WriteString("") case NodeListItem: buf.WriteString("
  • ") + if err := renderChildren(buf, n.Content); err != nil { return err } + buf.WriteString("
  • ") case NodeTable: buf.WriteString("") + if err := renderChildren(buf, n.Content); err != nil { return err } + buf.WriteString("
    ") case NodeTableRow: buf.WriteString("") + if err := renderChildren(buf, n.Content); err != nil { return err } + buf.WriteString("") case NodeTableCell: return renderTableCell(buf, n, "td") @@ -156,6 +191,7 @@ func renderNode(buf *bytes.Buffer, n Node) error { default: return fmt.Errorf("cannot render node: unknown type %q", n.Type) } + return nil } @@ -165,6 +201,7 @@ func renderChildren(buf *bytes.Buffer, nodes []Node) error { return err } } + return nil } @@ -172,15 +209,19 @@ func renderText(buf *bytes.Buffer, n Node) error { if n.Text == nil { return fmt.Errorf("cannot render text node: text is nil") } + for _, m := range n.Marks { if err := openMark(buf, m); err != nil { return err } } + buf.WriteString(html.EscapeString(*n.Text)) + for i := len(n.Marks) - 1; i >= 0; i-- { closeMark(buf, n.Marks[i]) } + return nil } @@ -201,25 +242,32 @@ func openMark(buf *bytes.Buffer, m Mark) error { if err != nil { return fmt.Errorf("cannot render link mark: %w", err) } + buf.WriteString("') default: return fmt.Errorf("cannot render mark: unknown type %q", m.Type) } + return nil } @@ -245,28 +293,37 @@ func renderTableCell(buf *bytes.Buffer, n Node, tag string) error { if err != nil { return fmt.Errorf("cannot render %s node: %w", tag, err) } + buf.WriteByte('<') buf.WriteString(tag) + if attrs.Colspan > 1 { writeAttr(buf, "colspan", strconv.Itoa(attrs.Colspan)) } + if attrs.Rowspan > 1 { writeAttr(buf, "rowspan", strconv.Itoa(attrs.Rowspan)) } + if len(attrs.Colwidth) > 0 { total := 0 for _, w := range attrs.Colwidth { total += w } + writeAttr(buf, "style", "min-width: "+strconv.Itoa(total)+"px") } + buf.WriteByte('>') + if err := renderChildren(buf, n.Content); err != nil { return err } + buf.WriteString("') + return nil } @@ -285,6 +342,7 @@ func linkRelToEmit(attrs LinkAttrs) string { if blanksTarget { return ensureNoopener(s) } + return s } } @@ -292,6 +350,7 @@ func linkRelToEmit(attrs LinkAttrs) string { if blanksTarget { return linkRelBlankTargetDefault } + return "" } @@ -303,6 +362,7 @@ func ensureNoopener(rel string) string { return rel } } + return rel + " noopener" } @@ -323,19 +383,24 @@ func safeLinkHref(href string) string { if href == "" { return "#" } + if href[0] == '#' { return href } + if strings.HasPrefix(href, "/") { if len(href) > 1 && (href[1] == '/' || href[1] == '\\') { return "#" } + return href } + u, err := url.Parse(href) if err != nil { return "#" } + if u.Scheme != "" { switch strings.ToLower(u.Scheme) { case "http", "https", "mailto", "tel": @@ -344,9 +409,11 @@ func safeLinkHref(href string) string { return "#" } } + if u.Host != "" { return "#" } + return href } @@ -359,16 +426,20 @@ func safeImageSrc(src string) string { if src == "" { return "" } + if strings.HasPrefix(src, "/") { if len(src) > 1 && (src[1] == '/' || src[1] == '\\') { return "" } + return src } + u, err := url.Parse(src) if err != nil { return "" } + if u.Scheme != "" { switch strings.ToLower(u.Scheme) { case "http", "https", "data": @@ -377,8 +448,10 @@ func safeImageSrc(src string) string { return "" } } + if u.Host != "" { return "" } + return src } diff --git a/pkg/prosemirror/html_renderer_test.go b/pkg/prosemirror/html_renderer_test.go index 33e98f625..34df0a655 100644 --- a/pkg/prosemirror/html_renderer_test.go +++ b/pkg/prosemirror/html_renderer_test.go @@ -64,7 +64,9 @@ func TestRenderHTML_HeadingLevels(t *testing.T) { "level "+string(rune('0'+tc.level)), func(t *testing.T) { t.Parallel() + raw := `{"type":"heading","attrs":{"level":` + string(rune('0'+tc.level)) + `},"content":[{"type":"text","text":"X"}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -80,6 +82,7 @@ func TestRenderHTML_HeadingInvalidLevel(t *testing.T) { t.Parallel() raw := `{"type":"heading","attrs":{"level":7},"content":[{"type":"text","text":"X"}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -92,6 +95,7 @@ func TestRenderHTML_CodeBlockWithLanguage(t *testing.T) { t.Parallel() raw := `{"type":"codeBlock","attrs":{"language":"go"},"content":[{"type":"text","text":"fmt.Println()"}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -104,6 +108,7 @@ func TestRenderHTML_CodeBlockMermaid(t *testing.T) { t.Parallel() raw := `{"type":"codeBlock","attrs":{"language":"mermaid"},"content":[{"type":"text","text":"graph TD\n A-->B"}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -116,6 +121,7 @@ func TestRenderHTML_CodeBlockWithoutLanguage(t *testing.T) { t.Parallel() raw := `{"type":"codeBlock","attrs":{"language":null},"content":[{"type":"text","text":"hello"}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -128,6 +134,7 @@ func TestRenderHTML_OrderedListWithStart(t *testing.T) { t.Parallel() raw := `{"type":"orderedList","attrs":{"start":5,"type":null},"content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"item"}]}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -140,6 +147,7 @@ func TestRenderHTML_TableCellColspan(t *testing.T) { t.Parallel() raw := `{"type":"tableCell","attrs":{"colspan":2,"rowspan":1,"colwidth":null},"content":[{"type":"paragraph","content":[{"type":"text","text":"wide"}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -152,6 +160,7 @@ func TestRenderHTML_TableCellColwidth(t *testing.T) { t.Parallel() raw := `{"type":"tableCell","attrs":{"colspan":1,"rowspan":1,"colwidth":[100]},"content":[{"type":"paragraph","content":[{"type":"text","text":"X"}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -179,6 +188,7 @@ func TestRenderHTML_LinkAllAttrs(t *testing.T) { t.Parallel() raw := `{"type":"text","marks":[{"type":"link","attrs":{"href":"https://example.com","target":"_blank","rel":"noopener","class":"btn","title":"Click"}}],"text":"hi"}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -191,6 +201,7 @@ func TestRenderHTML_LinkMinimalAttrs(t *testing.T) { t.Parallel() raw := `{"type":"text","marks":[{"type":"link","attrs":{"href":"https://example.com","target":null,"rel":null,"class":null,"title":null}}],"text":"hi"}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -247,6 +258,7 @@ func TestRenderHTML_LinkBlankTargetDefaultRel(t *testing.T) { tc.name, func(t *testing.T) { t.Parallel() + var n Node require.NoError(t, json.Unmarshal([]byte(tc.raw), &n)) @@ -283,17 +295,21 @@ func TestRenderHTML_LinkSanitizesDangerousHrefs(t *testing.T) { tc.name, func(t *testing.T) { t.Parallel() + hrefJSON, err := json.Marshal(tc.href) require.NoError(t, err) + raw := fmt.Sprintf( `{"type":"text","marks":[{"type":"link","attrs":{"href":%s,"target":null,"rel":null,"class":null,"title":null}}],"text":"x"}`, string(hrefJSON), ) + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) got, err := RenderHTML(n) require.NoError(t, err) + want := fmt.Sprintf(`x`, html.EscapeString(tc.wantHref)) assert.Equal(t, want, got) }, @@ -305,6 +321,7 @@ func TestRenderHTML_Image(t *testing.T) { t.Parallel() raw := `{"type":"image","attrs":{"src":"https://example.com/img.png","alt":"A photo","title":"My image"}}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -336,17 +353,21 @@ func TestRenderHTML_ImageSanitizesDangerousSrc(t *testing.T) { tc.name, func(t *testing.T) { t.Parallel() + srcJSON, err := json.Marshal(tc.src) require.NoError(t, err) + raw := fmt.Sprintf( `{"type":"image","attrs":{"src":%s}}`, string(srcJSON), ) + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) got, err := RenderHTML(n) require.NoError(t, err) + want := fmt.Sprintf(``, html.EscapeString(tc.wantSrc)) assert.Equal(t, want, got) }, @@ -358,6 +379,7 @@ func TestRenderHTML_MultipleMarks(t *testing.T) { t.Parallel() raw := `{"type":"text","marks":[{"type":"bold"},{"type":"italic"}],"text":"hello"}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) diff --git a/pkg/prosemirror/markdown_converter.go b/pkg/prosemirror/markdown_converter.go index eb43b79dd..caed089cb 100644 --- a/pkg/prosemirror/markdown_converter.go +++ b/pkg/prosemirror/markdown_converter.go @@ -63,9 +63,11 @@ func normalizeCodeBlockContent(content string) string { if content == "" { return content } + if strings.HasSuffix(content, "\n") && !strings.HasSuffix(content, "\n\n") { return strings.TrimSuffix(content, "\n") } + return content } @@ -82,6 +84,7 @@ func (c *converter) convertChildren(n ast.Node) ([]Node, error) { if err != nil { return nil, err } + nodes = append(nodes, converted...) } @@ -97,12 +100,15 @@ func (c *converter) convertInlineChildren(n ast.Node) ([]Node, error) { for ch := n.FirstChild(); ch != nil; { if ch.Kind() == ast.KindRawHTML { run, next := c.collectRawHTMLRun(ch) + inodes, err := convertProseMirrorFromInlineHTML(run) if err != nil { return nil, err } + nodes = append(nodes, prependOuterMarks(copyMarks(c.marks), inodes)...) ch = next + continue } @@ -110,6 +116,7 @@ func (c *converter) convertInlineChildren(n ast.Node) ([]Node, error) { if err != nil { return nil, err } + nodes = append(nodes, converted...) ch = ch.NextSibling() } @@ -122,6 +129,7 @@ func (c *converter) convertInlineChildren(n ast.Node) ([]Node, error) { // sibling not consumed (or nil). func (c *converter) collectRawHTMLRun(start ast.Node) (run string, next ast.Node) { var buf bytes.Buffer + ch := start for ch != nil { switch ch.Kind() { @@ -134,6 +142,7 @@ func (c *converter) collectRawHTMLRun(start ast.Node) (run string, next ast.Node case ast.KindText: t := ch.(*ast.Text) buf.Write(t.Segment.Value(c.source)) + if t.SoftLineBreak() { buf.WriteByte(' ') } @@ -142,8 +151,10 @@ func (c *converter) collectRawHTMLRun(start ast.Node) (run string, next ast.Node default: return buf.String(), ch } + ch = ch.NextSibling() } + return buf.String(), nil } @@ -198,6 +209,7 @@ func (c *converter) convertNode(n ast.Node) ([]Node, error) { case goldmarkast.KindTableCell: return nil, fmt.Errorf("cannot convert table cell outside of a table row") } + return nil, fmt.Errorf("cannot convert markdown node of kind %s", n.Kind()) } } @@ -255,6 +267,7 @@ func (c *converter) convertFencedCodeBlock(n *ast.FencedCodeBlock) ([]Node, erro content := normalizeCodeBlockContent(buf.String()) var lang *string + if n.Language(c.source) != nil { l := string(n.Language(c.source)) lang = &l @@ -321,6 +334,7 @@ func (c *converter) convertList(n *ast.List) ([]Node, error) { if err != nil { return nil, fmt.Errorf("cannot marshal ordered list attrs: %w", err) } + return []Node{{ Type: NodeOrderedList, Content: children, @@ -376,6 +390,7 @@ func (c *converter) convertText(n *ast.Text) ([]Node, error) { if n.SoftLineBreak() { content += " " } + if content == "" { return nil, nil } @@ -417,6 +432,7 @@ func (c *converter) convertEmphasis(n *ast.Emphasis) ([]Node, error) { c.marks = append(c.marks, mark) children, err := c.convertInlineChildren(n) c.marks = c.marks[:len(c.marks)-1] + if err != nil { return nil, err } @@ -466,6 +482,7 @@ func (c *converter) convertLink(n *ast.Link) ([]Node, error) { c.marks = append(c.marks, Mark{Type: MarkLink, Attrs: attrs}) children, err := c.convertInlineChildren(n) c.marks = c.marks[:len(c.marks)-1] + if err != nil { return nil, err } @@ -477,6 +494,7 @@ func (c *converter) convertAutoLink(n *ast.AutoLink) ([]Node, error) { url := string(n.URL(c.source)) linkAttrs := LinkAttrs{Href: safeLinkHref(url)} + attrs, err := json.Marshal(linkAttrs) if err != nil { return nil, fmt.Errorf("cannot marshal link attrs: %w", err) @@ -496,19 +514,24 @@ func (c *converter) convertRawHTML(n ast.Node) ([]Node, error) { if !ok { return nil, fmt.Errorf("cannot convert raw html: unexpected node type %T", n) } + var buf bytes.Buffer + for i := 0; i < raw.Segments.Len(); i++ { seg := raw.Segments.At(i) buf.Write(seg.Value(c.source)) } + run := buf.String() if run == "" { return nil, nil } + nodes, err := convertProseMirrorFromInlineHTML(run) if err != nil { return nil, err } + return prependOuterMarks(copyMarks(c.marks), nodes), nil } @@ -519,6 +542,7 @@ func (c *converter) convertHTMLBlock(n *ast.HTMLBlock) ([]Node, error) { line := n.Lines().At(i) buf.Write(line.Value(c.source)) } + if n.HasClosure() { buf.Write(n.ClosureLine.Value(c.source)) } @@ -535,6 +559,7 @@ func (c *converter) convertStrikethrough(n ast.Node) ([]Node, error) { c.marks = append(c.marks, Mark{Type: MarkStrike}) children, err := c.convertInlineChildren(n) c.marks = c.marks[:len(c.marks)-1] + if err != nil { return nil, err } @@ -550,6 +575,7 @@ func (c *converter) convertTable(n ast.Node) ([]Node, error) { if err != nil { return nil, err } + rows = append(rows, converted...) } @@ -622,10 +648,12 @@ func (c *converter) convertTableCells(row ast.Node, cellType NodeType) ([]Node, func (c *converter) convertTableCellContent(cell ast.Node) ([]Node, error) { if c.cellHasBlockHTML(cell) { raw := c.collectCellRawContent(cell) + nodes, err := convertProseMirrorFromHTMLBlock(raw) if err != nil { return nil, err } + if len(nodes) > 0 { return nodes, nil } @@ -635,6 +663,7 @@ func (c *converter) convertTableCellContent(cell ast.Node) ([]Node, error) { if err != nil { return nil, err } + return []Node{{Type: NodeParagraph, Content: inlineContent}}, nil } @@ -643,15 +672,18 @@ func (c *converter) cellHasBlockHTML(cell ast.Node) bool { if ch.Kind() != ast.KindRawHTML { continue } + raw := ch.(*ast.RawHTML) for i := 0; i < raw.Segments.Len(); i++ { seg := raw.Segments.At(i) + val := strings.ToLower(string(seg.Value(c.source))) if containsBlockOpenTag(val) { return true } } } + return false } @@ -664,11 +696,13 @@ func containsBlockOpenTag(s string) bool { return true } } + return false } func (c *converter) collectCellRawContent(cell ast.Node) string { var buf bytes.Buffer + for ch := cell.FirstChild(); ch != nil; ch = ch.NextSibling() { switch ch.Kind() { case ast.KindRawHTML: @@ -680,6 +714,7 @@ func (c *converter) collectCellRawContent(cell ast.Node) string { case ast.KindText: t := ch.(*ast.Text) buf.Write(t.Segment.Value(c.source)) + if t.SoftLineBreak() { buf.WriteByte(' ') } @@ -689,12 +724,14 @@ func (c *converter) collectCellRawContent(cell ast.Node) string { buf.WriteString(c.extractText(ch)) } } + return buf.String() } // extractText recursively collects the text content of all descendant nodes. func (c *converter) extractText(n ast.Node) string { var buf bytes.Buffer + for child := n.FirstChild(); child != nil; child = child.NextSibling() { switch child.Kind() { case ast.KindText: @@ -705,6 +742,7 @@ func (c *converter) extractText(n ast.Node) string { buf.WriteString(c.extractText(child)) } } + return buf.String() } @@ -725,11 +763,14 @@ func prependOuterMarks(outer []Mark, nodes []Node) []Node { if len(outer) == 0 { return nodes } + for i := range nodes { if nodes[i].Type == NodeImage { continue } + nodes[i].Marks = append(copyMarks(outer), nodes[i].Marks...) } + return nodes } diff --git a/pkg/prosemirror/markdown_converter_test.go b/pkg/prosemirror/markdown_converter_test.go index e943e18b3..bc5ca44f2 100644 --- a/pkg/prosemirror/markdown_converter_test.go +++ b/pkg/prosemirror/markdown_converter_test.go @@ -240,6 +240,7 @@ func TestParseMarkdown_LinkSanitizesDangerousHrefs(t *testing.T) { doc, err := ParseMarkdown(tt.markdown) require.NoError(t, err) + txt := doc.Content[0].Content[0] linkAttrs, err := txt.Marks[0].LinkAttrs() require.NoError(t, err) @@ -378,11 +379,13 @@ func TestParseMarkdown_HardBreak(t *testing.T) { // Should contain: text("line one"), hardBreak, text("line two") var hasHardBreak bool + for _, child := range p.Content { if child.Type == NodeHardBreak { hasHardBreak = true } } + assert.True(t, hasHardBreak, "expected hard break node") } @@ -397,11 +400,13 @@ func TestParseMarkdown_SoftLineBreak(t *testing.T) { require.Equal(t, NodeParagraph, p.Type) var joined strings.Builder + for _, child := range p.Content { if child.Type == NodeText && child.Text != nil { joined.WriteString(*child.Text) } } + assert.Equal(t, "line one and line two", joined.String()) } @@ -423,6 +428,7 @@ func TestParseMarkdown_NestedMarks(t *testing.T) { for _, m := range txt.Marks { markTypes[m.Type] = true } + assert.True(t, markTypes[MarkStrong]) assert.True(t, markTypes[MarkEm]) } diff --git a/pkg/prosemirror/markdown_renderer.go b/pkg/prosemirror/markdown_renderer.go index bcec0db5f..11cc9a7c7 100644 --- a/pkg/prosemirror/markdown_renderer.go +++ b/pkg/prosemirror/markdown_renderer.go @@ -30,10 +30,12 @@ func RenderMarkdown(node Node) (string, error) { if err := r.renderNode(node); err != nil { return "", err } + out := strings.TrimRight(r.buf.String(), "\n") if out != "" { out += "\n" } + return out, nil } @@ -62,53 +64,69 @@ func (r *mdRenderer) renderNode(n Node) error { return r.renderBlocks(n.Content) case NodeParagraph: r.ensurePrefix() + if err := r.renderInline(n.Content); err != nil { return err } + r.newLine() case NodeHeading: attrs, err := n.HeadingAttrs() if err != nil { return fmt.Errorf("cannot render heading node: %w", err) } + if attrs.Level < 1 || attrs.Level > 6 { return fmt.Errorf("cannot render heading node: invalid level %d", attrs.Level) } + r.ensurePrefix() + for i := 0; i < attrs.Level; i++ { r.buf.WriteByte('#') } + r.buf.WriteByte(' ') + if err := r.renderInline(n.Content); err != nil { return err } + r.newLine() case NodeBlockquote: oldPrefix := r.prefix + r.prefix += "> " if err := r.renderBlocks(n.Content); err != nil { r.prefix = oldPrefix return err } + r.prefix = oldPrefix case NodeCodeBlock: attrs, err := n.CodeBlockAttrs() if err != nil { return fmt.Errorf("cannot render code block node: %w", err) } + code := collectText(n.Content) fence := chooseFence(code) + r.ensurePrefix() r.buf.WriteString(fence) + if attrs.Language != nil { r.buf.WriteString(*attrs.Language) } + r.newLine() + for line := range strings.SplitSeq(code, "\n") { r.ensurePrefix() r.buf.WriteString(line) r.newLine() } + r.ensurePrefix() r.buf.WriteString(fence) r.newLine() @@ -126,18 +144,23 @@ func (r *mdRenderer) renderNode(n Node) error { if err != nil { return fmt.Errorf("cannot render image node: %w", err) } + r.ensurePrefix() r.buf.WriteString("![") + if attrs.Alt != nil { r.buf.WriteString(escapeMarkdown(*attrs.Alt)) } + r.buf.WriteString("](") r.buf.WriteString(safeImageSrc(attrs.Src)) + if attrs.Title != nil { r.buf.WriteString(` "`) r.buf.WriteString(strings.ReplaceAll(*attrs.Title, `"`, `\"`)) r.buf.WriteByte('"') } + r.buf.WriteByte(')') case NodeBulletList: return r.renderBulletList(n) @@ -152,6 +175,7 @@ func (r *mdRenderer) renderNode(n Node) error { default: return fmt.Errorf("cannot render node: unknown type %q", n.Type) } + return nil } @@ -161,10 +185,12 @@ func (r *mdRenderer) renderBlocks(nodes []Node) error { r.ensurePrefix() r.newLine() } + if err := r.renderNode(n); err != nil { return err } } + return nil } @@ -174,6 +200,7 @@ func (r *mdRenderer) renderInline(nodes []Node) error { return err } } + return nil } @@ -190,12 +217,14 @@ func (r *mdRenderer) renderText(n Node) error { } var hasCode bool + for _, m := range n.Marks { if m.Type == MarkCode { hasCode = true break } } + if hasCode { return r.renderCodeText(n) } @@ -203,6 +232,7 @@ func (r *mdRenderer) renderText(n Node) error { text := *n.Text var needsTrim bool + for _, m := range n.Marks { switch m.Type { case MarkStrong, MarkEm, MarkStrike: @@ -211,6 +241,7 @@ func (r *mdRenderer) renderText(n Node) error { } var leading, trailing string + if needsTrim { origLen := len(text) text = strings.TrimLeft(text, " ") @@ -223,6 +254,7 @@ func (r *mdRenderer) renderText(n Node) error { if text == "" { r.buf.WriteString(leading) r.buf.WriteString(trailing) + return nil } @@ -251,6 +283,7 @@ func (r *mdRenderer) renderText(n Node) error { // Inline code fences must be longer than this value (CommonMark). func maxConsecutiveBackticks(s string) int { max, cur := 0, 0 + for i := 0; i < len(s); i++ { if s[i] == '`' { cur++ @@ -261,6 +294,7 @@ func maxConsecutiveBackticks(s string) int { cur = 0 } } + return max } @@ -269,6 +303,7 @@ func (r *mdRenderer) renderCodeText(n Node) error { fence := strings.Repeat("`", maxConsecutiveBackticks(text)+1) var otherMarks []Mark + for _, m := range n.Marks { if m.Type != MarkCode { otherMarks = append(otherMarks, m) @@ -282,13 +317,17 @@ func (r *mdRenderer) renderCodeText(n Node) error { } r.buf.WriteString(fence) + if len(fence) > 1 { r.buf.WriteByte(' ') } + r.buf.WriteString(text) + if len(fence) > 1 { r.buf.WriteByte(' ') } + r.buf.WriteString(fence) for i := len(otherMarks) - 1; i >= 0; i-- { @@ -315,6 +354,7 @@ func (r *mdRenderer) openMark(m Mark) error { default: return fmt.Errorf("cannot render mark: unknown type %q", m.Type) } + return nil } @@ -333,17 +373,21 @@ func (r *mdRenderer) closeMark(m Mark) error { if err != nil { return fmt.Errorf("cannot render link mark: %w", err) } + r.buf.WriteString("](") r.buf.WriteString(safeLinkHref(attrs.Href)) + if attrs.Title != nil { r.buf.WriteString(` "`) r.buf.WriteString(strings.ReplaceAll(*attrs.Title, `"`, `\"`)) r.buf.WriteByte('"') } + r.buf.WriteByte(')') default: return fmt.Errorf("cannot render mark: unknown type %q", m.Type) } + return nil } @@ -352,11 +396,13 @@ func (r *mdRenderer) renderBulletList(n Node) error { if err != nil { return fmt.Errorf("cannot render bullet list: %w", err) } + for i, item := range n.Content { if i > 0 && !tight { r.ensurePrefix() r.newLine() } + r.ensurePrefix() r.buf.WriteString("- ") r.atLineStart = false @@ -369,12 +415,14 @@ func (r *mdRenderer) renderBulletList(n Node) error { if err := r.renderBlocks(item.Content); err != nil { r.prefix = oldPrefix r.tight = oldTight + return err } r.prefix = oldPrefix r.tight = oldTight } + return nil } @@ -383,17 +431,22 @@ func (r *mdRenderer) renderOrderedList(n Node) error { if err != nil { return fmt.Errorf("cannot render ordered list node: %w", err) } + tight, err := listTightness(n) if err != nil { return fmt.Errorf("cannot render ordered list: %w", err) } + start := max(attrs.Start, 1) + for i, item := range n.Content { if i > 0 && !tight { r.ensurePrefix() r.newLine() } + r.ensurePrefix() + num := strconv.Itoa(start + i) r.buf.WriteString(num) r.buf.WriteString(". ") @@ -408,12 +461,14 @@ func (r *mdRenderer) renderOrderedList(n Node) error { if err := r.renderBlocks(item.Content); err != nil { r.prefix = oldPrefix r.tight = oldTight + return err } r.prefix = oldPrefix r.tight = oldTight } + return nil } @@ -427,34 +482,45 @@ func (r *mdRenderer) renderGFMTable(n Node) error { } headerRow := n.Content[0] + r.ensurePrefix() r.buf.WriteByte('|') + for _, cell := range headerRow.Content { r.buf.WriteByte(' ') + if err := r.renderCellInline(cell); err != nil { return err } + r.buf.WriteString(" |") } + r.newLine() r.ensurePrefix() r.buf.WriteByte('|') + for range headerRow.Content { r.buf.WriteString(" --- |") } + r.newLine() for _, row := range n.Content[1:] { r.ensurePrefix() r.buf.WriteByte('|') + for _, cell := range row.Content { r.buf.WriteByte(' ') + if err := r.renderCellInline(cell); err != nil { return err } + r.buf.WriteString(" |") } + r.newLine() } @@ -465,13 +531,16 @@ func (r *mdRenderer) renderCellInline(cell Node) error { if len(cell.Content) == 1 && cell.Content[0].Type == NodeParagraph { return r.renderInline(cell.Content[0].Content) } + for _, child := range cell.Content { h, err := RenderHTML(child) if err != nil { return fmt.Errorf("cannot render table cell content: %w", err) } + r.buf.WriteString(strings.ReplaceAll(h, "|", `\|`)) } + return nil } @@ -479,6 +548,7 @@ func (r *mdRenderer) renderCellInline(cell Node) error { // Every direct child of n must be a listItem; otherwise listTightness returns an error. func listTightness(n Node) (tight bool, err error) { tight = true + for _, item := range n.Content { if item.Type != NodeListItem { return false, fmt.Errorf( @@ -487,20 +557,24 @@ func listTightness(n Node) (tight bool, err error) { NodeListItem, ) } + if len(item.Content) != 1 { tight = false } } + return tight, nil } func collectText(nodes []Node) string { var buf strings.Builder + for _, n := range nodes { if n.Text != nil { buf.WriteString(*n.Text) } } + return buf.String() } @@ -509,18 +583,22 @@ func chooseFence(code string) string { for strings.Contains(code, fence) { fence += "`" } + return fence } func escapeMarkdown(s string) string { var buf strings.Builder buf.Grow(len(s)) + for _, c := range s { switch c { case '\\', '*', '_', '`', '[', ']', '~', '|', '<': buf.WriteByte('\\') } + buf.WriteRune(c) } + return buf.String() } diff --git a/pkg/prosemirror/markdown_renderer_test.go b/pkg/prosemirror/markdown_renderer_test.go index 801caf076..23105eb91 100644 --- a/pkg/prosemirror/markdown_renderer_test.go +++ b/pkg/prosemirror/markdown_renderer_test.go @@ -91,7 +91,9 @@ func TestRenderMarkdown_HeadingLevels(t *testing.T) { "level "+string(rune('0'+tc.level)), func(t *testing.T) { t.Parallel() + raw := `{"type":"doc","content":[{"type":"heading","attrs":{"level":` + string(rune('0'+tc.level)) + `},"content":[{"type":"text","text":"X"}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -107,6 +109,7 @@ func TestRenderMarkdown_HeadingInvalidLevel(t *testing.T) { t.Parallel() raw := `{"type":"heading","attrs":{"level":7},"content":[{"type":"text","text":"X"}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -119,6 +122,7 @@ func TestRenderMarkdown_Bold(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold"}],"text":"bold"}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -131,6 +135,7 @@ func TestRenderMarkdown_Italic(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"italic"}],"text":"italic"}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -143,6 +148,7 @@ func TestRenderMarkdown_ItalicTrailingSpace(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"italic"}],"text":"italic "},{"type":"text","text":"rest"}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -155,6 +161,7 @@ func TestRenderMarkdown_Strikethrough(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"strike"}],"text":"deleted"}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -167,6 +174,7 @@ func TestRenderMarkdown_Underline(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"underline"}],"text":"underlined"}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -179,6 +187,7 @@ func TestRenderMarkdown_InlineCode(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"code"}],"text":"code"}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -191,6 +200,7 @@ func TestRenderMarkdown_InlineCodeWithBacktick(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"code"}],"text":"a ` + "`" + ` b"}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -204,6 +214,7 @@ func TestRenderMarkdown_InlineCodeWithDoubleBacktickRun(t *testing.T) { // Two consecutive backticks in content need a 3+ backtick fence. raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"code"}],"text":"` + "``" + `x"}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -216,6 +227,7 @@ func TestRenderMarkdown_InlineCodeWithTripleBacktickRun(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"code"}],"text":"` + "```" + `"}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -228,6 +240,7 @@ func TestRenderMarkdown_Link(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"link","attrs":{"href":"https://example.com","target":null,"rel":null,"class":null,"title":null}}],"text":"click"}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -240,6 +253,7 @@ func TestRenderMarkdown_LinkWithTitle(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"link","attrs":{"href":"https://example.com","target":null,"rel":null,"class":null,"title":"My Title"}}],"text":"click"}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -267,9 +281,12 @@ func TestRenderMarkdown_LinkSanitizesDangerousHrefs(t *testing.T) { tc.name, func(t *testing.T) { t.Parallel() + hrefJSON, err := json.Marshal(tc.href) require.NoError(t, err) + raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"link","attrs":{"href":` + string(hrefJSON) + `,"target":null,"rel":null,"class":null,"title":null}}],"text":"x"}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -285,6 +302,7 @@ func TestRenderMarkdown_MultipleMarks(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold"},{"type":"italic"}],"text":"hello"}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -297,6 +315,7 @@ func TestRenderMarkdown_CodeBlockWithLanguage(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"codeBlock","attrs":{"language":"go"},"content":[{"type":"text","text":"fmt.Println()"}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -309,6 +328,7 @@ func TestRenderMarkdown_CodeBlockWithoutLanguage(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"codeBlock","attrs":{"language":null},"content":[{"type":"text","text":"hello"}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -321,6 +341,7 @@ func TestRenderMarkdown_CodeBlockWithTripleBackticks(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"codeBlock","attrs":{"language":null},"content":[{"type":"text","text":"` + "```" + `\nsome code"}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -369,6 +390,7 @@ func TestRenderMarkdown_Image(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"image","attrs":{"src":"https://example.com/img.png","alt":"A photo","title":"My image"}}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -381,6 +403,7 @@ func TestRenderMarkdown_ImageWithoutTitle(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"image","attrs":{"src":"https://example.com/img.png","alt":"A photo","title":null}}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -393,6 +416,7 @@ func TestRenderMarkdown_ImageSanitizesDangerousSrc(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"image","attrs":{"src":"javascript:alert(1)","alt":null,"title":null}}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -405,6 +429,7 @@ func TestRenderMarkdown_BulletList(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"bulletList","content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"one"}]}]},{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"two"}]}]},{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"three"}]}]}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -417,6 +442,7 @@ func TestRenderMarkdown_OrderedList(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"orderedList","attrs":{"start":1,"type":null},"content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"first"}]}]},{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"second"}]}]}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -429,6 +455,7 @@ func TestRenderMarkdown_OrderedListWithStart(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"orderedList","attrs":{"start":5,"type":null},"content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"item"}]}]}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -441,6 +468,7 @@ func TestRenderMarkdown_NestedList(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"bulletList","content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"parent"}]},{"type":"bulletList","content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"child"}]}]}]}]}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -453,6 +481,7 @@ func TestRenderMarkdown_Blockquote(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"blockquote","content":[{"type":"paragraph","content":[{"type":"text","text":"quoted"}]}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -465,6 +494,7 @@ func TestRenderMarkdown_BlockquoteMultipleParagraphs(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"blockquote","content":[{"type":"paragraph","content":[{"type":"text","text":"first"}]},{"type":"paragraph","content":[{"type":"text","text":"second"}]}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -477,6 +507,7 @@ func TestRenderMarkdown_GFMTable(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"table","content":[{"type":"tableRow","content":[{"type":"tableHeader","attrs":{"colspan":1,"rowspan":1,"colwidth":null},"content":[{"type":"paragraph","content":[{"type":"text","text":"Name"}]}]},{"type":"tableHeader","attrs":{"colspan":1,"rowspan":1,"colwidth":null},"content":[{"type":"paragraph","content":[{"type":"text","text":"Age"}]}]}]},{"type":"tableRow","content":[{"type":"tableCell","attrs":{"colspan":1,"rowspan":1,"colwidth":null},"content":[{"type":"paragraph","content":[{"type":"text","text":"Alice"}]}]},{"type":"tableCell","attrs":{"colspan":1,"rowspan":1,"colwidth":null},"content":[{"type":"paragraph","content":[{"type":"text","text":"30"}]}]}]}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -489,6 +520,7 @@ func TestRenderMarkdown_TableWithBlockContent(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"table","content":[{"type":"tableRow","content":[{"type":"tableHeader","attrs":{"colspan":1,"rowspan":1,"colwidth":null},"content":[{"type":"paragraph","content":[{"type":"text","text":"Header"}]}]}]},{"type":"tableRow","content":[{"type":"tableCell","attrs":{"colspan":1,"rowspan":1,"colwidth":null},"content":[{"type":"bulletList","content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"item"}]}]}]}]}]}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -562,6 +594,7 @@ func TestRenderMarkdown_MixedContent(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Normal "},{"type":"text","marks":[{"type":"bold"}],"text":"bold"},{"type":"text","text":" and "},{"type":"text","marks":[{"type":"italic"}],"text":"italic"},{"type":"text","text":" text"}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -574,6 +607,7 @@ func TestRenderMarkdown_BlockquoteWithHardBreak(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"blockquote","content":[{"type":"paragraph","content":[{"type":"text","text":"line one"},{"type":"hardBreak"},{"type":"text","text":"line two"}]}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -586,6 +620,7 @@ func TestRenderMarkdown_GFMTableWithMarks(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"table","content":[{"type":"tableRow","content":[{"type":"tableHeader","attrs":{"colspan":1,"rowspan":1,"colwidth":null},"content":[{"type":"paragraph","content":[{"type":"text","text":"Header"}]}]}]},{"type":"tableRow","content":[{"type":"tableCell","attrs":{"colspan":1,"rowspan":1,"colwidth":null},"content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold"}],"text":"bold"},{"type":"text","text":" and "},{"type":"text","marks":[{"type":"italic"}],"text":"italic"}]}]}]}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -617,6 +652,7 @@ func TestRenderMarkdown_CodeBlockInBlockquote(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"blockquote","content":[{"type":"codeBlock","attrs":{"language":"go"},"content":[{"type":"text","text":"fmt.Println()"}]}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -645,6 +681,7 @@ func TestRenderMarkdown_TableBlockCellEscapesPipes(t *testing.T) { t.Parallel() raw := `{"type":"doc","content":[{"type":"table","content":[{"type":"tableRow","content":[{"type":"tableHeader","attrs":{"colspan":1,"rowspan":1,"colwidth":null},"content":[{"type":"paragraph","content":[{"type":"text","text":"H"}]}]}]},{"type":"tableRow","content":[{"type":"tableCell","attrs":{"colspan":1,"rowspan":1,"colwidth":null},"content":[{"type":"bulletList","content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"a | b"}]}]}]}]}]}]}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) diff --git a/pkg/prosemirror/node.go b/pkg/prosemirror/node.go index 932221dff..bf3ab9c43 100644 --- a/pkg/prosemirror/node.go +++ b/pkg/prosemirror/node.go @@ -118,6 +118,7 @@ func Parse(s string) (Node, error) { if err := json.Unmarshal([]byte(s), &n); err != nil { return Node{}, fmt.Errorf("cannot parse prosemirror node: %w", err) } + return n, nil } @@ -127,6 +128,7 @@ func (n Node) HeadingAttrs() (HeadingAttrs, error) { if err := json.Unmarshal(n.Attrs, &a); err != nil { return a, fmt.Errorf("cannot parse heading attrs: %w", err) } + return a, nil } @@ -135,10 +137,12 @@ func (n Node) CodeBlockAttrs() (CodeBlockAttrs, error) { if len(n.Attrs) == 0 { return CodeBlockAttrs{}, nil } + var a CodeBlockAttrs if err := json.Unmarshal(n.Attrs, &a); err != nil { return a, fmt.Errorf("cannot parse code block attrs: %w", err) } + return a, nil } @@ -148,6 +152,7 @@ func (n Node) OrderedListAttrs() (OrderedListAttrs, error) { if err := json.Unmarshal(n.Attrs, &a); err != nil { return a, fmt.Errorf("cannot parse ordered list attrs: %w", err) } + return a, nil } @@ -157,6 +162,7 @@ func (n Node) ImageAttrs() (ImageAttrs, error) { if err := json.Unmarshal(n.Attrs, &a); err != nil { return a, fmt.Errorf("cannot parse image attrs: %w", err) } + return a, nil } @@ -166,6 +172,7 @@ func (n Node) TableCellAttrs() (TableCellAttrs, error) { if err := json.Unmarshal(n.Attrs, &a); err != nil { return a, fmt.Errorf("cannot parse table cell attrs: %w", err) } + return a, nil } @@ -176,9 +183,11 @@ func (n Node) TextLength() int { if n.Text != nil { length += utf8.RuneCountInString(*n.Text) } + for _, child := range n.Content { length += child.TextLength() } + return length } @@ -188,5 +197,6 @@ func (m Mark) LinkAttrs() (LinkAttrs, error) { if err := json.Unmarshal(m.Attrs, &a); err != nil { return a, fmt.Errorf("cannot parse link attrs: %w", err) } + return a, nil } diff --git a/pkg/prosemirror/node_test.go b/pkg/prosemirror/node_test.go index d1a605334..96b154937 100644 --- a/pkg/prosemirror/node_test.go +++ b/pkg/prosemirror/node_test.go @@ -31,6 +31,7 @@ func loadTestDocument(t *testing.T) Node { var doc Node require.NoError(t, json.Unmarshal(data, &doc)) + return doc } @@ -46,6 +47,7 @@ func TestUnmarshalDocument(t *testing.T) { "heading level 1", func(t *testing.T) { t.Parallel() + h1 := doc.Content[0] assert.Equal(t, NodeHeading, h1.Type) @@ -64,6 +66,7 @@ func TestUnmarshalDocument(t *testing.T) { "paragraph with mixed marks", func(t *testing.T) { t.Parallel() + p := doc.Content[1] assert.Equal(t, NodeParagraph, p.Type) require.True(t, len(p.Content) > 5) @@ -119,6 +122,7 @@ func TestUnmarshalDocument(t *testing.T) { "heading level 2", func(t *testing.T) { t.Parallel() + h2 := doc.Content[2] assert.Equal(t, NodeHeading, h2.Type) @@ -132,6 +136,7 @@ func TestUnmarshalDocument(t *testing.T) { "code block", func(t *testing.T) { t.Parallel() + cb := doc.Content[4] assert.Equal(t, NodeCodeBlock, cb.Type) @@ -149,6 +154,7 @@ func TestUnmarshalDocument(t *testing.T) { "heading level 3", func(t *testing.T) { t.Parallel() + h3 := doc.Content[5] attrs, err := h3.HeadingAttrs() require.NoError(t, err) @@ -160,6 +166,7 @@ func TestUnmarshalDocument(t *testing.T) { "bullet list", func(t *testing.T) { t.Parallel() + bl := doc.Content[6] assert.Equal(t, NodeBulletList, bl.Type) require.Len(t, bl.Content, 3) @@ -186,6 +193,7 @@ func TestUnmarshalDocument(t *testing.T) { "ordered list", func(t *testing.T) { t.Parallel() + ol := doc.Content[8] assert.Equal(t, NodeOrderedList, ol.Type) require.Len(t, ol.Content, 3) @@ -201,6 +209,7 @@ func TestUnmarshalDocument(t *testing.T) { "blockquote", func(t *testing.T) { t.Parallel() + bq := doc.Content[10] assert.Equal(t, NodeBlockquote, bq.Type) require.Len(t, bq.Content, 1) @@ -217,6 +226,7 @@ func TestUnmarshalDocument(t *testing.T) { "table", func(t *testing.T) { t.Parallel() + table := doc.Content[12] assert.Equal(t, NodeTable, table.Type) require.Len(t, table.Content, 3) @@ -225,6 +235,7 @@ func TestUnmarshalDocument(t *testing.T) { headerRow := table.Content[0] assert.Equal(t, NodeTableRow, headerRow.Type) require.Len(t, headerRow.Content, 4) + for _, cell := range headerRow.Content { assert.Equal(t, NodeTableHeader, cell.Type) } @@ -247,6 +258,7 @@ func TestUnmarshalDocument(t *testing.T) { for _, row := range table.Content[1:] { assert.Equal(t, NodeTableRow, row.Type) require.Len(t, row.Content, 4) + for _, cell := range row.Content { assert.Equal(t, NodeTableCell, cell.Type) } @@ -268,6 +280,7 @@ func TestUnmarshalDocument(t *testing.T) { "trailing empty paragraph", func(t *testing.T) { t.Parallel() + emptyP := doc.Content[13] assert.Equal(t, NodeParagraph, emptyP.Type) assert.Empty(t, emptyP.Content) @@ -297,6 +310,7 @@ func TestHeadingAttrs(t *testing.T) { t.Parallel() raw := `{"type":"heading","attrs":{"level":3},"content":[{"type":"text","text":"Hello"}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -312,7 +326,9 @@ func TestCodeBlockAttrs(t *testing.T) { "with language", func(t *testing.T) { t.Parallel() + raw := `{"type":"codeBlock","attrs":{"language":"go"},"content":[{"type":"text","text":"fmt.Println()"}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -327,7 +343,9 @@ func TestCodeBlockAttrs(t *testing.T) { "with null language", func(t *testing.T) { t.Parallel() + raw := `{"type":"codeBlock","attrs":{"language":null}}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -342,6 +360,7 @@ func TestOrderedListAttrs(t *testing.T) { t.Parallel() raw := `{"type":"orderedList","attrs":{"start":5,"type":null}}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -355,6 +374,7 @@ func TestImageAttrs(t *testing.T) { t.Parallel() raw := `{"type":"image","attrs":{"src":"https://example.com/img.png","alt":"An image","title":null}}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -373,7 +393,9 @@ func TestTableCellAttrs(t *testing.T) { "with colwidth", func(t *testing.T) { t.Parallel() + raw := `{"type":"tableCell","attrs":{"colspan":2,"rowspan":1,"colwidth":[100,200]}}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -389,7 +411,9 @@ func TestTableCellAttrs(t *testing.T) { "with null colwidth", func(t *testing.T) { t.Parallel() + raw := `{"type":"tableHeader","attrs":{"colspan":1,"rowspan":1,"colwidth":null}}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) @@ -406,6 +430,7 @@ func TestLinkAttrs(t *testing.T) { t.Parallel() raw := `{"type":"link","attrs":{"href":"https://example.com","target":"_blank","rel":"noopener","class":null,"title":"Example"}}` + var m Mark require.NoError(t, json.Unmarshal([]byte(raw), &m)) @@ -428,6 +453,7 @@ func TestTextLength(t *testing.T) { "empty doc", func(t *testing.T) { t.Parallel() + n := Node{Type: NodeDoc} assert.Equal(t, 0, n.TextLength()) }, @@ -437,6 +463,7 @@ func TestTextLength(t *testing.T) { "single text node", func(t *testing.T) { t.Parallel() + text := "hello" n := Node{Type: NodeText, Text: &text} assert.Equal(t, 5, n.TextLength()) @@ -447,6 +474,7 @@ func TestTextLength(t *testing.T) { "paragraph with text", func(t *testing.T) { t.Parallel() + raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello world"}]}]}` doc, err := Parse(raw) require.NoError(t, err) @@ -458,6 +486,7 @@ func TestTextLength(t *testing.T) { "multiple paragraphs", func(t *testing.T) { t.Parallel() + raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"aaa"}]},{"type":"paragraph","content":[{"type":"text","text":"bb"}]}]}` doc, err := Parse(raw) require.NoError(t, err) @@ -469,6 +498,7 @@ func TestTextLength(t *testing.T) { "formatted text counts only text", func(t *testing.T) { t.Parallel() + raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"plain "},{"type":"text","marks":[{"type":"bold"}],"text":"bold"}]}]}` doc, err := Parse(raw) require.NoError(t, err) @@ -480,6 +510,7 @@ func TestTextLength(t *testing.T) { "nested list structure", func(t *testing.T) { t.Parallel() + raw := `{"type":"doc","content":[{"type":"bulletList","content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"item 1"}]}]},{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"item 2"}]}]}]}]}` doc, err := Parse(raw) require.NoError(t, err) @@ -491,6 +522,7 @@ func TestTextLength(t *testing.T) { "multi-byte unicode characters", func(t *testing.T) { t.Parallel() + raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"café résumé"}]}]}` doc, err := Parse(raw) require.NoError(t, err) @@ -512,6 +544,7 @@ func TestNodeWithNoAttrs(t *testing.T) { t.Parallel() raw := `{"type":"paragraph","content":[{"type":"text","text":"Hello"}]}` + var n Node require.NoError(t, json.Unmarshal([]byte(raw), &n)) diff --git a/pkg/prosemirror/sanitize.go b/pkg/prosemirror/sanitize.go index 163afe48c..39e91eae6 100644 --- a/pkg/prosemirror/sanitize.go +++ b/pkg/prosemirror/sanitize.go @@ -26,7 +26,9 @@ func ValidateDocumentContentJSON(s string) error { if strings.TrimSpace(s) == "" { return nil } + _, err := parseDocRoot(s) + return err } @@ -35,9 +37,11 @@ func parseDocRoot(s string) (Node, error) { if err != nil { return Node{}, fmt.Errorf("cannot parse document content as ProseMirror JSON: %w", err) } + if n.Type != NodeDoc { return Node{}, fmt.Errorf("document content root must be type %q", NodeDoc) } + return n, nil } @@ -69,9 +73,11 @@ func sanitizeNode(n *Node) { if n.Type == NodeImage { sanitizeImageNode(n) } + for i := range n.Marks { sanitizeLinkMark(&n.Marks[i]) } + for i := range n.Content { sanitizeNode(&n.Content[i]) } @@ -85,6 +91,7 @@ func sanitizeImageNode(n *Node) { } attrs.Src = safeImageSrc(attrs.Src) + raw, err := json.Marshal(attrs) if err != nil { n.Attrs = []byte(`{"src":""}`) @@ -106,6 +113,7 @@ func sanitizeLinkMark(m *Mark) { } attrs.Href = safeLinkHref(attrs.Href) + raw, err := json.Marshal(attrs) if err != nil { m.Attrs = []byte(`{"href":"#"}`) diff --git a/pkg/rfc5988/rfc5988.go b/pkg/rfc5988/rfc5988.go index c205df691..d5a64b61a 100644 --- a/pkg/rfc5988/rfc5988.go +++ b/pkg/rfc5988/rfc5988.go @@ -40,6 +40,7 @@ func Parse(header string) []Link { } start := strings.Index(part, "<") + end := strings.Index(part, ">") if start == -1 || end == -1 || end <= start { continue diff --git a/pkg/saferedirect/saferedirect.go b/pkg/saferedirect/saferedirect.go index f8ad06a69..8e855c7e3 100644 --- a/pkg/saferedirect/saferedirect.go +++ b/pkg/saferedirect/saferedirect.go @@ -57,6 +57,7 @@ func (sr *SafeRedirect) Validate(ctx context.Context, redirectURL string) (strin if len(redirectURL) > 1 && (redirectURL[1] == '/' || redirectURL[1] == '\\') { return "", false } + return redirectURL, true } @@ -84,6 +85,7 @@ func (sr *SafeRedirect) GetSafeRedirectURL(ctx context.Context, redirectURL, fal if safeURL, isValid := sr.Validate(ctx, redirectURL); isValid { return safeURL } + return fallbackURL } diff --git a/pkg/saferedirect/saferedirect_test.go b/pkg/saferedirect/saferedirect_test.go index 83bd2f0f0..5c68cdb00 100644 --- a/pkg/saferedirect/saferedirect_test.go +++ b/pkg/saferedirect/saferedirect_test.go @@ -115,6 +115,7 @@ func TestSafeRedirect_Validate(t *testing.T) { if gotIsValid != tt.expectedIsValid { t.Errorf("Validate() isValid = %v, want %v", gotIsValid, tt.expectedIsValid) } + if gotURL != tt.expectedURL { t.Errorf("Validate() url = %v, want %v", gotURL, tt.expectedURL) } @@ -349,6 +350,7 @@ func TestStaticHosts(t *testing.T) { if !fn(context.Background(), "example.com") { t.Error("expected example.com to be allowed") } + if fn(context.Background(), "other.com") { t.Error("expected other.com to be rejected") } @@ -361,12 +363,15 @@ func TestStaticHosts(t *testing.T) { if !fn(context.Background(), "a.com") { t.Error("expected a.com to be allowed") } + if !fn(context.Background(), "b.com") { t.Error("expected b.com to be allowed") } + if !fn(context.Background(), "c.com") { t.Error("expected c.com to be allowed") } + if fn(context.Background(), "d.com") { t.Error("expected d.com to be rejected") } @@ -379,6 +384,7 @@ func TestStaticHosts(t *testing.T) { if fn(context.Background(), "") { t.Error("expected empty host to be rejected") } + if !fn(context.Background(), "example.com") { t.Error("expected example.com to be allowed") } diff --git a/pkg/securecookie/securecookie.go b/pkg/securecookie/securecookie.go index 1491bbbb6..d13f7a954 100644 --- a/pkg/securecookie/securecookie.go +++ b/pkg/securecookie/securecookie.go @@ -78,6 +78,7 @@ func Set(w http.ResponseWriter, config Config, value string) error { } http.SetCookie(w, cookie) + return nil } diff --git a/pkg/server/api/api.go b/pkg/server/api/api.go index a750b11ca..beba99c70 100644 --- a/pkg/server/api/api.go +++ b/pkg/server/api/api.go @@ -231,6 +231,7 @@ func NewServer(cfg Config) (*Server, error) { } _, err := cfg.Trust.GetByDomainName(ctx, host) + return err == nil }, func(ctx context.Context, host string) bool { diff --git a/pkg/server/api/authn/api_key_middleware.go b/pkg/server/api/authn/api_key_middleware.go index 1906251e1..ddda36d85 100644 --- a/pkg/server/api/authn/api_key_middleware.go +++ b/pkg/server/api/authn/api_key_middleware.go @@ -58,13 +58,16 @@ func NewAPIKeyMiddleware(svc *iam.Service, tokenSecret string) func(next http.Ha }, }, ) + return } apiKey, err := svc.APIKeyService.GetAPIKey(ctx, keyID) if err != nil { - var errPersonalAPIKeyNotFound *iam.ErrPersonalAPIKeyNotFound - var errPersonalAPIKeyExpired *iam.ErrPersonalAPIKeyExpired + var ( + errPersonalAPIKeyNotFound *iam.ErrPersonalAPIKeyNotFound + errPersonalAPIKeyExpired *iam.ErrPersonalAPIKeyExpired + ) if errors.As(err, &errPersonalAPIKeyNotFound) || errors.As(err, &errPersonalAPIKeyExpired) { next.ServeHTTP(w, r) diff --git a/pkg/server/api/authn/identity_presence_middleware.go b/pkg/server/api/authn/identity_presence_middleware.go index b33da265b..618821049 100644 --- a/pkg/server/api/authn/identity_presence_middleware.go +++ b/pkg/server/api/authn/identity_presence_middleware.go @@ -42,6 +42,7 @@ func NewIdentityPresenceMiddleware() func(next http.Handler) http.Handler { }, }, ) + return } diff --git a/pkg/server/api/authn/session_middleware.go b/pkg/server/api/authn/session_middleware.go index 0003cccbb..8e2d5c15b 100644 --- a/pkg/server/api/authn/session_middleware.go +++ b/pkg/server/api/authn/session_middleware.go @@ -46,6 +46,7 @@ func NewSessionMiddleware(svc *iam.Service, cookieConfig securecookie.Config) fu if err != nil { securecookie.Clear(w, cookieConfig) next.ServeHTTP(w, r) + return } @@ -60,17 +61,21 @@ func NewSessionMiddleware(svc *iam.Service, cookieConfig securecookie.Config) fu }, }, ) + return } session, err := svc.SessionService.GetSession(ctx, sessionID) if err != nil { - var errSessionNotFound *iam.ErrSessionNotFound - var errSessionExpired *iam.ErrSessionExpired + var ( + errSessionNotFound *iam.ErrSessionNotFound + errSessionExpired *iam.ErrSessionExpired + ) if errors.As(err, &errSessionNotFound) || errors.As(err, &errSessionExpired) { securecookie.Clear(w, cookieConfig) next.ServeHTTP(w, r) + return } @@ -83,6 +88,7 @@ func NewSessionMiddleware(svc *iam.Service, cookieConfig securecookie.Config) fu if errors.As(err, &errIdentityNotFound) { securecookie.Clear(w, cookieConfig) next.ServeHTTP(w, r) + return } diff --git a/pkg/server/api/authz/authorization.go b/pkg/server/api/authz/authorization.go index e84da2bce..13b5c9979 100644 --- a/pkg/server/api/authz/authorization.go +++ b/pkg/server/api/authz/authorization.go @@ -94,6 +94,7 @@ func NewAuthorizeFunc( } logger.ErrorCtx(ctx, "cannot authorize", log.Error(err)) + return gqlutils.Internal(ctx) } diff --git a/pkg/server/api/clientip/clientip.go b/pkg/server/api/clientip/clientip.go index 287165aed..6a3c55499 100644 --- a/pkg/server/api/clientip/clientip.go +++ b/pkg/server/api/clientip/clientip.go @@ -36,11 +36,13 @@ func Extract(r *http.Request) string { if i := strings.LastIndexByte(xff, ','); i != -1 { xff = xff[i+1:] } + xff = strings.TrimSpace(xff) if ip, _, err := net.SplitHostPort(xff); err == nil { return ip } + return xff } @@ -77,6 +79,7 @@ func parseForwardedFor(header string) string { if ip, _, err := net.SplitHostPort(val); err == nil { return ip } + return val } diff --git a/pkg/server/api/clientip/clientip_test.go b/pkg/server/api/clientip/clientip_test.go index dd5b994b9..701f91dd1 100644 --- a/pkg/server/api/clientip/clientip_test.go +++ b/pkg/server/api/clientip/clientip_test.go @@ -119,6 +119,7 @@ func TestExtract(t *testing.T) { t.Parallel() r := httptest.NewRequest("GET", "/", nil) + r.RemoteAddr = tt.remoteAddr for k, v := range tt.headers { r.Header.Set(k, v) diff --git a/pkg/server/api/compliancepage/compliance_page_presence_middleware.go b/pkg/server/api/compliancepage/compliance_page_presence_middleware.go index a543e41c7..598d6cc9b 100644 --- a/pkg/server/api/compliancepage/compliance_page_presence_middleware.go +++ b/pkg/server/api/compliancepage/compliance_page_presence_middleware.go @@ -42,6 +42,7 @@ func NewCompliancePagePresenceMiddleware() func(next http.Handler) http.Handler }, }, ) + return } diff --git a/pkg/server/api/compliancepage/id_middleware.go b/pkg/server/api/compliancepage/id_middleware.go index a93aedd45..60812a18c 100644 --- a/pkg/server/api/compliancepage/id_middleware.go +++ b/pkg/server/api/compliancepage/id_middleware.go @@ -54,6 +54,7 @@ func NewIDMiddleware(trustSvc *trust.Service, baseURL string) func(next http.Han }, }, ) + return } @@ -68,6 +69,7 @@ func NewIDMiddleware(trustSvc *trust.Service, baseURL string) func(next http.Han ctx = context.WithValue(ctx, compliancePageKey, compliancePage) next.ServeHTTP(w, r.WithContext(ctx)) + return } @@ -87,6 +89,7 @@ func NewIDMiddleware(trustSvc *trust.Service, baseURL string) func(next http.Han }, }, ) + return } @@ -97,6 +100,7 @@ func NewIDMiddleware(trustSvc *trust.Service, baseURL string) func(next http.Han if compliancePage.Active { ctx = context.WithValue(ctx, compliancePageKey, compliancePage) next.ServeHTTP(w, r.WithContext(ctx)) + return } diff --git a/pkg/server/api/compliancepage/member_provisioning_middleware.go b/pkg/server/api/compliancepage/member_provisioning_middleware.go index bc5d49042..b91c0239b 100644 --- a/pkg/server/api/compliancepage/member_provisioning_middleware.go +++ b/pkg/server/api/compliancepage/member_provisioning_middleware.go @@ -51,6 +51,7 @@ func NewMemberProvisioningMiddleware(trustSvc *trust.Service, logger *log.Logger }, }, ) + return } diff --git a/pkg/server/api/compliancepage/sni_middleware.go b/pkg/server/api/compliancepage/sni_middleware.go index ce2c26ace..0007dcadc 100644 --- a/pkg/server/api/compliancepage/sni_middleware.go +++ b/pkg/server/api/compliancepage/sni_middleware.go @@ -53,6 +53,7 @@ func NewSNIMiddleware(trustSvc *trust.Service) func(next http.Handler) http.Hand }, }, ) + return } @@ -72,6 +73,7 @@ func NewSNIMiddleware(trustSvc *trust.Service) func(next http.Handler) http.Hand if compliancePage.Active { ctx = context.WithValue(ctx, compliancePageKey, compliancePage) next.ServeHTTP(w, r.WithContext(ctx)) + return } diff --git a/pkg/server/api/connect/v1/base_resolvers.go b/pkg/server/api/connect/v1/base_resolvers.go index da776e79e..c9044e063 100644 --- a/pkg/server/api/connect/v1/base_resolvers.go +++ b/pkg/server/api/connect/v1/base_resolvers.go @@ -119,6 +119,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewPersonalAPIKey(personalAPIKey), nil } case coredata.SCIMConfigurationEntityType: @@ -128,6 +129,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewSCIMConfiguration(scimConfiguration), nil } case coredata.SCIMEventEntityType: @@ -137,6 +139,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewSCIMEvent(scimEvent), nil } default: @@ -174,6 +177,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error } r.logger.ErrorCtx(ctx, "cannot load node", log.Error(err)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/connect/v1/graphql_handler.go b/pkg/server/api/connect/v1/graphql_handler.go index d780bde64..179697389 100644 --- a/pkg/server/api/connect/v1/graphql_handler.go +++ b/pkg/server/api/connect/v1/graphql_handler.go @@ -44,5 +44,6 @@ func NewGraphQLHandler(svc *iam.Service, logger *log.Logger, baseURL *baseurl.Ba es := schema.NewExecutableSchema(config) gqlh := gqlutils.NewHandler(es, logger) + return gqlh } diff --git a/pkg/server/api/connect/v1/identity_resolvers.go b/pkg/server/api/connect/v1/identity_resolvers.go index 66974746f..d7c7e2c55 100644 --- a/pkg/server/api/connect/v1/identity_resolvers.go +++ b/pkg/server/api/connect/v1/identity_resolvers.go @@ -168,9 +168,11 @@ func (r *identityResolver) SsoLoginURL(ctx context.Context, obj *types.Identity) r.logger.ErrorCtx(ctx, "cannot find SAML config") return nil, gqlutils.NotFoundf(ctx, "cannot find SAML config") } + samlConfig := samlConfigs[0] loginURL := r.SSOLoginURL(samlConfig.ID) + return &loginURL, nil } diff --git a/pkg/server/api/connect/v1/invitation_resolvers.go b/pkg/server/api/connect/v1/invitation_resolvers.go index 59e58e9a0..2f94d8fbf 100644 --- a/pkg/server/api/connect/v1/invitation_resolvers.go +++ b/pkg/server/api/connect/v1/invitation_resolvers.go @@ -36,8 +36,10 @@ func (r *mutationResolver) InviteUser(ctx context.Context, input types.InviteUse }, ) if err != nil { - var errOrganizationNotFound *iam.ErrOrganizationNotFound - var errUserAlreadyExists *iam.ErrUserAlreadyExists + var ( + errOrganizationNotFound *iam.ErrOrganizationNotFound + errUserAlreadyExists *iam.ErrUserAlreadyExists + ) if errors.As(err, &errOrganizationNotFound) { return nil, gqlutils.NotFound(ctx, err) @@ -48,6 +50,7 @@ func (r *mutationResolver) InviteUser(ctx context.Context, input types.InviteUse } r.logger.ErrorCtx(ctx, "cannot invite user", log.Error(err)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/connect/v1/membership_resolvers.go b/pkg/server/api/connect/v1/membership_resolvers.go index 1878d230d..0415d8319 100644 --- a/pkg/server/api/connect/v1/membership_resolvers.go +++ b/pkg/server/api/connect/v1/membership_resolvers.go @@ -38,6 +38,7 @@ func (r *membershipResolver) LastSession(ctx context.Context, obj *types.Members } r.logger.ErrorCtx(ctx, "cannot get active session for membership", log.Error(err)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/connect/v1/oauth2_error.go b/pkg/server/api/connect/v1/oauth2_error.go index 28a7bd27e..ced561ad8 100644 --- a/pkg/server/api/connect/v1/oauth2_error.go +++ b/pkg/server/api/connect/v1/oauth2_error.go @@ -89,6 +89,7 @@ func toOAuth2Error(err error) *oauth2server.OAuth2Error { if oauthErr, ok := errors.AsType[*oauth2server.OAuth2Error](err); ok { return oauthErr } + return oauth2server.NewError(oauth2server.ErrServerError, oauth2server.WithDescription("internal error")) } } @@ -108,12 +109,15 @@ func redirectWithError(w http.ResponseWriter, r *http.Request, redirectURI, stat q := u.Query() q.Set("error", oauthErr.ErrorCode()) + if desc := oauthErr.Description(); desc != "" { q.Set("error_description", desc) } + if state != "" { q.Set("state", state) } + u.RawQuery = q.Encode() http.Redirect(w, r, u.String(), http.StatusFound) diff --git a/pkg/server/api/connect/v1/oauth2_handler.go b/pkg/server/api/connect/v1/oauth2_handler.go index dad472a03..bf48b094a 100644 --- a/pkg/server/api/connect/v1/oauth2_handler.go +++ b/pkg/server/api/connect/v1/oauth2_handler.go @@ -98,6 +98,7 @@ func (h *OAuth2Handler) BearerTokenMiddleware(next http.Handler) http.Handler { if err != nil { w.Header().Set("WWW-Authenticate", `Bearer error="invalid_token"`) http.Error(w, "unauthorized", http.StatusUnauthorized) + return } @@ -105,6 +106,7 @@ func (h *OAuth2Handler) BearerTokenMiddleware(next http.Handler) http.Handler { if err != nil { w.Header().Set("WWW-Authenticate", `Bearer error="invalid_token"`) http.Error(w, "unauthorized", http.StatusUnauthorized) + return } @@ -160,6 +162,7 @@ func (h *OAuth2Handler) AuthorizeHandler(w http.ResponseWriter, r *http.Request) WithQuery("continue", continueURL). MustString() http.Redirect(w, r, loginURL, http.StatusFound) + return } @@ -170,6 +173,7 @@ func (h *OAuth2Handler) AuthorizeHandler(w http.ResponseWriter, r *http.Request) } session := authn.SessionFromContext(r.Context()) + authTime := time.Now() if session != nil { authTime = session.CreatedAt @@ -197,12 +201,14 @@ func (h *OAuth2Handler) AuthorizeHandler(w http.ResponseWriter, r *http.Request) WithQuery("consent_id", consentErr.ConsentID.String()). MustString() http.Redirect(w, r, consentURL, http.StatusFound) + return } if err != nil { oauthErr := toOAuth2Error(err) h.handleAuthorizeError(w, r, oauthErr, in.RedirectURI, in.State) + return } @@ -282,6 +288,7 @@ func (h *OAuth2Handler) RevokeHandler(w http.ResponseWriter, r *http.Request) { h.logger.ErrorCtx(r.Context(), "cannot revoke token", log.Error(err)) w.Header().Set("Retry-After", "30") w.WriteHeader(http.StatusServiceUnavailable) + return } @@ -340,21 +347,26 @@ func (h *OAuth2Handler) RegisterHandler(w http.ResponseWriter, r *http.Request) r, oauth2server.NewError(oauth2server.ErrInvalidRequest, oauth2server.WithDescription("invalid JSON body")), ) + return } if len(in.GrantTypes) == 0 { in.GrantTypes = []coredata.OAuth2GrantType{coredata.OAuth2GrantTypeAuthorizationCode} } + if len(in.ResponseTypes) == 0 { in.ResponseTypes = []coredata.OAuth2ResponseType{coredata.OAuth2ResponseTypeCode} } + if in.TokenEndpointAuthMethod == "" { in.TokenEndpointAuthMethod = coredata.OAuth2ClientTokenEndpointAuthMethodClientSecretBasic } + if in.Visibility == "" { in.Visibility = coredata.OAuth2ClientVisibilityPrivate } + if len(in.Scopes) == 0 { in.Scopes = coredata.OAuth2Scopes{ coredata.OAuth2ScopeOpenID, @@ -539,9 +551,11 @@ func redirectWithCode(w http.ResponseWriter, r *http.Request, redirectURI, code, u, _ := url.Parse(redirectURI) q := u.Query() q.Set("code", code) + if state != "" { q.Set("state", state) } + u.RawQuery = q.Encode() http.Redirect(w, r, u.String(), http.StatusFound) diff --git a/pkg/server/api/connect/v1/oauth2_resolvers.go b/pkg/server/api/connect/v1/oauth2_resolvers.go index b7178d3bf..dafd17a33 100644 --- a/pkg/server/api/connect/v1/oauth2_resolvers.go +++ b/pkg/server/api/connect/v1/oauth2_resolvers.go @@ -50,6 +50,7 @@ func (r *mutationResolver) AuthorizeDevice(ctx context.Context, input types.Auth } r.logger.ErrorCtx(ctx, "cannot authorize device", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -89,12 +90,15 @@ func (r *mutationResolver) ApproveConsent(ctx context.Context, input types.Appro q := u.Query() q.Set("error", "access_denied") q.Set("error_description", "user denied the request") + if result.State != "" { q.Set("state", result.State) } + u.RawQuery = q.Encode() redirectURL := u.String() + return &types.ApproveConsentPayload{ RedirectURL: &redirectURL, }, nil @@ -109,12 +113,15 @@ func (r *mutationResolver) ApproveConsent(ctx context.Context, input types.Appro u, _ := url.Parse(result.RedirectURI) q := u.Query() q.Set("code", result.Code) + if result.State != "" { q.Set("state", result.State) } + u.RawQuery = q.Encode() redirectURL := u.String() + return &types.ApproveConsentPayload{ RedirectURL: &redirectURL, }, nil diff --git a/pkg/server/api/connect/v1/oidc_handler.go b/pkg/server/api/connect/v1/oidc_handler.go index 4ac3118dc..118c49d88 100644 --- a/pkg/server/api/connect/v1/oidc_handler.go +++ b/pkg/server/api/connect/v1/oidc_handler.go @@ -81,6 +81,7 @@ func (h *OIDCHandler) LoginHandler(w http.ResponseWriter, r *http.Request) { if err != nil { h.logger.ErrorCtx(ctx, "cannot initiate OIDC login", log.Error(err)) httpserver.RenderError(w, http.StatusInternalServerError, errors.New("internal server error")) + return } @@ -105,6 +106,7 @@ func (h *OIDCHandler) CallbackHandler(w http.ResponseWriter, r *http.Request) { log.String("error_description", r.URL.Query().Get("error_description")), ) httpserver.RenderError(w, http.StatusUnauthorized, errors.New("authentication failed")) + return } @@ -120,6 +122,7 @@ func (h *OIDCHandler) CallbackHandler(w http.ResponseWriter, r *http.Request) { if err != nil { h.logger.ErrorCtx(ctx, "cannot handle OIDC callback", log.Error(err)) httpserver.RenderError(w, http.StatusUnauthorized, errors.New("authentication failed")) + return } @@ -131,6 +134,7 @@ func (h *OIDCHandler) CallbackHandler(w http.ResponseWriter, r *http.Request) { if err != nil { h.logger.ErrorCtx(ctx, "cannot open root session", log.Error(err)) httpserver.RenderError(w, http.StatusInternalServerError, errors.New("internal server error")) + return } case rootSession.IdentityID != identity.ID: @@ -138,6 +142,7 @@ func (h *OIDCHandler) CallbackHandler(w http.ResponseWriter, r *http.Request) { if err != nil { h.logger.ErrorCtx(ctx, "cannot close session", log.Error(err)) httpserver.RenderError(w, http.StatusInternalServerError, errors.New("internal server error")) + return } @@ -145,6 +150,7 @@ func (h *OIDCHandler) CallbackHandler(w http.ResponseWriter, r *http.Request) { if err != nil { h.logger.ErrorCtx(ctx, "cannot open root session", log.Error(err)) httpserver.RenderError(w, http.StatusInternalServerError, errors.New("internal server error")) + return } } diff --git a/pkg/server/api/connect/v1/organization_resolvers.go b/pkg/server/api/connect/v1/organization_resolvers.go index 074dc8305..2e4fe86d9 100644 --- a/pkg/server/api/connect/v1/organization_resolvers.go +++ b/pkg/server/api/connect/v1/organization_resolvers.go @@ -56,6 +56,7 @@ func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.C Size: input.HorizontalLogoFile.Size, } } + organization, profile, err := r.iam.OrganizationService.CreateOrganization( ctx, identity.ID, @@ -71,6 +72,7 @@ func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.C } r.logger.ErrorCtx(ctx, "cannot create organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -267,6 +269,7 @@ func (r *organizationResolver) ScimConfiguration(ctx context.Context, obj *types } r.logger.ErrorCtx(ctx, "cannot get scim configuration", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -307,16 +310,20 @@ func (r *organizationResolver) AuditLogEntries(ctx context.Context, obj *types.O c := cursor.NewCursor(first, after, last, before, pageOrderBy) coredataFilter := coredata.NewAuditLogEntryFilter() + if filter != nil { if filter.Action != nil { coredataFilter.WithAction(*filter.Action) } + if filter.ActorID != nil { coredataFilter.WithActorID(*filter.ActorID) } + if filter.ResourceType != nil { coredataFilter.WithResourceType(*filter.ResourceType) } + if filter.ResourceID != nil { coredataFilter.WithResourceID(*filter.ResourceID) } @@ -347,6 +354,7 @@ func (r *organizationResolver) Viewer(ctx context.Context, obj *types.Organizati } r.logger.ErrorCtx(ctx, "cannot get profile", log.Error(err)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/connect/v1/personal_api_key_resolvers.go b/pkg/server/api/connect/v1/personal_api_key_resolvers.go index ae45fb293..87c878f96 100644 --- a/pkg/server/api/connect/v1/personal_api_key_resolvers.go +++ b/pkg/server/api/connect/v1/personal_api_key_resolvers.go @@ -99,6 +99,7 @@ func (r *personalAPIKeyConnectionResolver) TotalCount(ctx context.Context, obj * } r.logger.ErrorCtx(ctx, "unsupported resolver", log.Any("resolver", obj.Resolver)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/connect/v1/profile_resolvers.go b/pkg/server/api/connect/v1/profile_resolvers.go index ac8d5b876..0ee68b60d 100644 --- a/pkg/server/api/connect/v1/profile_resolvers.go +++ b/pkg/server/api/connect/v1/profile_resolvers.go @@ -47,6 +47,7 @@ func (r *mutationResolver) CreateUser(ctx context.Context, input types.CreateUse } r.logger.ErrorCtx(ctx, "cannot create user", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -66,7 +67,6 @@ func (r *mutationResolver) DeactivateUser(ctx context.Context, input types.Deact input.ProfileID, coredata.ProfileStateInactive, ) - if err != nil { r.logger.ErrorCtx(ctx, "cannot deactivate profile", log.Error(err)) return nil, gqlutils.Internal(ctx) @@ -113,8 +113,10 @@ func (r *mutationResolver) RemoveUser(ctx context.Context, input types.RemoveUse err := r.iam.OrganizationService.RemoveUser(ctx, input.OrganizationID, input.ProfileID) if err != nil { - var errManagedBySCIM *iam.ErrUserManagedBySCIM - var errLastActiveOwner *iam.ErrLastActiveOwner + var ( + errManagedBySCIM *iam.ErrUserManagedBySCIM + errLastActiveOwner *iam.ErrLastActiveOwner + ) if errors.As(err, &errManagedBySCIM) { return nil, gqlutils.Conflict(ctx, err) @@ -125,6 +127,7 @@ func (r *mutationResolver) RemoveUser(ctx context.Context, input types.RemoveUse } r.logger.ErrorCtx(ctx, "cannot remove user from organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -150,6 +153,7 @@ func (r *profileResolver) Identity(ctx context.Context, obj *types.Profile) (*ty } r.logger.ErrorCtx(ctx, "cannot get identity", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -170,6 +174,7 @@ func (r *profileResolver) Organization(ctx context.Context, obj *types.Profile) } r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -190,6 +195,7 @@ func (r *profileResolver) Membership(ctx context.Context, obj *types.Profile) (* } r.logger.ErrorCtx(ctx, "cannot get membership", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -232,6 +238,7 @@ func (r *profileConnectionResolver) TotalCount(ctx context.Context, obj *types.P r.logger.ErrorCtx(ctx, "cannot count profiles", log.Error(err)) return nil, gqlutils.Internal(ctx) } + return &count, nil case *organizationResolver: count, err := r.iam.OrganizationService.CountProfiles(ctx, obj.ParentID, obj.Filters) @@ -239,10 +246,12 @@ func (r *profileConnectionResolver) TotalCount(ctx context.Context, obj *types.P r.logger.ErrorCtx(ctx, "cannot count profiles", log.Error(err)) return nil, gqlutils.Internal(ctx) } + return &count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver", log.Any("resolver", obj.Resolver)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/connect/v1/saml_handler.go b/pkg/server/api/connect/v1/saml_handler.go index 5de3ff3c1..219cda091 100644 --- a/pkg/server/api/connect/v1/saml_handler.go +++ b/pkg/server/api/connect/v1/saml_handler.go @@ -100,9 +100,9 @@ func (h *SAMLHandler) ConsumeHandler(w http.ResponseWriter, r *http.Request) { } continueURL := "/organizations/" + membership.OrganizationID.String() + if len(relayState) > gid.EncodedGIDSize { unescapedContinueURL, err := url.QueryUnescape(relayState[gid.EncodedGIDSize:]) - if err != nil { h.logger.WarnCtx(ctx, "cannot unescape continue URL from RelayState", log.Error(err)) } else { @@ -118,6 +118,7 @@ func (h *SAMLHandler) ConsumeHandler(w http.ResponseWriter, r *http.Request) { if err != nil { h.logger.ErrorCtx(ctx, "cannot open root session", log.Error(err)) h.renderInternalServerError(w) + return } case rootSession.IdentityID != user.ID: @@ -125,6 +126,7 @@ func (h *SAMLHandler) ConsumeHandler(w http.ResponseWriter, r *http.Request) { if err != nil { h.logger.ErrorCtx(ctx, "cannot close session", log.Error(err)) h.renderInternalServerError(w) + return } @@ -132,6 +134,7 @@ func (h *SAMLHandler) ConsumeHandler(w http.ResponseWriter, r *http.Request) { if err != nil { h.logger.ErrorCtx(ctx, "cannot open root session", log.Error(err)) h.renderInternalServerError(w) + return } } @@ -140,6 +143,7 @@ func (h *SAMLHandler) ConsumeHandler(w http.ResponseWriter, r *http.Request) { if err != nil { h.logger.ErrorCtx(ctx, "cannot open SAML child session", log.Error(err)) h.renderInternalServerError(w) + return } diff --git a/pkg/server/api/connect/v1/saml_resolvers.go b/pkg/server/api/connect/v1/saml_resolvers.go index 34e2483c8..6511ece42 100644 --- a/pkg/server/api/connect/v1/saml_resolvers.go +++ b/pkg/server/api/connect/v1/saml_resolvers.go @@ -43,7 +43,6 @@ func (r *mutationResolver) CreateSAMLConfiguration(ctx context.Context, input ty input.OrganizationID, req, ) - if err != nil { var errSAMLConfigurationEmailDomainAlreadyExists *iam.ErrSAMLConfigurationEmailDomainAlreadyExists if errors.As(err, &errSAMLConfigurationEmailDomainAlreadyExists) { @@ -51,6 +50,7 @@ func (r *mutationResolver) CreateSAMLConfiguration(ctx context.Context, input ty } r.logger.ErrorCtx(ctx, "cannot create saml configuration", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -133,10 +133,12 @@ func (r *sAMLConfigurationConnectionResolver) TotalCount(ctx context.Context, ob r.logger.ErrorCtx(ctx, "cannot count saml configurations", log.Error(err)) return nil, gqlutils.Internal(ctx) } + return &count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver", log.Any("resolver", obj.Resolver)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/connect/v1/scim_handler.go b/pkg/server/api/connect/v1/scim_handler.go index e20d2bc06..aca424da7 100644 --- a/pkg/server/api/connect/v1/scim_handler.go +++ b/pkg/server/api/connect/v1/scim_handler.go @@ -139,6 +139,7 @@ func (h *SCIMHandler) BearerTokenMiddleware(next http.Handler) http.Handler { h.logger.ErrorCtx(r.Context(), "SCIM token validation error", log.Error(err)) httpserver.RenderError(w, http.StatusInternalServerError, errors.New("internal server error")) + return } @@ -157,13 +158,17 @@ func (rc *scimRequestContext) logAndWrapError(err error, logMsg string) error { if scimErr.Status == http.StatusNotFound { userName = "" } + rc.handler.handler.iam.SCIMService.LogEvent(rc.ctx, rc.config, rc.method, rc.path, userName, rc.ipAddress, scimErr.Status, &errMsg) + return err } rc.handler.handler.logger.ErrorCtx(rc.ctx, logMsg, log.Error(err)) + errMsg := "internal server error" rc.handler.handler.iam.SCIMService.LogEvent(rc.ctx, rc.config, rc.method, rc.path, rc.userName, rc.ipAddress, 500, &errMsg) + return scimerrors.ScimErrorInternal } @@ -236,6 +241,7 @@ func (h *scimResourceHandler) GetAll(r *http.Request, params scim.ListRequestPar } var filterExpr scimfilter.Expression + if params.FilterValidator != nil { if err := params.FilterValidator.Validate(); err != nil { return scim.Page{}, rc.logAndWrapError(scimerrors.ScimErrorBadRequest(err.Error()), "invalid filter") @@ -250,6 +256,7 @@ func (h *scimResourceHandler) GetAll(r *http.Request, params scim.ListRequestPar } rc.logSuccess(200) + return scim.Page{ TotalResults: totalCount, Resources: resources, diff --git a/pkg/server/api/connect/v1/scim_resolvers.go b/pkg/server/api/connect/v1/scim_resolvers.go index 2bbcf9519..a236adf46 100644 --- a/pkg/server/api/connect/v1/scim_resolvers.go +++ b/pkg/server/api/connect/v1/scim_resolvers.go @@ -44,6 +44,7 @@ func (r *mutationResolver) CreateSCIMConfiguration(ctx context.Context, input ty r.logger.ErrorCtx(ctx, "cannot create scim bridge", log.Error(err)) return nil, gqlutils.Internal(ctx) } + bridge = types.NewSCIMBridge(scimBridge) } @@ -188,6 +189,7 @@ func (r *sCIMConfigurationResolver) Organization(ctx context.Context, obj *types } r.logger.ErrorCtx(ctx, "cannot get organization for scim configuration", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -212,6 +214,7 @@ func (r *sCIMConfigurationResolver) Bridge(ctx context.Context, obj *types.SCIMC } r.logger.ErrorCtx(ctx, "cannot get scim bridge", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -267,10 +270,12 @@ func (r *sCIMEventConnectionResolver) TotalCount(ctx context.Context, obj *types r.logger.ErrorCtx(ctx, "cannot count scim events", log.Error(err)) return nil, gqlutils.Internal(ctx) } + return &count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver", log.Any("resolver", obj.Resolver)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/connect/v1/session_resolvers.go b/pkg/server/api/connect/v1/session_resolvers.go index 74d6c50a3..182bc958c 100644 --- a/pkg/server/api/connect/v1/session_resolvers.go +++ b/pkg/server/api/connect/v1/session_resolvers.go @@ -38,6 +38,7 @@ func (r *mutationResolver) SignIn(ctx context.Context, input types.SignInInput) } r.logger.ErrorCtx(ctx, "cannot check credentials", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -46,6 +47,7 @@ func (r *mutationResolver) SignIn(ctx context.Context, input types.SignInInput) switch { case session == nil: var err error + session, err = r.iam.AuthService.OpenSessionWithPassword( ctx, identity.ID, @@ -75,17 +77,21 @@ func (r *mutationResolver) SignIn(ctx context.Context, input types.SignInInput) if input.OrganizationID != nil { var err error + _, _, err = r.iam.SessionService.OpenPasswordChildSessionForOrganization(ctx, session.ID, *input.OrganizationID) if err != nil { // Here session middleware already took care of expired/nil root session so we only handle membership related errors - var errMembershipNotFound *iam.ErrMembershipNotFound - var errUserInactive *iam.ErrUserInactive + var ( + errMembershipNotFound *iam.ErrMembershipNotFound + errUserInactive *iam.ErrUserInactive + ) if errors.As(err, &errMembershipNotFound) || errors.As(err, &errUserInactive) { return nil, gqlutils.Forbiddenf(ctx, "forbidden") } r.logger.ErrorCtx(ctx, "cannot assume organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } } @@ -118,6 +124,7 @@ func (r *mutationResolver) SignUp(ctx context.Context, input types.SignUpInput) } r.logger.ErrorCtx(ctx, "cannot create identity with password", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -141,6 +148,7 @@ func (r *mutationResolver) SignOut(ctx context.Context) (*types.SignOutPayload, } r.logger.ErrorCtx(ctx, "cannot close session", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -163,7 +171,6 @@ func (r *mutationResolver) ActivateAccount(ctx context.Context, input types.Acti r.logger.ErrorCtx(ctx, "cannot close session", log.Error(err)) return nil, gqlutils.Internal(ctx) } - } w := gqlutils.HTTPResponseWriterFromContext(ctx) @@ -196,10 +203,12 @@ func (r *mutationResolver) ActivateAccount(ctx context.Context, input types.Acti } r.logger.ErrorCtx(ctx, "cannot activate account from invitation", log.Error(err)) + return nil, gqlutils.Internal(ctx) } var ssoLoginURL *string + samlConfigs, err := r.iam.AccountService.ListSAMLConfigurationsForEmail(ctx, user.EmailAddress) if err != nil { r.logger.ErrorCtx(ctx, "cannot list saml configurations", log.Error(err)) @@ -223,6 +232,7 @@ func (r *mutationResolver) ActivateAccount(ctx context.Context, input types.Acti } var createPasswordToken *string + if identity.HashedPassword == nil { token, err := r.iam.AuthService.GetResetPasswordToken(ctx, identity.EmailAddress) if err != nil { @@ -272,6 +282,7 @@ func (r *mutationResolver) ResetPassword(ctx context.Context, input types.ResetP } r.logger.ErrorCtx(ctx, "cannot reset password", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -307,6 +318,7 @@ func (r *mutationResolver) VerifyEmail(ctx context.Context, input types.VerifyEm } r.logger.ErrorCtx(ctx, "cannot verify email", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -344,6 +356,7 @@ func (r *mutationResolver) ChangePassword(ctx context.Context, input types.Chang } r.logger.ErrorCtx(ctx, "cannot change password", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -379,6 +392,7 @@ func (r *mutationResolver) ChangeEmail(ctx context.Context, input types.ChangeEm } r.logger.ErrorCtx(ctx, "cannot change email", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -447,6 +461,7 @@ func (r *mutationResolver) RevokeSession(ctx context.Context, input types.Revoke } r.logger.ErrorCtx(ctx, "cannot revoke session", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -506,6 +521,7 @@ func (r *sessionConnectionResolver) TotalCount(ctx context.Context, obj *types.S } r.logger.ErrorCtx(ctx, "unsupported resolver", log.Any("resolver", obj.Resolver)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/connect/v1/types/oauth2.go b/pkg/server/api/connect/v1/types/oauth2.go index bc6f935b5..820742d3f 100644 --- a/pkg/server/api/connect/v1/types/oauth2.go +++ b/pkg/server/api/connect/v1/types/oauth2.go @@ -44,6 +44,7 @@ func parseScopes(s string) (coredata.OAuth2Scopes, error) { if err := scopes.UnmarshalText([]byte(s)); err != nil { return nil, err } + return scopes, nil } diff --git a/pkg/server/api/connect/v1/types/page_info.go b/pkg/server/api/connect/v1/types/page_info.go index 52593eaee..83e2009f6 100644 --- a/pkg/server/api/connect/v1/types/page_info.go +++ b/pkg/server/api/connect/v1/types/page_info.go @@ -21,6 +21,7 @@ import ( func NewPageInfo[T page.Paginable[O], O page.OrderField](p *page.Page[T, O]) *PageInfo { data := pageinfo.NewPageInfo(p) + return &PageInfo{ HasNextPage: data.HasNextPage, HasPreviousPage: data.HasPreviousPage, diff --git a/pkg/server/api/console/v1/access_review_campaign_resolvers.go b/pkg/server/api/console/v1/access_review_campaign_resolvers.go index cd3f12d2f..88cad8d90 100644 --- a/pkg/server/api/console/v1/access_review_campaign_resolvers.go +++ b/pkg/server/api/console/v1/access_review_campaign_resolvers.go @@ -34,6 +34,7 @@ func (r *accessEntryResolver) Campaign(ctx context.Context, obj *types.AccessEnt if errors.Is(err, coredata.ErrResourceNotFound) { return nil, gqlutils.NotFound(ctx, err) } + panic(fmt.Errorf("cannot get access review campaign: %w", err)) } @@ -53,6 +54,7 @@ func (r *accessEntryResolver) AccessSource(ctx context.Context, obj *types.Acces if errors.Is(err, coredata.ErrResourceNotFound) { return nil, gqlutils.NotFound(ctx, err) } + panic(fmt.Errorf("cannot get access source: %w", err)) } @@ -96,12 +98,15 @@ func (r *accessEntryConnectionResolver) TotalCount(ctx context.Context, obj *typ if err != nil { panic(fmt.Errorf("cannot count access entries: %w", err)) } + return count, nil } + count, err := r.accessReview.Entries(scope).CountForCampaignID(ctx, obj.ParentID, obj.Filter) if err != nil { panic(fmt.Errorf("cannot count access entries: %w", err)) } + return count, nil } @@ -248,6 +253,7 @@ func (r *accessReviewCampaignResolver) Entries(ctx context.Context, obj *types.A } else { p, err = r.accessReview.Entries(scope).ListForCampaignID(ctx, obj.ID, cursor, filter) } + if err != nil { panic(fmt.Errorf("cannot list access entries: %w", err)) } @@ -302,6 +308,7 @@ func (r *accessReviewCampaignConnectionResolver) TotalCount(ctx context.Context, if err != nil { panic(fmt.Errorf("cannot count access review campaigns: %w", err)) } + return count, nil } @@ -335,6 +342,7 @@ func (r *accessReviewCampaignScopeSourceResolver) Entries(ctx context.Context, o } sourceID := obj.ID + return types.NewAccessEntryConnection(p, r, obj.CampaignID, &sourceID, filter), nil } @@ -372,6 +380,7 @@ func (r *accessSourceResolver) Connector(ctx context.Context, obj *types.AccessS if errors.Is(err, coredata.ErrResourceNotFound) { return nil, nil } + panic(fmt.Errorf("cannot get connector: %w", err)) } @@ -395,6 +404,7 @@ func (r *accessSourceResolver) ProviderOrganizations(ctx context.Context, obj *t if errors.Is(err, coredata.ErrResourceNotFound) { return []*types.ProviderOrganization{}, nil } + return nil, fmt.Errorf("cannot get connector HTTP client: %w", err) } @@ -412,6 +422,7 @@ func (r *accessSourceResolver) ProviderOrganizations(ctx context.Context, obj *t for i, o := range orgs { result[i] = &types.ProviderOrganization{Slug: o.Slug, DisplayName: o.DisplayName} } + return result, nil } @@ -437,6 +448,7 @@ func (r *accessSourceResolver) NeedsConfiguration(ctx context.Context, obj *type if errors.Is(err, coredata.ErrResourceNotFound) { return false, nil } + panic(fmt.Errorf("cannot get connector: %w", err)) } @@ -444,6 +456,7 @@ func (r *accessSourceResolver) NeedsConfiguration(ctx context.Context, obj *type if !ok || !cfg.NeedsPicker { return false, nil } + return cfg.SelectedSlug(dbConnector) == "", nil } @@ -460,6 +473,7 @@ func (r *accessSourceResolver) ConnectionStatus(ctx context.Context, obj *types. if errors.Is(err, coredata.ErrResourceNotFound) { return types.AccessSourceConnectionStatusNotApplicable, nil } + return types.AccessSourceConnectionStatusDisconnected, nil } @@ -495,6 +509,7 @@ func (r *accessSourceResolver) SelectedOrganization(ctx context.Context, obj *ty if errors.Is(err, coredata.ErrResourceNotFound) { return nil, nil } + panic(fmt.Errorf("cannot get connector: %w", err)) } @@ -502,10 +517,12 @@ func (r *accessSourceResolver) SelectedOrganization(ctx context.Context, obj *ty if !ok { return nil, nil } + slug := cfg.SelectedSlug(dbConnector) if slug == "" { return nil, nil } + return &slug, nil } @@ -524,6 +541,7 @@ func (r *accessSourceConnectionResolver) TotalCount(ctx context.Context, obj *ty if err != nil { panic(fmt.Errorf("cannot count access sources: %w", err)) } + return count, nil } @@ -568,9 +586,11 @@ func (r *mutationResolver) UpdateAccessSource(ctx context.Context, input types.U if input.Name.IsSet() { req.Name = input.Name.Value() } + if input.ConnectorID.IsSet() { req.ConnectorID = gqlutils.UnwrapOmittable(input.ConnectorID) } + if input.CSVData.IsSet() { req.CsvData = gqlutils.UnwrapOmittable(input.CSVData) } @@ -580,6 +600,7 @@ func (r *mutationResolver) UpdateAccessSource(ctx context.Context, input types.U if errors.Is(err, coredata.ErrResourceNotFound) { return nil, gqlutils.NotFound(ctx, err) } + panic(fmt.Errorf("cannot update access source: %w", err)) } @@ -600,6 +621,7 @@ func (r *mutationResolver) DeleteAccessSource(ctx context.Context, input types.D if errors.Is(err, coredata.ErrResourceNotFound) { return nil, gqlutils.NotFound(ctx, err) } + panic(fmt.Errorf("cannot delete access source: %w", err)) } @@ -627,6 +649,7 @@ func (r *mutationResolver) ConfigureAccessSource(ctx context.Context, input type if errors.Is(err, coredata.ErrResourceNotFound) { return nil, gqlutils.NotFound(ctx, err) } + panic(fmt.Errorf("cannot configure access source: %w", err)) } @@ -678,9 +701,11 @@ func (r *mutationResolver) UpdateAccessReviewCampaign(ctx context.Context, input if input.Name.IsSet() { req.Name = input.Name.Value() } + if input.Description.IsSet() { req.Description = input.Description.Value() } + if input.FrameworkControls.IsSet() { controls := input.FrameworkControls.Value() req.FrameworkControls = &controls @@ -691,6 +716,7 @@ func (r *mutationResolver) UpdateAccessReviewCampaign(ctx context.Context, input if errors.Is(err, coredata.ErrResourceNotFound) { return nil, gqlutils.NotFound(ctx, err) } + panic(fmt.Errorf("cannot update access review campaign: %w", err)) } @@ -711,6 +737,7 @@ func (r *mutationResolver) DeleteAccessReviewCampaign(ctx context.Context, input if errors.Is(err, coredata.ErrResourceNotFound) { return nil, gqlutils.NotFound(ctx, err) } + panic(fmt.Errorf("cannot delete access review campaign: %w", err)) } @@ -850,6 +877,7 @@ func (r *mutationResolver) RecordAccessEntryDecision(ctx context.Context, input if errors.Is(err, coredata.ErrResourceNotFound) { return nil, gqlutils.NotFound(ctx, err) } + panic(fmt.Errorf("cannot record access entry decision: %w", err)) } @@ -893,6 +921,7 @@ func (r *mutationResolver) RecordAccessEntryDecisions(ctx context.Context, input decisions := make([]accessreview.RecordAccessEntryDecisionRequest, len(input.Decisions)) for i, d := range input.Decisions { var decidedByID *gid.GID + organizationID, err := r.accessReview.ResolveEntryOrganizationID(ctx, d.AccessEntryID) if err == nil { if cached, ok := profileCache[organizationID]; ok { @@ -902,6 +931,7 @@ func (r *mutationResolver) RecordAccessEntryDecisions(ctx context.Context, input if err == nil { decidedByID = &profile.ID } + profileCache[organizationID] = decidedByID } } @@ -919,6 +949,7 @@ func (r *mutationResolver) RecordAccessEntryDecisions(ctx context.Context, input if errors.Is(err, coredata.ErrResourceNotFound) { return nil, gqlutils.NotFound(ctx, err) } + panic(fmt.Errorf("cannot record access entry decisions: %w", err)) } @@ -949,6 +980,7 @@ func (r *mutationResolver) FlagAccessEntry(ctx context.Context, input types.Flag if errors.Is(err, coredata.ErrResourceNotFound) { return nil, gqlutils.NotFound(ctx, err) } + panic(fmt.Errorf("cannot flag access entry: %w", err)) } diff --git a/pkg/server/api/console/v1/asset_resolvers.go b/pkg/server/api/console/v1/asset_resolvers.go index 9db646d93..07403b6c8 100644 --- a/pkg/server/api/console/v1/asset_resolvers.go +++ b/pkg/server/api/console/v1/asset_resolvers.go @@ -38,6 +38,7 @@ func (r *assetResolver) Owner(ctx context.Context, obj *types.Asset) (*types.Pro } r.logger.ErrorCtx(ctx, "cannot get owner", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -84,7 +85,6 @@ func (r *assetResolver) Organization(ctx context.Context, obj *types.Asset) (*ty asset, err := prb.Assets.Get(ctx, obj.ID) if err != nil { - r.logger.ErrorCtx(ctx, "cannot load audit", log.Error(err)) return nil, gqlutils.Internal(ctx) } @@ -96,6 +96,7 @@ func (r *assetResolver) Organization(ctx context.Context, obj *types.Asset) (*ty } r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -122,10 +123,12 @@ func (r *assetConnectionResolver) TotalCount(ctx context.Context, obj *types.Ass r.logger.ErrorCtx(ctx, "cannot count assets", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver") + return 0, gqlutils.Internal(ctx) } @@ -194,6 +197,7 @@ func (r *datumResolver) Organization(ctx context.Context, obj *types.Datum) (*ty } r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -220,10 +224,12 @@ func (r *datumConnectionResolver) TotalCount(ctx context.Context, obj *types.Dat r.logger.ErrorCtx(ctx, "cannot count data", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver") + return 0, gqlutils.Internal(ctx) } @@ -247,12 +253,13 @@ func (r *mutationResolver) CreateAsset(ctx context.Context, input types.CreateAs ThirdPartyIDs: input.ThirdPartyIds, }, ) - if err != nil { if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create asset", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -285,7 +292,9 @@ func (r *mutationResolver) UpdateAsset(ctx context.Context, input types.UpdateAs if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update asset", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -331,12 +340,13 @@ func (r *mutationResolver) CreateDatum(ctx context.Context, input types.CreateDa ThirdPartyIDs: input.ThirdPartyIds, }, ) - if err != nil { if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create datum", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -363,12 +373,13 @@ func (r *mutationResolver) UpdateDatum(ctx context.Context, input types.UpdateDa ThirdPartyIDs: input.ThirdPartyIds, }, ) - if err != nil { if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update datum", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -408,10 +419,13 @@ func (r *mutationResolver) PublishDataList(ctx context.Context, input types.Publ if errors.Is(err, coredata.ErrResourceAlreadyExists) { return nil, gqlutils.Conflict(ctx, err) } + if errMinor, ok := errors.AsType[*probo.ErrCannotPublishMinorWithoutMajor](err); ok { return nil, gqlutils.Invalid(ctx, errMinor) } + r.logger.ErrorCtx(ctx, "cannot publish data list", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -434,10 +448,13 @@ func (r *mutationResolver) PublishAssetList(ctx context.Context, input types.Pub if errors.Is(err, coredata.ErrResourceAlreadyExists) { return nil, gqlutils.Conflict(ctx, err) } + if errMinor, ok := errors.AsType[*probo.ErrCannotPublishMinorWithoutMajor](err); ok { return nil, gqlutils.Invalid(ctx, errMinor) } + r.logger.ErrorCtx(ctx, "cannot publish asset list", log.Error(err)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/console/v1/audit_resolvers.go b/pkg/server/api/console/v1/audit_resolvers.go index 626827ff9..6dd17d305 100644 --- a/pkg/server/api/console/v1/audit_resolvers.go +++ b/pkg/server/api/console/v1/audit_resolvers.go @@ -39,6 +39,7 @@ func (r *auditResolver) Organization(ctx context.Context, obj *types.Audit) (*ty } r.logger.ErrorCtx(ctx, "cannot load organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -60,6 +61,7 @@ func (r *auditResolver) Framework(ctx context.Context, obj *types.Audit) (*types } r.logger.ErrorCtx(ctx, "cannot load framework", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -85,6 +87,7 @@ func (r *auditResolver) Report(ctx context.Context, obj *types.Audit) (*types.Re } r.logger.ErrorCtx(ctx, "cannot load report", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -212,6 +215,7 @@ func (r *auditConnectionResolver) TotalCount(ctx context.Context, obj *types.Aud r.logger.ErrorCtx(ctx, "cannot count audits", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil case *findingResolver: count, err := prb.Audits.CountForFindingID(ctx, obj.ParentID) @@ -219,6 +223,7 @@ func (r *auditConnectionResolver) TotalCount(ctx context.Context, obj *types.Aud r.logger.ErrorCtx(ctx, "cannot count audits", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil case *controlResolver: count, err := prb.Audits.CountForControlID(ctx, obj.ParentID) @@ -226,6 +231,7 @@ func (r *auditConnectionResolver) TotalCount(ctx context.Context, obj *types.Aud r.logger.ErrorCtx(ctx, "cannot count audits", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil default: r.logger.ErrorCtx(ctx, "unsupported resolver", log.Any("resolver", obj.Resolver)) @@ -248,6 +254,7 @@ func (r *findingResolver) Organization(ctx context.Context, obj *types.Finding) } r.logger.ErrorCtx(ctx, "cannot get finding organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -303,6 +310,7 @@ func (r *findingResolver) Owner(ctx context.Context, obj *types.Finding) (*types } r.logger.ErrorCtx(ctx, "cannot get finding owner", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -328,6 +336,7 @@ func (r *findingResolver) Risk(ctx context.Context, obj *types.Finding) (*types. } r.logger.ErrorCtx(ctx, "cannot get finding risk", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -369,6 +378,7 @@ func (r *findingConnectionResolver) TotalCount(ctx context.Context, obj *types.F r.logger.ErrorCtx(ctx, "cannot count findings", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil case *auditResolver: count, err := prb.Findings.CountForAuditID(ctx, obj.ParentID, findingFilter) @@ -376,10 +386,12 @@ func (r *findingConnectionResolver) TotalCount(ctx context.Context, obj *types.F r.logger.ErrorCtx(ctx, "cannot count findings", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver", log.Any("resolver", obj.Resolver)) + return 0, gqlutils.Internal(ctx) } @@ -406,7 +418,9 @@ func (r *mutationResolver) CreateAudit(ctx context.Context, input types.CreateAu if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create audit", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -426,7 +440,9 @@ func (r *mutationResolver) CreateAudit(ctx context.Context, input types.CreateAu if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot upload audit report", log.Error(err)) + return nil, gqlutils.Internal(ctx) } } @@ -458,7 +474,9 @@ func (r *mutationResolver) UpdateAudit(ctx context.Context, input types.UpdateAu if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update audit", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -509,7 +527,9 @@ func (r *mutationResolver) UploadAuditReport(ctx context.Context, input types.Up if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot upload audit report", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -566,7 +586,9 @@ func (r *mutationResolver) CreateFinding(ctx context.Context, input types.Create if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create finding", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -603,7 +625,9 @@ func (r *mutationResolver) UpdateFinding(ctx context.Context, input types.Update if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update finding", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -684,10 +708,13 @@ func (r *mutationResolver) PublishFindingList(ctx context.Context, input types.P if errors.Is(err, coredata.ErrResourceAlreadyExists) { return nil, gqlutils.Conflict(ctx, err) } + if errMinor, ok := errors.AsType[*probo.ErrCannotPublishMinorWithoutMajor](err); ok { return nil, gqlutils.Invalid(ctx, errMinor) } + r.logger.ErrorCtx(ctx, "cannot publish finding list", log.Error(err)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/console/v1/base_resolvers.go b/pkg/server/api/console/v1/base_resolvers.go index 169c07a6c..f05eb3d65 100644 --- a/pkg/server/api/console/v1/base_resolvers.go +++ b/pkg/server/api/console/v1/base_resolvers.go @@ -37,6 +37,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewOrganization(organization), nil } case coredata.ThirdPartyEntityType: @@ -46,6 +47,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewThirdParty(thirdParty), nil } case coredata.FrameworkEntityType: @@ -55,6 +57,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewFramework(framework), nil } case coredata.MeasureEntityType: @@ -64,6 +67,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewMeasure(measure), nil } case coredata.TaskEntityType: @@ -73,6 +77,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewTask(task), nil } case coredata.EvidenceEntityType: @@ -82,6 +87,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewEvidence(evidence), nil } case coredata.DocumentEntityType: @@ -91,6 +97,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewDocument(document), nil } case coredata.ControlEntityType: @@ -100,6 +107,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewControl(control), nil } case coredata.RiskEntityType: @@ -109,6 +117,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewRisk(risk), nil } case coredata.RiskAssessmentEntityType: @@ -178,6 +187,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewThirdPartyComplianceReport(thirdPartyComplianceReport), nil } case coredata.ThirdPartyContactEntityType: @@ -187,6 +197,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewThirdPartyContact(thirdPartyContact), nil } case coredata.ThirdPartyServiceEntityType: @@ -196,6 +207,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewThirdPartyService(thirdPartyService), nil } case coredata.DocumentVersionEntityType: @@ -205,6 +217,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewDocumentVersion(documentVersion), nil } case coredata.DocumentVersionSignatureEntityType: @@ -214,6 +227,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewDocumentVersionSignature(documentVersionSignature), nil } case coredata.AssetEntityType: @@ -223,6 +237,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewAsset(asset), nil } case coredata.DatumEntityType: @@ -232,6 +247,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewDatum(datum), nil } case coredata.AuditEntityType: @@ -241,6 +257,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewAudit(audit), nil } case coredata.FindingEntityType: @@ -250,6 +267,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewFinding(finding), nil } case coredata.ObligationEntityType: @@ -259,6 +277,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewObligation(obligation), nil } case coredata.ReportEntityType: @@ -268,6 +287,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewReport(report), nil } case coredata.ProcessingActivityEntityType: @@ -277,6 +297,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewProcessingActivity(processingActivity), nil } case coredata.DataProtectionImpactAssessmentEntityType: @@ -287,6 +308,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewDataProtectionImpactAssessment(dpia), nil } case coredata.TransferImpactAssessmentEntityType: @@ -297,6 +319,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewTransferImpactAssessment(tia), nil } case coredata.TrustCenterEntityType: @@ -324,6 +347,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewTrustCenterAccess(trustCenterAccess), nil } case coredata.RightsRequestEntityType: @@ -333,6 +357,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewRightsRequest(rightsRequest), nil } case coredata.StatementOfApplicabilityEntityType: @@ -342,6 +367,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewStatementOfApplicability(statementOfApplicability), nil } case coredata.WebhookSubscriptionEntityType: @@ -351,76 +377,91 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if err != nil { return nil, err } + return types.NewWebhookSubscription(wc), nil } case coredata.AccessReviewCampaignEntityType: action = probo.ActionAccessReviewCampaignGet loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) { scope := coredata.NewScopeFromObjectID(id) + campaign, err := r.accessReview.Campaigns(scope).Get(ctx, id) if err != nil { return nil, err } + return types.NewAccessReviewCampaign(campaign), nil } case coredata.AccessSourceEntityType: action = probo.ActionAccessSourceGet loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) { scope := coredata.NewScopeFromObjectID(id) + source, err := r.accessReview.Sources(scope).Get(ctx, id) if err != nil { return nil, err } + return types.NewAccessSource(source), nil } case coredata.AccessEntryEntityType: action = probo.ActionAccessEntryGet loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) { scope := coredata.NewScopeFromObjectID(id) + entry, err := r.accessReview.Entries(scope).Get(ctx, id) if err != nil { return nil, err } + return types.NewAccessEntry(entry), nil } case coredata.CookieBannerEntityType: action = probo.ActionCookieBannerGet loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) { scope := coredata.NewScopeFromObjectID(id) + banner, err := r.cookieBanner.GetCookieBanner(ctx, scope, id) if err != nil { return nil, err } + return types.NewCookieBanner(banner), nil } case coredata.CookieCategoryEntityType: action = probo.ActionCookieCategoryGet loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) { scope := coredata.NewScopeFromObjectID(id) + category, err := r.cookieBanner.GetCookieCategory(ctx, scope, id) if err != nil { return nil, err } + return types.NewCookieCategory(category), nil } case coredata.CookieConsentRecordEntityType: action = probo.ActionCookieConsentRecordList loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) { scope := coredata.NewScopeFromObjectID(id) + record, err := r.cookieBanner.GetCookieConsentRecord(ctx, scope, id) if err != nil { return nil, err } + return types.NewCookieConsentRecord(record), nil } case coredata.CookieBannerVersionEntityType: action = probo.ActionCookieBannerVersionGet loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) { scope := coredata.NewScopeFromObjectID(id) + version, err := r.cookieBanner.GetCookieBannerVersion(ctx, scope, id) if err != nil { return nil, err } + return &types.CookieBannerVersion{ ID: version.ID, Version: version.Version, @@ -443,6 +484,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error } r.logger.ErrorCtx(ctx, "cannot load node", log.Error(err)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/console/v1/connector_initiate.go b/pkg/server/api/console/v1/connector_initiate.go index b783380ac..b1b6679f7 100644 --- a/pkg/server/api/console/v1/connector_initiate.go +++ b/pkg/server/api/console/v1/connector_initiate.go @@ -65,6 +65,7 @@ func handleConnectorInitiate( httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("authentication required")) return } + session := authn.SessionFromContext(r.Context()) if session == nil { httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("authentication required")) @@ -94,12 +95,15 @@ func handleConnectorInitiate( httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot reconnect: connector not found")) return } + if errors.Is(err, errInvalidReconnectConnector) { httpserver.RenderError(w, http.StatusBadRequest, err) return } + logger.ErrorCtx(r.Context(), "cannot look up existing connector", log.Error(err)) httpserver.RenderError(w, http.StatusInternalServerError, fmt.Errorf("internal error")) + return } @@ -118,6 +122,7 @@ func handleConnectorInitiate( if err != nil { logger.ErrorCtx(r.Context(), "cannot initiate connector", log.Error(err)) httpserver.RenderError(w, http.StatusInternalServerError, fmt.Errorf("internal error")) + return } @@ -147,6 +152,7 @@ func loadExistingConnector( if err != nil { return nil, err } + return found, nil } @@ -158,5 +164,6 @@ func loadExistingConnector( if errors.Is(err, coredata.ErrResourceNotFound) { return nil, nil } + return found, err } diff --git a/pkg/server/api/console/v1/connector_provider_info.go b/pkg/server/api/console/v1/connector_provider_info.go index af39c55d9..95a7984bf 100644 --- a/pkg/server/api/console/v1/connector_provider_info.go +++ b/pkg/server/api/console/v1/connector_provider_info.go @@ -75,5 +75,6 @@ func providerExtraSettings(provider coredata.ConnectorProvider) []*types.Connect if settings, ok := providerExtraSettingsMap[provider]; ok { return settings } + return []*types.ConnectorProviderSettingInfo{} } diff --git a/pkg/server/api/console/v1/connector_resolvers.go b/pkg/server/api/console/v1/connector_resolvers.go index 03caeb4e2..162e62001 100644 --- a/pkg/server/api/console/v1/connector_resolvers.go +++ b/pkg/server/api/console/v1/connector_resolvers.go @@ -26,6 +26,7 @@ func (r *connectorResolver) Oauth2Scopes(ctx context.Context, obj *types.Connect if scopes == nil { return []string{}, nil } + return scopes, nil } @@ -49,26 +50,31 @@ func (r *mutationResolver) CreateAPIKeyConnector(ctx context.Context, input type OrganizationID: *input.TallyOrganizationID, } } + if input.SentryOrganizationSlug != nil { req.SentrySettings = &coredata.SentryConnectorSettings{ OrganizationSlug: *input.SentryOrganizationSlug, } } + if input.SupabaseOrganizationSlug != nil { req.SupabaseSettings = &coredata.SupabaseConnectorSettings{ OrganizationSlug: *input.SupabaseOrganizationSlug, } } + if input.GithubOrganization != nil { req.GitHubSettings = &coredata.GitHubConnectorSettings{ Organization: *input.GithubOrganization, } } + if input.OnePasswordScimBridgeURL != nil { req.OnePasswordSettings = &coredata.OnePasswordConnectorSettings{ SCIMBridgeURL: *input.OnePasswordScimBridgeURL, } } + cnnctr, err := prb.Connectors.Create(ctx, req) if err != nil { if errors.Is(err, coredata.ErrResourceAlreadyExists) { diff --git a/pkg/server/api/console/v1/control_resolvers.go b/pkg/server/api/console/v1/control_resolvers.go index 5041d8cc6..860f5dda0 100644 --- a/pkg/server/api/console/v1/control_resolvers.go +++ b/pkg/server/api/console/v1/control_resolvers.go @@ -54,6 +54,7 @@ func (r *applicabilityStatementResolver) Control(ctx context.Context, obj *types } r.logger.ErrorCtx(ctx, "cannot get control", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -80,10 +81,12 @@ func (r *applicabilityStatementConnectionResolver) TotalCount(ctx context.Contex r.logger.ErrorCtx(ctx, "cannot count applicability statements", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver for applicability statement connection", log.String("resolver", fmt.Sprintf("%T", obj.Resolver))) + return 0, gqlutils.Internal(ctx) } @@ -102,8 +105,10 @@ func (r *controlResolver) Organization(ctx context.Context, obj *types.Control) } r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } + return types.NewOrganization(organization), nil } @@ -161,6 +166,7 @@ func (r *controlResolver) Framework(ctx context.Context, obj *types.Control) (*t } r.logger.ErrorCtx(ctx, "cannot get framework", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -320,6 +326,7 @@ func (r *controlConnectionResolver) TotalCount(ctx context.Context, obj *types.C r.logger.ErrorCtx(ctx, "cannot count controls", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil case *frameworkResolver: count, err := prb.Controls.CountForFrameworkID(ctx, obj.ParentID, obj.Filters) @@ -327,6 +334,7 @@ func (r *controlConnectionResolver) TotalCount(ctx context.Context, obj *types.C r.logger.ErrorCtx(ctx, "cannot count controls", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil case *documentResolver: count, err := prb.Controls.CountForDocumentID(ctx, obj.ParentID, obj.Filters) @@ -334,6 +342,7 @@ func (r *controlConnectionResolver) TotalCount(ctx context.Context, obj *types.C r.logger.ErrorCtx(ctx, "cannot count controls", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil case *measureResolver: count, err := prb.Controls.CountForMeasureID(ctx, obj.ParentID, obj.Filters) @@ -341,6 +350,7 @@ func (r *controlConnectionResolver) TotalCount(ctx context.Context, obj *types.C r.logger.ErrorCtx(ctx, "cannot count controls", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil case *riskResolver: count, err := prb.Controls.CountForRiskID(ctx, obj.ParentID, obj.Filters) @@ -348,6 +358,7 @@ func (r *controlConnectionResolver) TotalCount(ctx context.Context, obj *types.C r.logger.ErrorCtx(ctx, "cannot count controls", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil case *statementOfApplicabilityResolver: count, err := prb.Controls.CountForStatementOfApplicabilityID(ctx, obj.ParentID, obj.Filters) @@ -355,10 +366,12 @@ func (r *controlConnectionResolver) TotalCount(ctx context.Context, obj *types.C r.logger.ErrorCtx(ctx, "cannot count controls", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver") + return 0, gqlutils.Internal(ctx) } @@ -390,7 +403,9 @@ func (r *mutationResolver) CreateControl(ctx context.Context, input types.Create if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create control", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -419,7 +434,6 @@ func (r *mutationResolver) UpdateControl(ctx context.Context, input types.Update NotImplementedJustification: gqlutils.UnwrapOmittable(input.NotImplementedJustification), }, ) - if err != nil { if errors.Is(err, coredata.ErrResourceAlreadyExists) { return nil, gqlutils.Conflict(ctx, err) @@ -428,7 +442,9 @@ func (r *mutationResolver) UpdateControl(ctx context.Context, input types.Update if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update control", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -491,6 +507,7 @@ func (r *mutationResolver) CreateControlDocumentMapping(ctx context.Context, inp } r.logger.ErrorCtx(ctx, "cannot create control document mapping", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -696,10 +713,13 @@ func (r *mutationResolver) CreateStatementOfApplicability(ctx context.Context, i if errors.Is(err, coredata.ErrResourceAlreadyExists) { return nil, gqlutils.Conflict(ctx, err) } + if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create statement_of_applicability", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -732,10 +752,13 @@ func (r *mutationResolver) UpdateStatementOfApplicability(ctx context.Context, i if errors.Is(err, coredata.ErrResourceAlreadyExists) { return nil, gqlutils.Conflict(ctx, err) } + if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update statement_of_applicability", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -776,10 +799,13 @@ func (r *mutationResolver) PublishStatementOfApplicability(ctx context.Context, if errors.Is(err, coredata.ErrResourceAlreadyExists) { return nil, gqlutils.Conflict(ctx, err) } + if errMinor, ok := errors.AsType[*probo.ErrCannotPublishMinorWithoutMajor](err); ok { return nil, gqlutils.Invalid(ctx, errMinor) } + r.logger.ErrorCtx(ctx, "cannot publish statement of applicability", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -806,7 +832,9 @@ func (r *statementOfApplicabilityResolver) Document(ctx context.Context, obj *ty if errors.Is(err, coredata.ErrResourceNotFound) { return nil, nil } + r.logger.ErrorCtx(ctx, "cannot load document", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -826,7 +854,9 @@ func (r *statementOfApplicabilityResolver) Organization(ctx context.Context, obj if errors.Is(err, coredata.ErrResourceNotFound) || errors.Is(err, dataloadgen.ErrNotFound) { return nil, gqlutils.NotFound(ctx, err) } + r.logger.ErrorCtx(ctx, "cannot load organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -879,10 +909,12 @@ func (r *statementOfApplicabilityConnectionResolver) TotalCount(ctx context.Cont r.logger.ErrorCtx(ctx, "cannot count statements_of_applicability", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver") + return 0, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/console/v1/cookie_banner_resolvers.go b/pkg/server/api/console/v1/cookie_banner_resolvers.go index 47f56a566..e41e30e8d 100644 --- a/pkg/server/api/console/v1/cookie_banner_resolvers.go +++ b/pkg/server/api/console/v1/cookie_banner_resolvers.go @@ -36,7 +36,9 @@ func (r *cookieBannerResolver) Organization(ctx context.Context, obj *types.Cook if errors.Is(err, coredata.ErrResourceNotFound) || errors.Is(err, dataloadgen.ErrNotFound) { return nil, gqlutils.NotFound(ctx, err) } + r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -124,6 +126,7 @@ func (r *cookieBannerResolver) LatestVersion(ctx context.Context, obj *types.Coo } v := versions[0] + return &types.CookieBannerVersion{ ID: v.ID, Version: v.Version, @@ -331,7 +334,9 @@ func (r *cookieCategoryResolver) CookieBanner(ctx context.Context, obj *types.Co if errors.Is(err, coredata.ErrResourceNotFound) || errors.Is(err, dataloadgen.ErrNotFound) { return nil, nil } + r.logger.ErrorCtx(ctx, "cannot get cookie banner", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -446,10 +451,13 @@ func (r *mutationResolver) CreateCookieBanner(ctx context.Context, input types.C if errors.Is(err, cookiebanner.ErrOriginAlreadyInUse) { return nil, gqlutils.Conflict(ctx, err) } + if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create cookie banner", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -482,10 +490,13 @@ func (r *mutationResolver) UpdateCookieBanner(ctx context.Context, input types.U if errors.Is(err, cookiebanner.ErrBannerNotFound) { return nil, gqlutils.NotFound(ctx, err) } + if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update cookie banner", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -507,7 +518,9 @@ func (r *mutationResolver) DeleteCookieBanner(ctx context.Context, input types.D if errors.Is(err, cookiebanner.ErrBannerNotFound) { return nil, gqlutils.NotFound(ctx, err) } + r.logger.ErrorCtx(ctx, "cannot delete cookie banner", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -529,13 +542,17 @@ func (r *mutationResolver) ActivateCookieBanner(ctx context.Context, input types if errors.Is(err, cookiebanner.ErrBannerNotFound) { return nil, gqlutils.NotFound(ctx, err) } + if errors.Is(err, cookiebanner.ErrBannerAlreadyActive) { return nil, gqlutils.Conflict(ctx, err) } + if errors.Is(err, cookiebanner.ErrOriginAlreadyInUse) { return nil, gqlutils.Conflict(ctx, err) } + r.logger.ErrorCtx(ctx, "cannot activate cookie banner", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -557,10 +574,13 @@ func (r *mutationResolver) DeactivateCookieBanner(ctx context.Context, input typ if errors.Is(err, cookiebanner.ErrBannerNotFound) { return nil, gqlutils.NotFound(ctx, err) } + if errors.Is(err, cookiebanner.ErrBannerAlreadyInactive) { return nil, gqlutils.Conflict(ctx, err) } + r.logger.ErrorCtx(ctx, "cannot deactivate cookie banner", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -582,7 +602,9 @@ func (r *mutationResolver) PublishCookieBannerVersion(ctx context.Context, input if errors.Is(err, cookiebanner.ErrNoDraftVersion) { return nil, gqlutils.Conflict(ctx, err) } + r.logger.ErrorCtx(ctx, "cannot publish cookie banner version", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -627,13 +649,17 @@ func (r *mutationResolver) CreateCookieCategory(ctx context.Context, input types if errors.Is(err, cookiebanner.ErrBannerNotFound) { return nil, gqlutils.NotFound(ctx, err) } + if errors.Is(err, cookiebanner.ErrCategorySlugAlreadyExists) { return nil, gqlutils.Conflict(ctx, err) } + if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create cookie category", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -678,20 +704,26 @@ func (r *mutationResolver) UpdateCookieCategory(ctx context.Context, input types if errors.Is(err, cookiebanner.ErrCategoryNotFound) { return nil, gqlutils.NotFound(ctx, err) } + if errors.Is(err, cookiebanner.ErrCategorySlugAlreadyExists) { return nil, gqlutils.Conflict(ctx, err) } + if errors.Is(err, cookiebanner.ErrPostHogConsentKindInvalid) { return nil, gqlutils.Invalid(ctx, err) } + if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update cookie category", log.Error(err)) + return nil, gqlutils.Internal(ctx) } bannerScope := coredata.NewScopeFromObjectID(category.CookieBannerID) + banner, err := r.cookieBanner.GetCookieBanner(ctx, bannerScope, category.CookieBannerID) if err != nil { r.logger.ErrorCtx(ctx, "cannot get cookie banner", log.Error(err)) @@ -717,7 +749,9 @@ func (r *mutationResolver) DeleteCookieCategory(ctx context.Context, input types if errors.Is(err, cookiebanner.ErrCategoryNotFound) { return nil, gqlutils.NotFound(ctx, err) } + r.logger.ErrorCtx(ctx, "cannot get cookie category", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -728,14 +762,18 @@ func (r *mutationResolver) DeleteCookieCategory(ctx context.Context, input types if errors.Is(err, cookiebanner.ErrCategoryNotFound) { return nil, gqlutils.NotFound(ctx, err) } + if errors.Is(err, cookiebanner.ErrCannotDeleteSystemCategory) { return nil, gqlutils.Conflict(ctx, err) } + r.logger.ErrorCtx(ctx, "cannot delete cookie category", log.Error(err)) + return nil, gqlutils.Internal(ctx) } bannerScope := coredata.NewScopeFromObjectID(bannerID) + banner, err := r.cookieBanner.GetCookieBanner(ctx, bannerScope, bannerID) if err != nil { r.logger.ErrorCtx(ctx, "cannot get cookie banner", log.Error(err)) @@ -768,10 +806,13 @@ func (r *mutationResolver) ReorderCookieCategory(ctx context.Context, input type if errors.Is(err, cookiebanner.ErrCategoryNotFound) { return nil, gqlutils.NotFound(ctx, err) } + if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot reorder cookie category", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -801,10 +842,13 @@ func (r *mutationResolver) UpsertCookieBannerTranslation(ctx context.Context, in if errors.Is(err, cookiebanner.ErrBannerNotFound) { return nil, gqlutils.NotFound(ctx, err) } + if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot upsert cookie banner translation", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -855,14 +899,18 @@ func (r *mutationResolver) CreateTrackerPattern(ctx context.Context, input types if errors.Is(err, cookiebanner.ErrPatternAlreadyExists) { return nil, gqlutils.Conflictf(ctx, "a pattern with this name already exists in this banner") } + if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create tracker pattern", log.Error(err)) + return nil, gqlutils.Internal(ctx) } bannerScope := coredata.NewScopeFromObjectID(pattern.CookieBannerID) + banner, err := r.cookieBanner.GetCookieBanner(ctx, bannerScope, pattern.CookieBannerID) if err != nil { r.logger.ErrorCtx(ctx, "cannot get cookie banner", log.Error(err)) @@ -897,11 +945,14 @@ func (r *mutationResolver) UpdateTrackerPattern(ctx context.Context, input types if errors.Is(err, cookiebanner.ErrTrackerPatternNotFound) { return nil, gqlutils.NotFound(ctx, err) } + r.logger.ErrorCtx(ctx, "cannot update tracker pattern", log.Error(err)) + return nil, gqlutils.Internal(ctx) } bannerScope := coredata.NewScopeFromObjectID(pattern.CookieBannerID) + banner, err := r.cookieBanner.GetCookieBanner(ctx, bannerScope, pattern.CookieBannerID) if err != nil { r.logger.ErrorCtx(ctx, "cannot get cookie banner", log.Error(err)) @@ -927,7 +978,9 @@ func (r *mutationResolver) DeleteTrackerPattern(ctx context.Context, input types if errors.Is(err, cookiebanner.ErrTrackerPatternNotFound) { return nil, gqlutils.NotFound(ctx, err) } + r.logger.ErrorCtx(ctx, "cannot get tracker pattern", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -937,11 +990,14 @@ func (r *mutationResolver) DeleteTrackerPattern(ctx context.Context, input types if errors.Is(err, cookiebanner.ErrTrackerPatternNotFound) { return nil, gqlutils.NotFound(ctx, err) } + r.logger.ErrorCtx(ctx, "cannot delete tracker pattern", log.Error(err)) + return nil, gqlutils.Internal(ctx) } bannerScope := coredata.NewScopeFromObjectID(bannerID) + banner, err := r.cookieBanner.GetCookieBanner(ctx, bannerScope, bannerID) if err != nil { r.logger.ErrorCtx(ctx, "cannot get cookie banner", log.Error(err)) @@ -1023,14 +1079,18 @@ func (r *mutationResolver) CreateTrackerResource(ctx context.Context, input type if errors.Is(err, cookiebanner.ErrResourceAlreadyExists) { return nil, gqlutils.Conflictf(ctx, "a resource with this origin and path already exists in this banner") } + if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create tracker resource", log.Error(err)) + return nil, gqlutils.Internal(ctx) } bannerScope := coredata.NewScopeFromObjectID(resource.CookieBannerID) + banner, err := r.cookieBanner.GetCookieBanner(ctx, bannerScope, resource.CookieBannerID) if err != nil { r.logger.ErrorCtx(ctx, "cannot get cookie banner", log.Error(err)) @@ -1065,14 +1125,18 @@ func (r *mutationResolver) UpdateTrackerResource(ctx context.Context, input type if errors.Is(err, cookiebanner.ErrTrackerResourceNotFound) { return nil, gqlutils.NotFound(ctx, err) } + if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update tracker resource", log.Error(err)) + return nil, gqlutils.Internal(ctx) } bannerScope := coredata.NewScopeFromObjectID(resource.CookieBannerID) + banner, err := r.cookieBanner.GetCookieBanner(ctx, bannerScope, resource.CookieBannerID) if err != nil { r.logger.ErrorCtx(ctx, "cannot get cookie banner", log.Error(err)) @@ -1098,7 +1162,9 @@ func (r *mutationResolver) DeleteTrackerResource(ctx context.Context, input type if errors.Is(err, cookiebanner.ErrTrackerResourceNotFound) { return nil, gqlutils.NotFound(ctx, err) } + r.logger.ErrorCtx(ctx, "cannot get tracker resource", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -1108,11 +1174,14 @@ func (r *mutationResolver) DeleteTrackerResource(ctx context.Context, input type if errors.Is(err, cookiebanner.ErrTrackerResourceNotFound) { return nil, gqlutils.NotFound(ctx, err) } + r.logger.ErrorCtx(ctx, "cannot delete tracker resource", log.Error(err)) + return nil, gqlutils.Internal(ctx) } bannerScope := coredata.NewScopeFromObjectID(bannerID) + banner, err := r.cookieBanner.GetCookieBanner(ctx, bannerScope, bannerID) if err != nil { r.logger.ErrorCtx(ctx, "cannot get cookie banner", log.Error(err)) @@ -1180,7 +1249,9 @@ func (r *trackerPatternResolver) CookieCategory(ctx context.Context, obj *types. if errors.Is(err, coredata.ErrResourceNotFound) || errors.Is(err, dataloadgen.ErrNotFound) { return nil, nil } + r.logger.ErrorCtx(ctx, "cannot get cookie category", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -1209,8 +1280,10 @@ func (r *trackerPatternResolver) Permission(ctx context.Context, obj *types.Trac func (r *trackerPatternConnectionResolver) TotalCount(ctx context.Context, obj *types.TrackerPatternConnection) (int, error) { scope := coredata.NewScopeFromObjectID(obj.ParentID) - var count int - var err error + var ( + count int + err error + ) switch obj.Resolver.(type) { case *cookieCategoryResolver: @@ -1220,6 +1293,7 @@ func (r *trackerPatternConnectionResolver) TotalCount(ctx context.Context, obj * if obj.Filter != nil { filter = filter.WithQuery(obj.Filter.Query).WithSource(obj.Filter.Source).WithTrackerType(obj.Filter.TrackerType) } + count, err = r.cookieBanner.CountUncategorisedTrackerPatterns(ctx, scope, obj.ParentID, filter) } @@ -1244,7 +1318,9 @@ func (r *trackerResourceResolver) CookieCategory(ctx context.Context, obj *types if errors.Is(err, coredata.ErrResourceNotFound) || errors.Is(err, dataloadgen.ErrNotFound) { return nil, nil } + r.logger.ErrorCtx(ctx, "cannot get cookie category", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -1260,8 +1336,10 @@ func (r *trackerResourceResolver) Permission(ctx context.Context, obj *types.Tra func (r *trackerResourceConnectionResolver) TotalCount(ctx context.Context, obj *types.TrackerResourceConnection) (int, error) { scope := coredata.NewScopeFromObjectID(obj.ParentID) - var count int - var err error + var ( + count int + err error + ) switch obj.Resolver.(type) { case *cookieCategoryResolver: @@ -1271,6 +1349,7 @@ func (r *trackerResourceConnectionResolver) TotalCount(ctx context.Context, obj if obj.Filter != nil { filter = filter.WithQuery(obj.Filter.Query).WithResourceType(obj.Filter.Type) } + count, err = r.cookieBanner.CountUncategorisedTrackerResources(ctx, scope, obj.ParentID, filter) } diff --git a/pkg/server/api/console/v1/cookie_consent_record_resolvers.go b/pkg/server/api/console/v1/cookie_consent_record_resolvers.go index d1bf3ad38..e04c6840a 100644 --- a/pkg/server/api/console/v1/cookie_consent_record_resolvers.go +++ b/pkg/server/api/console/v1/cookie_consent_record_resolvers.go @@ -33,7 +33,9 @@ func (r *cookieConsentRecordResolver) CookieBanner(ctx context.Context, obj *typ if errors.Is(err, coredata.ErrResourceNotFound) || errors.Is(err, dataloadgen.ErrNotFound) { return nil, nil } + r.logger.ErrorCtx(ctx, "cannot get cookie banner", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -53,7 +55,9 @@ func (r *cookieConsentRecordResolver) CookieBannerVersion(ctx context.Context, o if errors.Is(err, cookiebanner.ErrVersionNotFound) { return nil, nil } + r.logger.ErrorCtx(ctx, "cannot get cookie banner version", log.Error(err)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/console/v1/data_protection_impact_assessment_resolvers.go b/pkg/server/api/console/v1/data_protection_impact_assessment_resolvers.go index 475fa2eba..065b22857 100644 --- a/pkg/server/api/console/v1/data_protection_impact_assessment_resolvers.go +++ b/pkg/server/api/console/v1/data_protection_impact_assessment_resolvers.go @@ -62,7 +62,9 @@ func (r *dataProtectionImpactAssessmentResolver) Organization(ctx context.Contex if errors.Is(err, coredata.ErrResourceNotFound) { return nil, gqlutils.NotFound(ctx, err) } + r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -89,10 +91,12 @@ func (r *dataProtectionImpactAssessmentConnectionResolver) TotalCount(ctx contex r.logger.ErrorCtx(ctx, "cannot count organization data protection impact assessments", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver") + return 0, gqlutils.Internal(ctx) } @@ -122,7 +126,9 @@ func (r *mutationResolver) CreateDataProtectionImpactAssessment(ctx context.Cont if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create data protection impact assessment", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -153,7 +159,9 @@ func (r *mutationResolver) UpdateDataProtectionImpactAssessment(ctx context.Cont if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update data protection impact assessment", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -207,7 +215,9 @@ func (r *mutationResolver) CreateTransferImpactAssessment(ctx context.Context, i if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create transfer impact assessment", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -238,7 +248,9 @@ func (r *mutationResolver) UpdateTransferImpactAssessment(ctx context.Context, i if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update transfer impact assessment", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -279,10 +291,13 @@ func (r *mutationResolver) PublishDataProtectionImpactAssessmentList(ctx context if errors.Is(err, coredata.ErrResourceAlreadyExists) { return nil, gqlutils.Conflict(ctx, err) } + if errMinor, ok := errors.AsType[*probo.ErrCannotPublishMinorWithoutMajor](err); ok { return nil, gqlutils.Invalid(ctx, errMinor) } + r.logger.ErrorCtx(ctx, "cannot publish data protection impact assessment list", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -305,10 +320,13 @@ func (r *mutationResolver) PublishTransferImpactAssessmentList(ctx context.Conte if errors.Is(err, coredata.ErrResourceAlreadyExists) { return nil, gqlutils.Conflict(ctx, err) } + if errMinor, ok := errors.AsType[*probo.ErrCannotPublishMinorWithoutMajor](err); ok { return nil, gqlutils.Invalid(ctx, errMinor) } + r.logger.ErrorCtx(ctx, "cannot publish transfer impact assessment list", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -350,6 +368,7 @@ func (r *transferImpactAssessmentResolver) Organization(ctx context.Context, obj } r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -376,10 +395,12 @@ func (r *transferImpactAssessmentConnectionResolver) TotalCount(ctx context.Cont r.logger.ErrorCtx(ctx, "cannot count organization transfer impact assessments", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver") + return 0, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/console/v1/dataloader/dataloader.go b/pkg/server/api/console/v1/dataloader/dataloader.go index 1a74b3d2f..79769b49c 100644 --- a/pkg/server/api/console/v1/dataloader/dataloader.go +++ b/pkg/server/api/console/v1/dataloader/dataloader.go @@ -102,6 +102,7 @@ func (f *batchFetcher) fetchOrganizations(ctx context.Context, keys []gid.GID) ( for _, org := range orgs { result[org.ID] = org } + return result, nil } @@ -117,6 +118,7 @@ func (f *batchFetcher) fetchFrameworks(ctx context.Context, keys []gid.GID) (map for _, v := range frameworks { result[v.ID] = v } + return result, nil } @@ -132,6 +134,7 @@ func (f *batchFetcher) fetchControls(ctx context.Context, keys []gid.GID) (map[g for _, v := range controls { result[v.ID] = v } + return result, nil } @@ -147,6 +150,7 @@ func (f *batchFetcher) fetchThirdParties(ctx context.Context, keys []gid.GID) (m for _, v := range thirdParties { result[v.ID] = v } + return result, nil } @@ -162,6 +166,7 @@ func (f *batchFetcher) fetchDocuments(ctx context.Context, keys []gid.GID) (map[ for _, v := range documents { result[v.ID] = v } + return result, nil } @@ -177,6 +182,7 @@ func (f *batchFetcher) fetchProfiles(ctx context.Context, keys []gid.GID) (map[g for _, v := range profiles { result[v.ID] = v } + return result, nil } @@ -192,6 +198,7 @@ func (f *batchFetcher) fetchRisks(ctx context.Context, keys []gid.GID) (map[gid. for _, v := range risks { result[v.ID] = v } + return result, nil } @@ -207,6 +214,7 @@ func (f *batchFetcher) fetchMeasures(ctx context.Context, keys []gid.GID) (map[g for _, v := range measures { result[v.ID] = v } + return result, nil } @@ -222,6 +230,7 @@ func (f *batchFetcher) fetchTasks(ctx context.Context, keys []gid.GID) (map[gid. for _, v := range tasks { result[v.ID] = v } + return result, nil } @@ -237,6 +246,7 @@ func (f *batchFetcher) fetchFiles(ctx context.Context, keys []gid.GID) (map[gid. for _, v := range files { result[v.ID] = v } + return result, nil } @@ -252,6 +262,7 @@ func (f *batchFetcher) fetchReports(ctx context.Context, keys []gid.GID) (map[gi for _, v := range reports { result[v.ID] = v } + return result, nil } @@ -267,6 +278,7 @@ func (f *batchFetcher) fetchCookieBanners(ctx context.Context, keys []gid.GID) ( for _, v := range banners { result[v.ID] = v } + return result, nil } @@ -282,5 +294,6 @@ func (f *batchFetcher) fetchCookieCategories(ctx context.Context, keys []gid.GID for _, v := range categories { result[v.ID] = v } + return result, nil } diff --git a/pkg/server/api/console/v1/document_resolvers.go b/pkg/server/api/console/v1/document_resolvers.go index 467d3d3bf..905acc93c 100644 --- a/pkg/server/api/console/v1/document_resolvers.go +++ b/pkg/server/api/console/v1/document_resolvers.go @@ -42,6 +42,7 @@ func (r *documentResolver) Organization(ctx context.Context, obj *types.Document } r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -160,6 +161,7 @@ func (r *documentConnectionResolver) TotalCount(ctx context.Context, obj *types. r.logger.ErrorCtx(ctx, "cannot count controls", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil case *organizationResolver: count, err := prb.Documents.CountForOrganizationID(ctx, obj.ParentID, obj.Filters) @@ -167,6 +169,7 @@ func (r *documentConnectionResolver) TotalCount(ctx context.Context, obj *types. r.logger.ErrorCtx(ctx, "cannot count documents", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil case *riskResolver: count, err := prb.Documents.CountForRiskID(ctx, obj.ParentID, obj.Filters) @@ -174,6 +177,7 @@ func (r *documentConnectionResolver) TotalCount(ctx context.Context, obj *types. r.logger.ErrorCtx(ctx, "cannot count risks", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil case *measureResolver: count, err := prb.Documents.CountForMeasureID(ctx, obj.ParentID, obj.Filters) @@ -181,10 +185,12 @@ func (r *documentConnectionResolver) TotalCount(ctx context.Context, obj *types. r.logger.ErrorCtx(ctx, "cannot count documents", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver") + return 0, gqlutils.Internal(ctx) } @@ -203,6 +209,7 @@ func (r *documentVersionResolver) Document(ctx context.Context, obj *types.Docum } r.logger.ErrorCtx(ctx, "cannot get document", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -263,16 +270,21 @@ func (r *documentVersionResolver) Signatures(ctx context.Context, obj *types.Doc } } - var signatureStates []coredata.DocumentVersionSignatureState - var activeContract *bool + var ( + signatureStates []coredata.DocumentVersionSignatureState + activeContract *bool + ) + if filter != nil { if filter.States != nil { signatureStates = filter.States } + if filter.ActiveContract != nil { activeContract = filter.ActiveContract } } + signatureFilter := coredata.NewDocumentVersionSignatureFilter(signatureStates, activeContract) cursor := types.NewCursor(first, after, last, before, pageOrderBy) @@ -355,6 +367,7 @@ func (r *documentVersionApprovalDecisionResolver) Quorum(ctx context.Context, ob } r.logger.ErrorCtx(ctx, "cannot get approval quorum", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -376,6 +389,7 @@ func (r *documentVersionApprovalDecisionResolver) DocumentVersion(ctx context.Co } r.logger.ErrorCtx(ctx, "cannot get approval quorum", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -386,6 +400,7 @@ func (r *documentVersionApprovalDecisionResolver) DocumentVersion(ctx context.Co } r.logger.ErrorCtx(ctx, "cannot get document version", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -405,6 +420,7 @@ func (r *documentVersionApprovalDecisionResolver) Approver(ctx context.Context, } r.logger.ErrorCtx(ctx, "cannot get approver profile", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -475,6 +491,7 @@ func (r *documentVersionApprovalQuorumResolver) DocumentVersion(ctx context.Cont } r.logger.ErrorCtx(ctx, "cannot get document version", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -504,6 +521,7 @@ func (r *documentVersionApprovalQuorumResolver) Decisions(ctx context.Context, o if filter != nil && filter.States != nil { approvalStates = filter.States } + approvalFilter := coredata.NewDocumentVersionApprovalDecisionFilter(approvalStates) cursor := types.NewCursor(first, after, last, before, pageOrderBy) @@ -553,15 +571,18 @@ func (r *documentVersionConnectionResolver) TotalCount(ctx context.Context, obj if obj.Filters != nil { filter = obj.Filters } + count, err := prb.Documents.CountVersionsForDocumentID(ctx, obj.ParentID, filter) if err != nil { r.logger.ErrorCtx(ctx, "cannot count document versions", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver") + return 0, gqlutils.Internal(ctx) } @@ -580,6 +601,7 @@ func (r *documentVersionSignatureResolver) DocumentVersion(ctx context.Context, } r.logger.ErrorCtx(ctx, "cannot get document version", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -601,6 +623,7 @@ func (r *documentVersionSignatureResolver) SignedBy(ctx context.Context, obj *ty } r.logger.ErrorCtx(ctx, "cannot get people", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -626,15 +649,18 @@ func (r *documentVersionSignatureConnectionResolver) TotalCount(ctx context.Cont if obj.Filters != nil { filter = obj.Filters } + count, err := prb.Documents.CountSignaturesForVersionID(ctx, obj.ParentID, filter) if err != nil { r.logger.ErrorCtx(ctx, "cannot count signatures", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver") + return 0, gqlutils.Internal(ctx) } @@ -653,7 +679,9 @@ func (r *employeeDocumentResolver) Signed(ctx context.Context, obj *types.Employ if errors.Is(err, coredata.ErrResourceNotFound) { return nil, nil } + r.logger.ErrorCtx(ctx, "cannot check if document is signed", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -675,7 +703,9 @@ func (r *employeeDocumentResolver) ApprovalState(ctx context.Context, obj *types if errors.Is(err, coredata.ErrResourceNotFound) { return nil, nil } + r.logger.ErrorCtx(ctx, "cannot get viewer approval state", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -706,6 +736,7 @@ func (r *employeeDocumentResolver) Versions(ctx context.Context, obj *types.Empl identity := authn.IdentityFromContext(ctx) var filterMode coredata.EmployeeFilterMode + switch obj.FilterMode { case types.EmployeeDocumentFilterModeSignature: filterMode = coredata.EmployeeFilterModeSignature @@ -782,6 +813,7 @@ func (r *employeeDocumentVersionResolver) ApprovalDecision(ctx context.Context, } r.logger.ErrorCtx(ctx, "cannot get viewer approval decision", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -821,7 +853,9 @@ func (r *mutationResolver) CreateDocument(ctx context.Context, input types.Creat if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create document", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -856,21 +890,25 @@ func (r *mutationResolver) UpdateDocument(ctx context.Context, input types.Updat DefaultApproverIDs: defaultApproverIDs, }, ) - if err != nil { if errors.Is(err, coredata.ErrResourceNotFound) { return nil, gqlutils.NotFound(ctx, err) } + if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok { return nil, gqlutils.Conflict(ctx, errArchived) } + if errGenerated, ok := errors.AsType[*probo.ErrDocumentVersionGenerated](err); ok { return nil, gqlutils.Conflict(ctx, errGenerated) } + if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update document", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -905,13 +943,17 @@ func (r *mutationResolver) DeleteDocumentDraft(ctx context.Context, input types. if errors.Is(err, coredata.ErrResourceNotFound) { return nil, gqlutils.NotFound(ctx, err) } + if errNotDeletable, ok := errors.AsType[*probo.ErrDocumentDraftNotDeletable](err); ok { return nil, gqlutils.Conflict(ctx, errNotDeletable) } + if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok { return nil, gqlutils.Conflict(ctx, errArchived) } + r.logger.ErrorCtx(ctx, "cannot delete document draft", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -933,7 +975,9 @@ func (r *mutationResolver) ArchiveDocument(ctx context.Context, input types.Arch if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok { return nil, gqlutils.Conflict(ctx, errArchived) } + r.logger.ErrorCtx(ctx, "cannot archive document", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -955,7 +999,9 @@ func (r *mutationResolver) UnarchiveDocument(ctx context.Context, input types.Un if errNotArchived, ok := errors.AsType[*probo.ErrDocumentNotArchived](err); ok { return nil, gqlutils.Conflict(ctx, errNotArchived) } + r.logger.ErrorCtx(ctx, "cannot unarchive document", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -989,6 +1035,7 @@ func (r *mutationResolver) PublishDocument(ctx context.Context, input types.Publ if !input.Minor && len(input.ApproverIds) > 0 { action = probo.ActionDocumentVersionRequestApproval } + if err := r.authorize(ctx, input.DocumentID, action); err != nil { return nil, err } @@ -1027,6 +1074,7 @@ func (r *mutationResolver) PublishDocument(ctx context.Context, input types.Publ } r.logger.ErrorCtx(ctx, "cannot publish document", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -1037,6 +1085,7 @@ func (r *mutationResolver) PublishDocument(ctx context.Context, input types.Publ if result.Quorum != nil { payload.ApprovalQuorum = types.NewDocumentVersionApprovalQuorum(result.Quorum) } + return payload, nil } @@ -1076,6 +1125,7 @@ func (r *mutationResolver) BulkPublishDocuments(ctx context.Context, input types } r.logger.ErrorCtx(ctx, "cannot bulk publish documents", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -1118,6 +1168,7 @@ func (r *mutationResolver) VoidDocumentVersionApproval(ctx context.Context, inpu } r.logger.ErrorCtx(ctx, "cannot void document version approval", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -1256,6 +1307,7 @@ func (r *mutationResolver) GenerateDocumentChangelog(ctx context.Context, input } r.logger.ErrorCtx(ctx, "cannot generate document changelog", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -1297,6 +1349,7 @@ func (r *mutationResolver) RequestSignature(ctx context.Context, input types.Req } r.logger.ErrorCtx(ctx, "cannot request signature", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -1342,6 +1395,7 @@ func (r *mutationResolver) BulkRequestSignatures(ctx context.Context, input type } r.logger.ErrorCtx(ctx, "cannot bulk request signatures", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -1384,6 +1438,7 @@ func (r *mutationResolver) CancelSignatureRequest(ctx context.Context, input typ } r.logger.ErrorCtx(ctx, "cannot cancel signature request", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -1408,6 +1463,7 @@ func (r *mutationResolver) SignDocument(ctx context.Context, input types.SignDoc } r.logger.ErrorCtx(ctx, "cannot sign document", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -1459,6 +1515,7 @@ func (r *mutationResolver) ApproveDocumentVersion(ctx context.Context, input typ } r.logger.ErrorCtx(ctx, "cannot approve document version", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -1500,6 +1557,7 @@ func (r *mutationResolver) RejectDocumentVersion(ctx context.Context, input type } r.logger.ErrorCtx(ctx, "cannot reject document version", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -1567,6 +1625,7 @@ func (r *mutationResolver) ExportEmployeeDocumentVersionPDF(ctx context.Context, } r.logger.ErrorCtx(ctx, "cannot get employee document", log.Error(err)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/console/v1/evidence_resolvers.go b/pkg/server/api/console/v1/evidence_resolvers.go index 68717ca15..4280e135e 100644 --- a/pkg/server/api/console/v1/evidence_resolvers.go +++ b/pkg/server/api/console/v1/evidence_resolvers.go @@ -39,6 +39,7 @@ func (r *evidenceResolver) File(ctx context.Context, obj *types.Evidence) (*type } r.logger.ErrorCtx(ctx, "cannot load evidence file", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -65,6 +66,7 @@ func (r *evidenceResolver) Task(ctx context.Context, obj *types.Evidence) (*type } r.logger.ErrorCtx(ctx, "cannot load task", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -86,6 +88,7 @@ func (r *evidenceResolver) Measure(ctx context.Context, obj *types.Evidence) (*t } r.logger.ErrorCtx(ctx, "cannot load measure", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -112,6 +115,7 @@ func (r *evidenceConnectionResolver) TotalCount(ctx context.Context, obj *types. r.logger.ErrorCtx(ctx, "cannot count measure evidence", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil case *taskResolver: count, err := prb.Evidences.CountForTaskID(ctx, obj.ParentID) @@ -119,10 +123,12 @@ func (r *evidenceConnectionResolver) TotalCount(ctx context.Context, obj *types. r.logger.ErrorCtx(ctx, "cannot count task evidence", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver") + return 0, gqlutils.Internal(ctx) } @@ -169,7 +175,9 @@ func (r *mutationResolver) UploadMeasureEvidence(ctx context.Context, input type if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot upload measure evidence", log.Error(err)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/console/v1/framework_resolvers.go b/pkg/server/api/console/v1/framework_resolvers.go index 366951c67..0e46050af 100644 --- a/pkg/server/api/console/v1/framework_resolvers.go +++ b/pkg/server/api/console/v1/framework_resolvers.go @@ -39,6 +39,7 @@ func (r *frameworkResolver) Organization(ctx context.Context, obj *types.Framewo } r.logger.ErrorCtx(ctx, "cannot load organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -122,10 +123,12 @@ func (r *frameworkConnectionResolver) TotalCount(ctx context.Context, obj *types r.logger.ErrorCtx(ctx, "cannot count frameworks", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver") + return 0, gqlutils.Internal(ctx) } @@ -148,7 +151,9 @@ func (r *mutationResolver) CreateFramework(ctx context.Context, input types.Crea if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create framework", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -177,7 +182,9 @@ func (r *mutationResolver) UpdateFramework(ctx context.Context, input types.Upda if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update framework", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -207,6 +214,7 @@ func (r *mutationResolver) ImportFramework(ctx context.Context, input types.Impo } r.logger.ErrorCtx(ctx, "cannot import framework", log.Error(err)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/console/v1/graphql_handler.go b/pkg/server/api/console/v1/graphql_handler.go index b455f1909..a291031f7 100644 --- a/pkg/server/api/console/v1/graphql_handler.go +++ b/pkg/server/api/console/v1/graphql_handler.go @@ -64,5 +64,6 @@ func NewGraphQLHandler( es := schema.NewExecutableSchema(config) gqlh := gqlutils.NewHandler(es, logger) + return gqlh } diff --git a/pkg/server/api/console/v1/mailing_list_resolvers.go b/pkg/server/api/console/v1/mailing_list_resolvers.go index dad5d7611..fe159dec7 100644 --- a/pkg/server/api/console/v1/mailing_list_resolvers.go +++ b/pkg/server/api/console/v1/mailing_list_resolvers.go @@ -78,10 +78,12 @@ func (r *mailingListSubscriberConnectionResolver) TotalCount(ctx context.Context r.logger.ErrorCtx(ctx, "cannot count mailing list subscribers", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver for mailing list subscriber connection", log.String("resolver", fmt.Sprintf("%T", obj.Resolver))) + return 0, gqlutils.Internal(ctx) } @@ -118,7 +120,9 @@ func (r *mutationResolver) CreateMailingListUpdate(ctx context.Context, input ty if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create mailing list update", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -145,13 +149,17 @@ func (r *mutationResolver) UpdateMailingListUpdate(ctx context.Context, input ty if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + if errors.Is(err, mailman.ErrMailingListUpdateAlreadySent) { return nil, gqlutils.Conflictf(ctx, "mailing list update can only be edited when in draft") } + if errors.Is(err, mailman.ErrMailingListUpdateNotFound) { return nil, gqlutils.NotFoundf(ctx, "mailing list update not found") } + r.logger.ErrorCtx(ctx, "cannot update mailing list update", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -171,10 +179,13 @@ func (r *mutationResolver) SendMailingListUpdate(ctx context.Context, input type if errors.Is(err, mailman.ErrMailingListUpdateAlreadySent) { return nil, gqlutils.Conflictf(ctx, "mailing list update has already been queued for sending") } + if errors.Is(err, mailman.ErrMailingListUpdateNotFound) { return nil, gqlutils.NotFoundf(ctx, "mailing list update not found") } + r.logger.ErrorCtx(ctx, "cannot queue mailing list update for sending", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -193,7 +204,9 @@ func (r *mutationResolver) DeleteMailingListUpdate(ctx context.Context, input ty if errors.Is(err, mailman.ErrMailingListUpdateNotFound) { return nil, gqlutils.NotFoundf(ctx, "mailing list update not found") } + r.logger.ErrorCtx(ctx, "cannot delete mailing list update", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -238,10 +251,13 @@ func (r *mutationResolver) CreateMailingListSubscriber(ctx context.Context, inpu if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + if errors.Is(err, mailman.ErrSubscriberAlreadyExist) { return nil, gqlutils.Conflictf(ctx, "subscriber already exists in this mailing list") } + r.logger.ErrorCtx(ctx, "cannot create mailing list subscriber", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -260,7 +276,9 @@ func (r *mutationResolver) DeleteMailingListSubscriber(ctx context.Context, inpu if errors.Is(err, mailman.ErrSubscriberNotFound) { return nil, gqlutils.NotFoundf(ctx, "mailing list subscriber not found") } + r.logger.ErrorCtx(ctx, "cannot delete mailing list subscriber", log.Error(err)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/console/v1/measure_resolvers.go b/pkg/server/api/console/v1/measure_resolvers.go index e0137418c..61d09f434 100644 --- a/pkg/server/api/console/v1/measure_resolvers.go +++ b/pkg/server/api/console/v1/measure_resolvers.go @@ -208,6 +208,7 @@ func (r *measureConnectionResolver) TotalCount(ctx context.Context, obj *types.M r.logger.ErrorCtx(ctx, "cannot count measures", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil case *controlResolver: count, err := prb.Measures.CountForControlID(ctx, obj.ParentID, obj.Filters) @@ -215,6 +216,7 @@ func (r *measureConnectionResolver) TotalCount(ctx context.Context, obj *types.M r.logger.ErrorCtx(ctx, "cannot count measures", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil case *riskResolver: count, err := prb.Measures.CountForRiskID(ctx, obj.ParentID, obj.Filters) @@ -222,10 +224,12 @@ func (r *measureConnectionResolver) TotalCount(ctx context.Context, obj *types.M r.logger.ErrorCtx(ctx, "cannot count measures", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver") + return 0, gqlutils.Internal(ctx) } @@ -254,7 +258,9 @@ func (r *mutationResolver) CreateMeasure(ctx context.Context, input types.Create if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create measure", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -285,7 +291,9 @@ func (r *mutationResolver) UpdateMeasure(ctx context.Context, input types.Update if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update measure", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -358,6 +366,7 @@ func (r *mutationResolver) CreateMeasureDocumentMapping(ctx context.Context, inp } r.logger.ErrorCtx(ctx, "cannot create measure document mapping", log.Error(err)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/console/v1/obligation_resolvers.go b/pkg/server/api/console/v1/obligation_resolvers.go index c501ba9cc..a93fd84d3 100644 --- a/pkg/server/api/console/v1/obligation_resolvers.go +++ b/pkg/server/api/console/v1/obligation_resolvers.go @@ -48,7 +48,9 @@ func (r *mutationResolver) CreateObligation(ctx context.Context, input types.Cre if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create obligation", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -84,7 +86,9 @@ func (r *mutationResolver) UpdateObligation(ctx context.Context, input types.Upd if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update obligation", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -125,10 +129,13 @@ func (r *mutationResolver) PublishObligationList(ctx context.Context, input type if errors.Is(err, coredata.ErrResourceAlreadyExists) { return nil, gqlutils.Conflict(ctx, err) } + if errMinor, ok := errors.AsType[*probo.ErrCannotPublishMinorWithoutMajor](err); ok { return nil, gqlutils.Invalid(ctx, errMinor) } + r.logger.ErrorCtx(ctx, "cannot publish obligation list", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -153,6 +160,7 @@ func (r *obligationResolver) Organization(ctx context.Context, obj *types.Obliga } r.logger.ErrorCtx(ctx, "cannot get obligation organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -174,6 +182,7 @@ func (r *obligationResolver) Owner(ctx context.Context, obj *types.Obligation) ( } r.logger.ErrorCtx(ctx, "cannot get obligation owner", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -200,6 +209,7 @@ func (r *obligationConnectionResolver) TotalCount(ctx context.Context, obj *type r.logger.ErrorCtx(ctx, "cannot count obligations", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil case *riskResolver: count, err := prb.Obligations.CountForRiskID(ctx, obj.ParentID) @@ -207,10 +217,12 @@ func (r *obligationConnectionResolver) TotalCount(ctx context.Context, obj *type r.logger.ErrorCtx(ctx, "cannot count risk obligations", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver") + return 0, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/console/v1/organization_resolvers.go b/pkg/server/api/console/v1/organization_resolvers.go index 10eb03f1b..bbd0ea030 100644 --- a/pkg/server/api/console/v1/organization_resolvers.go +++ b/pkg/server/api/console/v1/organization_resolvers.go @@ -48,7 +48,9 @@ func (r *mutationResolver) UpdateOrganizationContext(ctx context.Context, input if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update organization context", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -236,6 +238,7 @@ func (r *organizationResolver) AssetListDocument(ctx context.Context, obj *types if err != nil { return nil, fmt.Errorf("cannot get asset list document ID: %w", err) } + if assetDocumentID == nil { return nil, nil } @@ -290,6 +293,7 @@ func (r *organizationResolver) DataListDocument(ctx context.Context, obj *types. if err != nil { return nil, fmt.Errorf("cannot get data export document ID: %w", err) } + if dataDocumentID == nil { return nil, nil } @@ -374,6 +378,7 @@ func (r *organizationResolver) FindingsDocument(ctx context.Context, obj *types. if err != nil { return nil, fmt.Errorf("cannot get finding list document ID: %w", err) } + if findingDocumentID == nil { return nil, nil } @@ -452,16 +457,20 @@ func (r *organizationResolver) AuditLogEntries(ctx context.Context, obj *types.O cursor := types.NewCursor(first, after, last, before, pageOrderBy) coredataFilter := coredata.NewAuditLogEntryFilter() + if filter != nil { if filter.Action != nil { coredataFilter.WithAction(*filter.Action) } + if filter.ActorID != nil { coredataFilter.WithActorID(*filter.ActorID) } + if filter.ResourceType != nil { coredataFilter.WithResourceType(*filter.ResourceType) } + if filter.ResourceID != nil { coredataFilter.WithResourceID(*filter.ResourceID) } @@ -533,6 +542,7 @@ func (r *organizationResolver) Connectors(ctx context.Context, obj *types.Organi filtered = append(filtered, cnnctr) } } + connectors = filtered } @@ -546,12 +556,15 @@ func (r *organizationResolver) ConnectorProviderInfos(ctx context.Context, obj * } var infos []*types.ConnectorProviderInfo + for _, provider := range coredata.ConnectorProviders() { _, oauthErr := r.connectorRegistry.Get(string(provider)) + scopes := drivers.ProviderOAuth2Scopes(provider) if scopes == nil { scopes = []string{} } + info := &types.ConnectorProviderInfo{ Provider: provider, DisplayName: providerDisplayName(provider), @@ -563,6 +576,7 @@ func (r *organizationResolver) ConnectorProviderInfos(ctx context.Context, obj * } infos = append(infos, info) } + return infos, nil } @@ -675,6 +689,7 @@ func (r *organizationResolver) DataProtectionImpactAssessmentsDocument(ctx conte r.logger.ErrorCtx(ctx, "cannot get DPIA list document ID", log.Error(err)) return nil, gqlutils.Internal(ctx) } + if documentID == nil { return nil, nil } @@ -684,7 +699,9 @@ func (r *organizationResolver) DataProtectionImpactAssessmentsDocument(ctx conte if errors.Is(err, coredata.ErrResourceNotFound) { return nil, nil } + r.logger.ErrorCtx(ctx, "cannot load DPIA list document", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -735,6 +752,7 @@ func (r *organizationResolver) TransferImpactAssessmentsDocument(ctx context.Con r.logger.ErrorCtx(ctx, "cannot get TIA list document ID", log.Error(err)) return nil, gqlutils.Internal(ctx) } + if documentID == nil { return nil, nil } @@ -744,7 +762,9 @@ func (r *organizationResolver) TransferImpactAssessmentsDocument(ctx context.Con if errors.Is(err, coredata.ErrResourceNotFound) { return nil, nil } + r.logger.ErrorCtx(ctx, "cannot load TIA list document", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -872,6 +892,7 @@ func (r *organizationResolver) ObligationsDocument(ctx context.Context, obj *typ if err != nil { return nil, fmt.Errorf("cannot get obligation list document ID: %w", err) } + if obligationDocumentID == nil { return nil, nil } @@ -959,6 +980,7 @@ func (r *organizationResolver) ProcessingActivitiesDocument(ctx context.Context, r.logger.ErrorCtx(ctx, "cannot get processing activities document ID", log.Error(err)) return nil, gqlutils.Internal(ctx) } + if documentID == nil { return nil, nil } @@ -968,7 +990,9 @@ func (r *organizationResolver) ProcessingActivitiesDocument(ctx context.Context, if errors.Is(err, coredata.ErrResourceNotFound) { return nil, nil } + r.logger.ErrorCtx(ctx, "cannot load processing activities document", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -1054,6 +1078,7 @@ func (r *organizationResolver) RisksDocument(ctx context.Context, obj *types.Org r.logger.ErrorCtx(ctx, "cannot get risks document ID", log.Error(err)) return nil, gqlutils.Internal(ctx) } + if documentID == nil { return nil, nil } @@ -1063,7 +1088,9 @@ func (r *organizationResolver) RisksDocument(ctx context.Context, obj *types.Org if errors.Is(err, coredata.ErrResourceNotFound) { return nil, nil } + r.logger.ErrorCtx(ctx, "cannot load risks document", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -1313,6 +1340,7 @@ func (r *organizationResolver) ThirdPartiesDocument(ctx context.Context, obj *ty r.logger.ErrorCtx(ctx, "cannot get thirdParties document ID", log.Error(err)) return nil, gqlutils.Internal(ctx) } + if documentID == nil { return nil, nil } @@ -1322,7 +1350,9 @@ func (r *organizationResolver) ThirdPartiesDocument(ctx context.Context, obj *ty if errors.Is(err, coredata.ErrResourceNotFound) { return nil, nil } + r.logger.ErrorCtx(ctx, "cannot load thirdParties document", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -1382,18 +1412,22 @@ func (r *profileConnectionResolver) TotalCount(ctx context.Context, obj *types.P r.logger.ErrorCtx(ctx, "cannot count profiles", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil case *documentVersionResolver: prb := r.ProboService(ctx, obj.ParentID.TenantID()) + count, err := prb.Documents.CountVersionApprovers(ctx, obj.ParentID) if err != nil { r.logger.ErrorCtx(ctx, "cannot count document version approvers", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver for profile connection", log.String("resolver", fmt.Sprintf("%T", obj.Resolver))) + return 0, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/console/v1/processing_activity_resolvers.go b/pkg/server/api/console/v1/processing_activity_resolvers.go index ea1024f98..f875ba81a 100644 --- a/pkg/server/api/console/v1/processing_activity_resolvers.go +++ b/pkg/server/api/console/v1/processing_activity_resolvers.go @@ -137,10 +137,13 @@ func (r *mutationResolver) PublishProcessingActivityList(ctx context.Context, in if errors.Is(err, coredata.ErrResourceAlreadyExists) { return nil, gqlutils.Conflict(ctx, err) } + if errMinor, ok := errors.AsType[*probo.ErrCannotPublishMinorWithoutMajor](err); ok { return nil, gqlutils.Invalid(ctx, errMinor) } + r.logger.ErrorCtx(ctx, "cannot publish processing activity list", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -165,6 +168,7 @@ func (r *processingActivityResolver) Organization(ctx context.Context, obj *type } r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -190,6 +194,7 @@ func (r *processingActivityResolver) DataProtectionOfficer(ctx context.Context, } r.logger.ErrorCtx(ctx, "cannot get data protection officer", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -239,7 +244,9 @@ func (r *processingActivityResolver) DataProtectionImpactAssessment(ctx context. if errors.Is(err, coredata.ErrResourceNotFound) { return nil, nil } + r.logger.ErrorCtx(ctx, "cannot get processing activity dpia", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -259,7 +266,9 @@ func (r *processingActivityResolver) TransferImpactAssessment(ctx context.Contex if errors.Is(err, coredata.ErrResourceNotFound) { return nil, nil } + r.logger.ErrorCtx(ctx, "cannot get processing activity tia", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -286,10 +295,12 @@ func (r *processingActivityConnectionResolver) TotalCount(ctx context.Context, o r.logger.ErrorCtx(ctx, "cannot count organization processing activities", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver") + return 0, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/console/v1/provider_organizations.go b/pkg/server/api/console/v1/provider_organizations.go index 04e6b562a..e6cf831ed 100644 --- a/pkg/server/api/console/v1/provider_organizations.go +++ b/pkg/server/api/console/v1/provider_organizations.go @@ -33,12 +33,14 @@ func probeConnection(ctx context.Context, httpClient *http.Client, probeURL stri if err != nil { return fmt.Errorf("cannot create probe request: %w", err) } + req.Header.Set("Accept", "application/json") resp, err := httpClient.Do(req) if err != nil { return fmt.Errorf("probe request failed: %w", err) } + defer func() { _, _ = io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() diff --git a/pkg/server/api/console/v1/resolver.go b/pkg/server/api/console/v1/resolver.go index a84b4227b..7f7cbab9d 100644 --- a/pkg/server/api/console/v1/resolver.go +++ b/pkg/server/api/console/v1/resolver.go @@ -162,6 +162,7 @@ func handleConnectorComplete( if err != nil { logger.ErrorCtx(r.Context(), "cannot complete connector", log.Error(err)) httpserver.RenderError(w, http.StatusInternalServerError, fmt.Errorf("internal error")) + return } @@ -196,6 +197,7 @@ func handleConnectorComplete( if err != nil { logger.ErrorCtx(r.Context(), "cannot reconnect connector", log.Error(err)) httpserver.RenderError(w, http.StatusInternalServerError, fmt.Errorf("internal error")) + return } } else { @@ -218,6 +220,7 @@ func handleConnectorComplete( // token response body. subdomain = state.ProviderMetadata["subdomain"] } + // The subdomain comes from an attacker-influenceable // callback parameter; refuse anything that isn't a valid // DNS label so it cannot be smuggled into URLs or logs. @@ -225,8 +228,10 @@ func handleConnectorComplete( logger.WarnCtx(r.Context(), "rejecting invalid pagerduty subdomain", log.String("provider", string(connectorProvider)), ) + subdomain = "" } + if subdomain != "" { createReq.PagerDutySettings = &coredata.PagerDutyConnectorSettings{ Subdomain: subdomain, @@ -250,6 +255,7 @@ func handleConnectorComplete( } } } + if teamID != "" { createReq.VercelSettings = &coredata.VercelConnectorSettings{ TeamID: teamID, @@ -261,6 +267,7 @@ func handleConnectorComplete( if err != nil { logger.ErrorCtx(r.Context(), "cannot create connector", log.Error(err)) httpserver.RenderError(w, http.StatusInternalServerError, fmt.Errorf("internal error")) + return } } @@ -273,8 +280,10 @@ func handleConnectorComplete( parsedURL, err := url.Parse(redirectURL) if err != nil { logger.ErrorCtx(r.Context(), "cannot parse redirect URL", log.Error(err)) + parsedURL, _ = url.Parse(baseURL.WithPath("/organizations/" + organizationID.String()).MustString()) } + q := parsedURL.Query() q.Set("connector_id", cnnctr.ID.String()) q.Set("provider", string(connectorProvider)) @@ -296,11 +305,13 @@ func handleConnectorOAuth2Error( provider := "unknown" redirectURL := baseURL.String() + if stateToken := query.Get("state"); stateToken != "" { if payload, err := connector.DecodeOAuth2StatePayload(stateToken); err == nil { if payload.Data.Provider != "" { provider = payload.Data.Provider } + if payload.Data.ContinueURL != "" { redirectURL = payload.Data.ContinueURL } @@ -331,6 +342,7 @@ func isValidPagerDutySubdomain(s string) bool { if s == "" || len(s) > 63 { return false } + for _, c := range s { switch { case c >= 'a' && c <= 'z': @@ -341,6 +353,7 @@ func isValidPagerDutySubdomain(s string) bool { return false } } + return true } diff --git a/pkg/server/api/console/v1/rights_request_resolvers.go b/pkg/server/api/console/v1/rights_request_resolvers.go index 928d29101..26c80750a 100644 --- a/pkg/server/api/console/v1/rights_request_resolvers.go +++ b/pkg/server/api/console/v1/rights_request_resolvers.go @@ -43,7 +43,9 @@ func (r *mutationResolver) CreateRightsRequest(ctx context.Context, input types. if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create rights request", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -76,7 +78,9 @@ func (r *mutationResolver) UpdateRightsRequest(ctx context.Context, input types. if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update rights request", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -123,7 +127,9 @@ func (r *rightsRequestResolver) Organization(ctx context.Context, obj *types.Rig if errors.Is(err, coredata.ErrResourceNotFound) { return nil, gqlutils.NotFound(ctx, err) } + r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/console/v1/risk_resolvers.go b/pkg/server/api/console/v1/risk_resolvers.go index c41f3078a..fb33defb5 100644 --- a/pkg/server/api/console/v1/risk_resolvers.go +++ b/pkg/server/api/console/v1/risk_resolvers.go @@ -54,7 +54,9 @@ func (r *mutationResolver) CreateRisk(ctx context.Context, input types.CreateRis if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create risk", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -91,7 +93,9 @@ func (r *mutationResolver) UpdateRisk(ctx context.Context, input types.UpdateRis if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update risk", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -252,10 +256,13 @@ func (r *mutationResolver) PublishRiskList(ctx context.Context, input types.Publ if errors.Is(err, coredata.ErrResourceAlreadyExists) { return nil, gqlutils.Conflict(ctx, err) } + if errMinor, ok := errors.AsType[*probo.ErrCannotPublishMinorWithoutMajor](err); ok { return nil, gqlutils.Invalid(ctx, errMinor) } + r.logger.ErrorCtx(ctx, "cannot publish risk list", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -284,6 +291,7 @@ func (r *riskResolver) Owner(ctx context.Context, obj *types.Risk) (*types.Profi } r.logger.ErrorCtx(ctx, "cannot get owner", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -305,6 +313,7 @@ func (r *riskResolver) Organization(ctx context.Context, obj *types.Risk) (*type } r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -404,6 +413,7 @@ func (r *riskResolver) Controls(ctx context.Context, obj *types.Risk, first *int } cursor := types.NewCursor(first, after, last, before, pageOrderBy) + var filters = coredata.NewControlFilter(nil) if filter != nil { filters = coredata.NewControlFilter(filter.Query) @@ -490,6 +500,7 @@ func (r *riskConnectionResolver) TotalCount(ctx context.Context, obj *types.Risk r.logger.ErrorCtx(ctx, "cannot count risks", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil case *organizationResolver: count, err := prb.Risks.CountForOrganizationID(ctx, obj.ParentID, obj.Filters) @@ -497,6 +508,7 @@ func (r *riskConnectionResolver) TotalCount(ctx context.Context, obj *types.Risk r.logger.ErrorCtx(ctx, "cannot count risks", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil case *riskAssessmentScenarioResolver: scope := coredata.NewScopeFromObjectID(obj.ParentID) @@ -509,6 +521,7 @@ func (r *riskConnectionResolver) TotalCount(ctx context.Context, obj *types.Risk } r.logger.ErrorCtx(ctx, "unsupported resolver") + return 0, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/console/v1/task_resolvers.go b/pkg/server/api/console/v1/task_resolvers.go index 6c4e53c97..dc150c5b4 100644 --- a/pkg/server/api/console/v1/task_resolvers.go +++ b/pkg/server/api/console/v1/task_resolvers.go @@ -51,7 +51,9 @@ func (r *mutationResolver) CreateTask(ctx context.Context, input types.CreateTas if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create task", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -87,7 +89,9 @@ func (r *mutationResolver) UpdateTask(ctx context.Context, input types.UpdateTas if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update task", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -134,6 +138,7 @@ func (r *taskResolver) AssignedTo(ctx context.Context, obj *types.Task) (*types. } r.logger.ErrorCtx(ctx, "cannot get assigned to", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -155,6 +160,7 @@ func (r *taskResolver) Organization(ctx context.Context, obj *types.Task) (*type } r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -180,6 +186,7 @@ func (r *taskResolver) Measure(ctx context.Context, obj *types.Task) (*types.Mea } r.logger.ErrorCtx(ctx, "cannot get measure", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -206,6 +213,7 @@ func (r *taskResolver) Evidences(ctx context.Context, obj *types.Task, first *in } cursor := types.NewCursor(first, after, last, before, pageOrderBy) + page, err := prb.Evidences.ListForTaskID(ctx, obj.ID, cursor) if err != nil { r.logger.ErrorCtx(ctx, "cannot list task evidences", log.Error(err)) @@ -235,6 +243,7 @@ func (r *taskConnectionResolver) TotalCount(ctx context.Context, obj *types.Task r.logger.ErrorCtx(ctx, "cannot count tasks", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil case *organizationResolver: count, err := prb.Tasks.CountForOrganizationID(ctx, obj.ParentID) @@ -242,10 +251,12 @@ func (r *taskConnectionResolver) TotalCount(ctx context.Context, obj *types.Task r.logger.ErrorCtx(ctx, "cannot count tasks", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver") + return 0, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/console/v1/third_party_resolvers.go b/pkg/server/api/console/v1/third_party_resolvers.go index fcaa1ddb3..cf760aa78 100644 --- a/pkg/server/api/console/v1/third_party_resolvers.go +++ b/pkg/server/api/console/v1/third_party_resolvers.go @@ -66,9 +66,12 @@ func (r *mutationResolver) CreateThirdParty(ctx context.Context, input types.Cre if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create thirdParty", log.Error(err)) + return nil, gqlutils.Internal(ctx) } + return &types.CreateThirdPartyPayload{ ThirdPartyEdge: types.NewThirdPartyEdge(thirdParty, coredata.ThirdPartyOrderFieldName), }, nil @@ -112,7 +115,9 @@ func (r *mutationResolver) UpdateThirdParty(ctx context.Context, input types.Upd if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update thirdParty", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -161,7 +166,9 @@ func (r *mutationResolver) CreateThirdPartyContact(ctx context.Context, input ty if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create thirdParty contact", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -191,7 +198,9 @@ func (r *mutationResolver) UpdateThirdPartyContact(ctx context.Context, input ty if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update thirdParty contact", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -238,7 +247,9 @@ func (r *mutationResolver) CreateThirdPartyService(ctx context.Context, input ty if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create thirdParty service", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -266,7 +277,9 @@ func (r *mutationResolver) UpdateThirdPartyService(ctx context.Context, input ty if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update thirdParty service", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -316,7 +329,9 @@ func (r *mutationResolver) UploadThirdPartyComplianceReport(ctx context.Context, if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot upload thirdParty compliance report", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -366,7 +381,9 @@ func (r *mutationResolver) UploadThirdPartyBusinessAssociateAgreement(ctx contex if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot upload thirdParty business associate agreement", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -395,7 +412,9 @@ func (r *mutationResolver) UpdateThirdPartyBusinessAssociateAgreement(ctx contex if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update thirdParty business associate agreement", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -445,7 +464,9 @@ func (r *mutationResolver) UploadThirdPartyDataPrivacyAgreement(ctx context.Cont if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot upload thirdParty data privacy agreement", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -474,7 +495,9 @@ func (r *mutationResolver) UpdateThirdPartyDataPrivacyAgreement(ctx context.Cont if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update thirdParty data privacy agreement", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -524,7 +547,9 @@ func (r *mutationResolver) CreateThirdPartyRiskAssessment(ctx context.Context, i if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create thirdParty risk assessment", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -555,6 +580,7 @@ func (r *mutationResolver) AssessThirdParty(ctx context.Context, input types.Ass } r.logger.ErrorCtx(ctx, "cannot assess thirdParty", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -578,10 +604,13 @@ func (r *mutationResolver) PublishThirdPartyList(ctx context.Context, input type if errors.Is(err, coredata.ErrResourceAlreadyExists) { return nil, gqlutils.Conflict(ctx, err) } + if errMinor, ok := errors.AsType[*probo.ErrCannotPublishMinorWithoutMajor](err); ok { return nil, gqlutils.Invalid(ctx, errMinor) } + r.logger.ErrorCtx(ctx, "cannot publish thirdParty list", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -606,6 +635,7 @@ func (r *thirdPartyResolver) Organization(ctx context.Context, obj *types.ThirdP } r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -657,6 +687,7 @@ func (r *thirdPartyResolver) BusinessAssociateAgreement(ctx context.Context, obj } r.logger.ErrorCtx(ctx, "cannot get thirdParty business associate agreement", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -678,6 +709,7 @@ func (r *thirdPartyResolver) DataPrivacyAgreement(ctx context.Context, obj *type } r.logger.ErrorCtx(ctx, "cannot get thirdParty data privacy agreement", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -793,6 +825,7 @@ func (r *thirdPartyResolver) BusinessOwner(ctx context.Context, obj *types.Third } r.logger.ErrorCtx(ctx, "cannot get business owner", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -818,6 +851,7 @@ func (r *thirdPartyResolver) SecurityOwner(ctx context.Context, obj *types.Third } r.logger.ErrorCtx(ctx, "cannot get security owner", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -886,6 +920,7 @@ func (r *thirdPartyComplianceReportResolver) ThirdParty(ctx context.Context, obj } r.logger.ErrorCtx(ctx, "cannot get thirdParty", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -917,6 +952,7 @@ func (r *thirdPartyComplianceReportResolver) File(ctx context.Context, obj *type } r.logger.ErrorCtx(ctx, "cannot load evidence file", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -943,6 +979,7 @@ func (r *thirdPartyConnectionResolver) TotalCount(ctx context.Context, obj *type r.logger.ErrorCtx(ctx, "cannot count thirdParties", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil case *assetResolver: count, err := prb.ThirdParties.CountForAssetID(ctx, obj.ParentID) @@ -950,6 +987,7 @@ func (r *thirdPartyConnectionResolver) TotalCount(ctx context.Context, obj *type r.logger.ErrorCtx(ctx, "cannot count thirdParties", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil case *datumResolver: count, err := prb.ThirdParties.CountForDatumID(ctx, obj.ParentID) @@ -957,10 +995,12 @@ func (r *thirdPartyConnectionResolver) TotalCount(ctx context.Context, obj *type r.logger.ErrorCtx(ctx, "cannot count thirdParties", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver") + return 0, gqlutils.Internal(ctx) } @@ -986,6 +1026,7 @@ func (r *thirdPartyContactResolver) ThirdParty(ctx context.Context, obj *types.T } r.logger.ErrorCtx(ctx, "cannot get thirdParty", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -1012,6 +1053,7 @@ func (r *thirdPartyDataPrivacyAgreementResolver) ThirdParty(ctx context.Context, } r.logger.ErrorCtx(ctx, "cannot get thirdParty", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -1055,6 +1097,7 @@ func (r *thirdPartyRiskAssessmentResolver) ThirdParty(ctx context.Context, obj * } r.logger.ErrorCtx(ctx, "cannot get thirdParty", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -1081,6 +1124,7 @@ func (r *thirdPartyServiceResolver) ThirdParty(ctx context.Context, obj *types.T } r.logger.ErrorCtx(ctx, "cannot get thirdParty", log.Error(err)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/console/v1/trust_center_resolvers.go b/pkg/server/api/console/v1/trust_center_resolvers.go index 85c50a56a..0bc4d69a7 100644 --- a/pkg/server/api/console/v1/trust_center_resolvers.go +++ b/pkg/server/api/console/v1/trust_center_resolvers.go @@ -44,6 +44,7 @@ func (r *complianceFrameworkResolver) Framework(ctx context.Context, obj *types. } r.logger.ErrorCtx(ctx, "cannot load framework", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -75,7 +76,9 @@ func (r *mutationResolver) UpdateTrustCenter(ctx context.Context, input types.Up if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update trust center", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -104,7 +107,9 @@ func (r *mutationResolver) UploadTrustCenterNda(ctx context.Context, input types if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot upload trust center NDA", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -148,6 +153,7 @@ func (r *mutationResolver) UpdateTrustCenterBrand(ctx context.Context, input typ logoFile := input.LogoFile.Value() if logoFile == nil { var nilFile *probo.FileUpload + req.LogoFile = &nilFile } else { fileUpload := &probo.FileUpload{ @@ -164,6 +170,7 @@ func (r *mutationResolver) UpdateTrustCenterBrand(ctx context.Context, input typ darkLogoFile := input.DarkLogoFile.Value() if darkLogoFile == nil { var nilFile *probo.FileUpload + req.DarkLogoFile = &nilFile } else { fileUpload := &probo.FileUpload{ @@ -181,7 +188,9 @@ func (r *mutationResolver) UpdateTrustCenterBrand(ctx context.Context, input typ if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update trust center brand", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -198,27 +207,33 @@ func (r *mutationResolver) UpdateTrustCenterAccess(ctx context.Context, input ty prb := r.ProboService(ctx, input.ID.TenantID()) - var documentAccesses []probo.UpdateTrustCenterDocumentAccessRequest - var reportAccesses []probo.UpdateTrustCenterDocumentAccessRequest - var fileAccesses []probo.UpdateTrustCenterDocumentAccessRequest + var ( + documentAccesses []probo.UpdateTrustCenterDocumentAccessRequest + reportAccesses []probo.UpdateTrustCenterDocumentAccessRequest + fileAccesses []probo.UpdateTrustCenterDocumentAccessRequest + ) + for _, documentAccess := range input.Documents { documentAccesses = append(documentAccesses, probo.UpdateTrustCenterDocumentAccessRequest{ ID: documentAccess.ID, Status: documentAccess.Status, }) } + for _, reportAccess := range input.Reports { reportAccesses = append(reportAccesses, probo.UpdateTrustCenterDocumentAccessRequest{ ID: reportAccess.ID, Status: reportAccess.Status, }) } + for _, fileAccess := range input.TrustCenterFiles { fileAccesses = append(fileAccesses, probo.UpdateTrustCenterDocumentAccessRequest{ ID: fileAccess.ID, Status: fileAccess.Status, }) } + access, err := prb.TrustCenterAccesses.Update( ctx, &probo.UpdateTrustCenterAccessRequest{ @@ -232,7 +247,9 @@ func (r *mutationResolver) UpdateTrustCenterAccess(ctx context.Context, input ty if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update trust center access", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -287,7 +304,9 @@ func (r *mutationResolver) CreateTrustCenterReference(ctx context.Context, input if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create trust center reference", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -326,7 +345,9 @@ func (r *mutationResolver) UpdateTrustCenterReference(ctx context.Context, input if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update trust center reference", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -373,7 +394,9 @@ func (r *mutationResolver) CreateComplianceFramework(ctx context.Context, input if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create compliance framework", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -398,7 +421,9 @@ func (r *mutationResolver) UpdateComplianceFramework(ctx context.Context, input if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update compliance framework", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -425,7 +450,9 @@ func (r *mutationResolver) DeleteComplianceFramework(ctx context.Context, input if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot delete compliance framework", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -454,7 +481,9 @@ func (r *mutationResolver) CreateComplianceExternalURL(ctx context.Context, inpu if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create compliance external URL", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -481,7 +510,9 @@ func (r *mutationResolver) UpdateComplianceExternalURL(ctx context.Context, inpu if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update compliance external URL", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -502,7 +533,9 @@ func (r *mutationResolver) DeleteComplianceExternalURL(ctx context.Context, inpu if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot delete compliance external URL", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -538,7 +571,9 @@ func (r *mutationResolver) CreateTrustCenterFile(ctx context.Context, input type if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create trust center file", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -568,7 +603,9 @@ func (r *mutationResolver) UpdateTrustCenterFile(ctx context.Context, input type if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update trust center file", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -634,7 +671,9 @@ func (r *mutationResolver) CreateCustomDomain(ctx context.Context, input types.C if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create custom domain", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -753,6 +792,7 @@ func (r *trustCenterResolver) Organization(ctx context.Context, obj *types.Trust } r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -956,6 +996,7 @@ func (r *trustCenterAccessResolver) ActiveCount(ctx context.Context, obj *types. if err := r.authorize(ctx, obj.ID, probo.ActionTrustCenterAccessGet); err != nil { return 0, err } + prb := r.ProboService(ctx, obj.ID.TenantID()) count, err := prb.TrustCenterAccesses.CountActiveDocumentAccesses(ctx, obj.ID) @@ -980,6 +1021,7 @@ func (r *trustCenterAccessResolver) Profile(ctx context.Context, obj *types.Trus } r.logger.ErrorCtx(ctx, "cannot get profile", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -1040,6 +1082,7 @@ func (r *trustCenterDocumentAccessResolver) Document(ctx context.Context, obj *t } r.logger.ErrorCtx(ctx, "cannot load document", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -1143,6 +1186,7 @@ func (r *trustCenterFileResolver) Organization(ctx context.Context, obj *types.T } r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -1167,6 +1211,7 @@ func (r *trustCenterFileConnectionResolver) TotalCount(ctx context.Context, obj r.logger.ErrorCtx(ctx, "cannot count trust center files", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } diff --git a/pkg/server/api/console/v1/types/access_review.go b/pkg/server/api/console/v1/types/access_review.go index 355c7cfdd..5c2857998 100644 --- a/pkg/server/api/console/v1/types/access_review.go +++ b/pkg/server/api/console/v1/types/access_review.go @@ -108,9 +108,13 @@ func NewAccessReviewCampaignScopeSource( status := coredata.AccessReviewCampaignSourceFetchStatusQueued fetchedAccountsCount := 0 attemptCount := 0 - var lastError *string - var fetchStartedAt *time.Time - var fetchCompletedAt *time.Time + + var ( + lastError *string + fetchStartedAt *time.Time + fetchCompletedAt *time.Time + ) + if fetch != nil { status = fetch.Status fetchedAccountsCount = fetch.FetchedAccountsCount diff --git a/pkg/server/api/console/v1/types/access_review_campaign_scope_source_test.go b/pkg/server/api/console/v1/types/access_review_campaign_scope_source_test.go index 31b07e0f4..c8393abdd 100644 --- a/pkg/server/api/console/v1/types/access_review_campaign_scope_source_test.go +++ b/pkg/server/api/console/v1/types/access_review_campaign_scope_source_test.go @@ -33,13 +33,16 @@ func TestNewAccessReviewCampaignScopeSource_DefaultFetchState(t *testing.T) { } campaignID := gid.New(tenantID, coredata.AccessReviewCampaignEntityType) + got := NewAccessReviewCampaignScopeSource(campaignID, source, nil) if got.FetchStatus != coredata.AccessReviewCampaignSourceFetchStatusQueued { t.Fatalf("fetch status = %q, want QUEUED", got.FetchStatus) } + if got.FetchedAccountsCount != 0 { t.Fatalf("fetched accounts count = %d, want 0", got.FetchedAccountsCount) } + if got.AttemptCount != 0 { t.Fatalf("attempt count = %d, want 0", got.AttemptCount) } @@ -66,16 +69,20 @@ func TestNewAccessReviewCampaignScopeSource_UsesFetchState(t *testing.T) { } campaignID := gid.New(tenantID, coredata.AccessReviewCampaignEntityType) + got := NewAccessReviewCampaignScopeSource(campaignID, source, fetch) if got.FetchStatus != coredata.AccessReviewCampaignSourceFetchStatusFailed { t.Fatalf("fetch status = %q, want FAILED", got.FetchStatus) } + if got.FetchedAccountsCount != 42 { t.Fatalf("fetched accounts count = %d, want 42", got.FetchedAccountsCount) } + if got.AttemptCount != 3 { t.Fatalf("attempt count = %d, want 3", got.AttemptCount) } + if got.LastError == nil || *got.LastError != errMsg { t.Fatalf("last error = %v, want %q", got.LastError, errMsg) } diff --git a/pkg/server/api/console/v1/types/compliance_external_url.go b/pkg/server/api/console/v1/types/compliance_external_url.go index 5ebd6c556..87e152f17 100644 --- a/pkg/server/api/console/v1/types/compliance_external_url.go +++ b/pkg/server/api/console/v1/types/compliance_external_url.go @@ -44,6 +44,7 @@ func NewComplianceExternalURLConnection( for i := range edges { edges[i] = NewComplianceExternalURLEdge(p.Data[i], p.Cursor.OrderBy.Field) } + return &ComplianceExternalURLConnection{ Edges: edges, PageInfo: NewPageInfo(p), diff --git a/pkg/server/api/console/v1/types/cookie_category.go b/pkg/server/api/console/v1/types/cookie_category.go index d993d1049..4b25621f3 100644 --- a/pkg/server/api/console/v1/types/cookie_category.go +++ b/pkg/server/api/console/v1/types/cookie_category.go @@ -65,6 +65,7 @@ func NewCookieCategory(c *coredata.CookieCategory) *CookieCategory { if gcmConsentTypes == nil { gcmConsentTypes = []string{} } + return &CookieCategory{ ID: c.ID, CookieBanner: &CookieBanner{ diff --git a/pkg/server/api/console/v1/types/evidence.go b/pkg/server/api/console/v1/types/evidence.go index 499313b23..ea4c8f984 100644 --- a/pkg/server/api/console/v1/types/evidence.go +++ b/pkg/server/api/console/v1/types/evidence.go @@ -62,6 +62,7 @@ func NewEvidenceEdge(e *coredata.Evidence, orderBy coredata.EvidenceOrderField) func NewEvidence(e *coredata.Evidence) *Evidence { var urlPtr *string = nil + if e.URL != "" { urlCopy := e.URL urlPtr = &urlCopy diff --git a/pkg/server/api/console/v1/types/pageinfo.go b/pkg/server/api/console/v1/types/pageinfo.go index 52593eaee..83e2009f6 100644 --- a/pkg/server/api/console/v1/types/pageinfo.go +++ b/pkg/server/api/console/v1/types/pageinfo.go @@ -21,6 +21,7 @@ import ( func NewPageInfo[T page.Paginable[O], O page.OrderField](p *page.Page[T, O]) *PageInfo { data := pageinfo.NewPageInfo(p) + return &PageInfo{ HasNextPage: data.HasNextPage, HasPreviousPage: data.HasPreviousPage, diff --git a/pkg/server/api/console/v1/types/slack_connection.go b/pkg/server/api/console/v1/types/slack_connection.go index 1edb201af..db927f8e9 100644 --- a/pkg/server/api/console/v1/types/slack_connection.go +++ b/pkg/server/api/console/v1/types/slack_connection.go @@ -51,6 +51,7 @@ func NewSlackConnection(c *coredata.Connector) *SlackConnection { if settings.Channel != "" { conn.Channel = &settings.Channel } + if settings.ChannelID != "" { conn.ChannelID = &settings.ChannelID } diff --git a/pkg/server/api/console/v1/types/third_party.go b/pkg/server/api/console/v1/types/third_party.go index 77cf85fc2..2eee7825a 100644 --- a/pkg/server/api/console/v1/types/third_party.go +++ b/pkg/server/api/console/v1/types/third_party.go @@ -113,5 +113,6 @@ func NewThirdPartySubprocessors(sps []probo.Subprocessor) []*ThirdPartySubproces Purpose: sp.Purpose, } } + return result } diff --git a/pkg/server/api/console/v1/types/tracker_pattern.go b/pkg/server/api/console/v1/types/tracker_pattern.go index 82b9e04f1..26f3bb5cd 100644 --- a/pkg/server/api/console/v1/types/tracker_pattern.go +++ b/pkg/server/api/console/v1/types/tracker_pattern.go @@ -68,6 +68,7 @@ func NewTrackerPatternConnectionWithFilter( ) *TrackerPatternConnection { conn := NewTrackerPatternConnection(p, parentType, parentID) conn.Filter = filter + return conn } diff --git a/pkg/server/api/console/v1/types/tracker_resource.go b/pkg/server/api/console/v1/types/tracker_resource.go index 78ccb249e..a807737d9 100644 --- a/pkg/server/api/console/v1/types/tracker_resource.go +++ b/pkg/server/api/console/v1/types/tracker_resource.go @@ -67,6 +67,7 @@ func NewTrackerResourceConnectionWithFilter( ) *TrackerResourceConnection { conn := NewTrackerResourceConnection(p, parentType, parentID) conn.Filter = filter + return conn } diff --git a/pkg/server/api/console/v1/types/webhook_event.go b/pkg/server/api/console/v1/types/webhook_event.go index dc4b171d0..cfc45b323 100644 --- a/pkg/server/api/console/v1/types/webhook_event.go +++ b/pkg/server/api/console/v1/types/webhook_event.go @@ -62,6 +62,7 @@ func NewWebhookEventEdge(we *coredata.WebhookEvent, orderBy coredata.WebhookEven func NewWebhookEvent(we *coredata.WebhookEvent) *WebhookEvent { var response *string + if len(we.Response) > 0 { s := string(we.Response) response = &s diff --git a/pkg/server/api/console/v1/viewer_resolvers.go b/pkg/server/api/console/v1/viewer_resolvers.go index d9623a43b..288f7c0cc 100644 --- a/pkg/server/api/console/v1/viewer_resolvers.go +++ b/pkg/server/api/console/v1/viewer_resolvers.go @@ -79,6 +79,7 @@ func (r *viewerResolver) SignableDocument(ctx context.Context, obj *types.Viewer identity := authn.IdentityFromContext(ctx) documentFilter := coredata.NewDocumentFilter(nil).WithEmployeeIdentityID(&identity.ID, coredata.EmployeeFilterModeSignature) + document, err := prb.Documents.GetWithFilter(ctx, id, documentFilter) if err != nil { if errors.Is(err, coredata.ErrResourceNotFound) { @@ -86,6 +87,7 @@ func (r *viewerResolver) SignableDocument(ctx context.Context, obj *types.Viewer } r.logger.ErrorCtx(ctx, "cannot get signable document", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -158,6 +160,7 @@ func (r *viewerResolver) ApprovableDocument(ctx context.Context, obj *types.View identity := authn.IdentityFromContext(ctx) documentFilter := coredata.NewDocumentFilter(nil).WithEmployeeIdentityID(&identity.ID, coredata.EmployeeFilterModeApproval) + document, err := prb.Documents.GetWithFilter(ctx, id, documentFilter) if err != nil { if errors.Is(err, coredata.ErrResourceNotFound) { @@ -165,6 +168,7 @@ func (r *viewerResolver) ApprovableDocument(ctx context.Context, obj *types.View } r.logger.ErrorCtx(ctx, "cannot get approvable document", log.Error(err)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/console/v1/webhook_resolvers.go b/pkg/server/api/console/v1/webhook_resolvers.go index aa2f74585..0e2261821 100644 --- a/pkg/server/api/console/v1/webhook_resolvers.go +++ b/pkg/server/api/console/v1/webhook_resolvers.go @@ -42,7 +42,9 @@ func (r *mutationResolver) CreateWebhookSubscription(ctx context.Context, input if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot create webhook subscription", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -71,7 +73,9 @@ func (r *mutationResolver) UpdateWebhookSubscription(ctx context.Context, input if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + r.logger.ErrorCtx(ctx, "cannot update webhook subscription", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -131,6 +135,7 @@ func (r *webhookSubscriptionResolver) Organization(ctx context.Context, obj *typ } r.logger.ErrorCtx(ctx, "cannot load organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -204,10 +209,12 @@ func (r *webhookSubscriptionConnectionResolver) TotalCount(ctx context.Context, r.logger.ErrorCtx(ctx, "cannot count webhook subscriptions", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } r.logger.ErrorCtx(ctx, "unsupported resolver for webhook subscription connection", log.String("resolver", fmt.Sprintf("%T", obj.Resolver))) + return 0, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/cookiebanner/v1/cors_middleware.go b/pkg/server/api/cookiebanner/v1/cors_middleware.go index 9aa070427..8b364d352 100644 --- a/pkg/server/api/cookiebanner/v1/cors_middleware.go +++ b/pkg/server/api/cookiebanner/v1/cors_middleware.go @@ -53,8 +53,10 @@ func newCORSMiddleware(logger *log.Logger, cookieBannerSvc *cookiebanner.Service jsonutil.RenderForbidden(w) return } + logger.ErrorCtx(r.Context(), "cannot load cookie banner for CORS check", log.Error(err)) jsonutil.RenderInternalServerError(w) + return } diff --git a/pkg/server/api/cookiebanner/v1/handler.go b/pkg/server/api/cookiebanner/v1/handler.go index 51bde9a22..a80028899 100644 --- a/pkg/server/api/cookiebanner/v1/handler.go +++ b/pkg/server/api/cookiebanner/v1/handler.go @@ -88,12 +88,15 @@ func (h *Handler) handleGetConfig(w http.ResponseWriter, r *http.Request) { jsonutil.RenderNotFound(w, fmt.Errorf("banner not found")) return } + if errors.Is(err, cookiebanner.ErrNoPublishedVersion) { jsonutil.RenderNotFound(w, fmt.Errorf("no published version")) return } + h.logger.ErrorCtx(r.Context(), "cannot get banner config", log.Error(err), log.String("sdk_version", sdkVersion)) jsonutil.RenderInternalServerError(w) + return } @@ -111,6 +114,7 @@ func (h *Handler) resolveCountryCode(r *http.Request) *coredata.CountryCode { log.Error(err), log.String("sdk_version", sdkVersionFromContext(r.Context())), ) + return nil } @@ -140,10 +144,12 @@ func (h *Handler) handleGetConsent(w http.ResponseWriter, r *http.Request) { jsonutil.RenderNotFound(w, fmt.Errorf("banner not found")) return } + if errors.Is(err, cookiebanner.ErrConsentNotFound) { jsonutil.RenderNotFound(w, fmt.Errorf("consent not found")) return } + h.logger.ErrorCtx( r.Context(), "cannot get visitor consent", @@ -151,6 +157,7 @@ func (h *Handler) handleGetConsent(w http.ResponseWriter, r *http.Request) { log.String("sdk_version", sdkVersionFromContext(r.Context())), ) jsonutil.RenderInternalServerError(w) + return } @@ -221,12 +228,15 @@ func (h *Handler) handlePostConsent(w http.ResponseWriter, r *http.Request) { jsonutil.RenderNotFound(w, fmt.Errorf("banner not found")) return } + if errors.Is(err, cookiebanner.ErrVersionNotFound) || errors.Is(err, cookiebanner.ErrVersionNotPublished) { jsonutil.RenderBadRequest(w, fmt.Errorf("invalid version")) return } + h.logger.ErrorCtx(r.Context(), "cannot record consent", log.Error(err), log.String("sdk_version", sdkVersion)) jsonutil.RenderInternalServerError(w) + return } @@ -266,20 +276,25 @@ func sanitizeInitiatorURL(raw *string) *string { if raw == nil { return nil } + s := strings.TrimSpace(*raw) if s == "" || len(s) > maxInitiatorURLLength { return nil } + u, err := url.Parse(s) if err != nil { return nil } + if u.Scheme != "http" && u.Scheme != "https" { return nil } + if u.Host == "" { return nil } + return &s } @@ -314,6 +329,7 @@ func (h *Handler) handleReportDetectedCookies(w http.ResponseWriter, r *http.Req } var source coredata.CookieSource + switch strings.TrimSpace(c.Source) { case "pre-existing": source = coredata.CookieSourcePreExisting @@ -356,6 +372,7 @@ func (h *Handler) handleReportDetectedCookies(w http.ResponseWriter, r *http.Req log.String("sdk_version", sdkVersionFromContext(r.Context())), ) jsonutil.RenderInternalServerError(w) + return } @@ -415,6 +432,7 @@ func (h *Handler) handleReportDetectedTrackers(w http.ResponseWriter, r *http.Re } var source coredata.CookieSource + switch strings.TrimSpace(c.Source) { case "pre-existing": source = coredata.CookieSourcePreExisting @@ -442,6 +460,7 @@ func (h *Handler) handleReportDetectedTrackers(w http.ResponseWriter, r *http.Re } var storageType coredata.TrackerType + switch strings.TrimSpace(s.StorageType) { case "local_storage": storageType = coredata.TrackerTypeLocalStorage @@ -476,6 +495,7 @@ func (h *Handler) handleReportDetectedTrackers(w http.ResponseWriter, r *http.Re } var resourceType coredata.TrackerResourceType + switch strings.TrimSpace(res.ResourceType) { case "script": resourceType = coredata.TrackerResourceTypeScript @@ -521,6 +541,7 @@ func (h *Handler) handleReportDetectedTrackers(w http.ResponseWriter, r *http.Re h.logger.ErrorCtx(r.Context(), "cannot report detected trackers", log.Error(err), log.String("sdk_version", sdkVersionFromContext(r.Context()))) jsonutil.RenderInternalServerError(w) + return } diff --git a/pkg/server/api/files/v1/handler.go b/pkg/server/api/files/v1/handler.go index c94b4790c..3d10474f5 100644 --- a/pkg/server/api/files/v1/handler.go +++ b/pkg/server/api/files/v1/handler.go @@ -69,6 +69,7 @@ func (h *Handler) handleGetPublicFile(w http.ResponseWriter, r *http.Request) { log.String("file_id", fileIDStr), ) http.Error(w, "internal server error", http.StatusInternalServerError) + return } diff --git a/pkg/server/api/mcp/mcputils/mcputils.go b/pkg/server/api/mcp/mcputils/mcputils.go index 47e6de070..1f3da2508 100644 --- a/pkg/server/api/mcp/mcputils/mcputils.go +++ b/pkg/server/api/mcp/mcputils/mcputils.go @@ -54,7 +54,6 @@ func LoggingMiddleware(logger *log.Logger) func(mcp.MethodHandler) mcp.MethodHan log.Error(err), ) } else { - logger.InfoCtx( ctx, fmt.Sprintf("mcp %q method completed", method), diff --git a/pkg/server/api/mcp/mcputils/recovery.go b/pkg/server/api/mcp/mcputils/recovery.go index 39fad2729..85c367ae1 100644 --- a/pkg/server/api/mcp/mcputils/recovery.go +++ b/pkg/server/api/mcp/mcputils/recovery.go @@ -88,5 +88,6 @@ func sanitizeError(ctx context.Context, logger *log.Logger, err error) error { } logger.ErrorCtx(ctx, "internal error in MCP tool handler", log.Error(err)) + return fmt.Errorf("internal server error") } diff --git a/pkg/server/api/mcp/v1/middleware.go b/pkg/server/api/mcp/v1/middleware.go index af8e28ace..b2b25135a 100644 --- a/pkg/server/api/mcp/v1/middleware.go +++ b/pkg/server/api/mcp/v1/middleware.go @@ -30,6 +30,7 @@ func RequireAPIKeyHandler( ) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + correlationID := r.Header.Get("X-Request-ID") if correlationID == "" { correlationID = r.Header.Get("X-Correlation-ID") @@ -43,10 +44,12 @@ func RequireAPIKeyHandler( ) apiKey := authn.APIKeyFromContext(ctx) + identity := authn.IdentityFromContext(ctx) if identity == nil { w.Header().Set("WWW-Authenticate", "Bearer") httpserver.RenderError(w, http.StatusUnauthorized, errors.New("authentication required")) + return } diff --git a/pkg/server/api/mcp/v1/schema.resolvers.go b/pkg/server/api/mcp/v1/schema.resolvers.go index e59239f1c..5ca8d353f 100644 --- a/pkg/server/api/mcp/v1/schema.resolvers.go +++ b/pkg/server/api/mcp/v1/schema.resolvers.go @@ -83,6 +83,7 @@ func (r *Resolver) AddThirdPartyTool(ctx context.Context, req *mcp.CallToolReque svc := r.ProboService(ctx, input.OrganizationID) var category *coredata.ThirdPartyCategory + if input.Category != nil { cat := coredata.ThirdPartyCategory(*input.Category) category = &cat @@ -211,6 +212,7 @@ func (r *Resolver) UpdateThirdPartyTool(ctx context.Context, req *mcp.CallToolRe } var category *coredata.ThirdPartyCategory + if input.Category != nil { cat := coredata.ThirdPartyCategory(*input.Category) category = &cat @@ -1403,14 +1405,17 @@ func (r *Resolver) ListControlsTool(ctx context.Context, req *mcp.CallToolReques controlFilter = coredata.NewControlFilter(input.Filter.Query) } - var controlPage *page.Page[*coredata.Control, coredata.ControlOrderField] - var err error + var ( + controlPage *page.Page[*coredata.Control, coredata.ControlOrderField] + err error + ) if input.Filter != nil && input.Filter.FrameworkID != nil { controlPage, err = prb.Controls.ListForFrameworkID(ctx, *input.Filter.FrameworkID, cursor, controlFilter) } else { controlPage, err = prb.Controls.ListForOrganizationID(ctx, input.OrganizationID, cursor, controlFilter) } + if err != nil { panic(fmt.Errorf("cannot list organization controls: %w", err)) } @@ -1465,6 +1470,7 @@ func (r *Resolver) UpdateControlTool(ctx context.Context, req *mcp.CallToolReque svc := r.ProboService(ctx, input.ID) var maturityLevel *coredata.ControlMaturityLevel + if input.MaturityLevel != nil { v := coredata.ControlMaturityLevel(*input.MaturityLevel) maturityLevel = &v @@ -1497,21 +1503,25 @@ func (r *Resolver) LinkControlTool(ctx context.Context, req *mcp.CallToolRequest switch input.ResourceID.EntityType() { case coredata.MeasureEntityType: r.MustAuthorize(ctx, input.ControlID, probo.ActionControlMeasureMappingCreate) + if _, _, err := svc.Controls.CreateMeasureMapping(ctx, input.ControlID, input.ResourceID); err != nil { return nil, types.LinkControlOutput{}, fmt.Errorf("failed to link control to measure: %w", err) } case coredata.DocumentEntityType: r.MustAuthorize(ctx, input.ControlID, probo.ActionControlDocumentMappingCreate) + if _, _, err := svc.Controls.CreateDocumentMapping(ctx, input.ControlID, input.ResourceID); err != nil { return nil, types.LinkControlOutput{}, fmt.Errorf("failed to link control to document: %w", err) } case coredata.AuditEntityType: r.MustAuthorize(ctx, input.ControlID, probo.ActionControlAuditMappingCreate) + if _, _, err := svc.Controls.CreateAuditMapping(ctx, input.ControlID, input.ResourceID); err != nil { return nil, types.LinkControlOutput{}, fmt.Errorf("failed to link control to audit: %w", err) } case coredata.ObligationEntityType: r.MustAuthorize(ctx, input.ControlID, probo.ActionControlObligationMappingCreate) + if _, _, err := svc.Controls.CreateObligationMapping(ctx, input.ControlID, input.ResourceID); err != nil { return nil, types.LinkControlOutput{}, fmt.Errorf("failed to link control to obligation: %w", err) } @@ -1528,21 +1538,25 @@ func (r *Resolver) UnlinkControlTool(ctx context.Context, req *mcp.CallToolReque switch input.ResourceID.EntityType() { case coredata.MeasureEntityType: r.MustAuthorize(ctx, input.ControlID, probo.ActionControlMeasureMappingDelete) + if _, _, err := svc.Controls.DeleteMeasureMapping(ctx, input.ControlID, input.ResourceID); err != nil { return nil, types.UnlinkControlOutput{}, fmt.Errorf("failed to unlink control from measure: %w", err) } case coredata.DocumentEntityType: r.MustAuthorize(ctx, input.ControlID, probo.ActionControlDocumentMappingDelete) + if _, _, err := svc.Controls.DeleteDocumentMapping(ctx, input.ControlID, input.ResourceID); err != nil { return nil, types.UnlinkControlOutput{}, fmt.Errorf("failed to unlink control from document: %w", err) } case coredata.AuditEntityType: r.MustAuthorize(ctx, input.ControlID, probo.ActionControlAuditMappingDelete) + if _, _, err := svc.Controls.DeleteAuditMapping(ctx, input.ControlID, input.ResourceID); err != nil { return nil, types.UnlinkControlOutput{}, fmt.Errorf("failed to unlink control from audit: %w", err) } case coredata.ObligationEntityType: r.MustAuthorize(ctx, input.ControlID, probo.ActionControlObligationMappingDelete) + if _, _, err := svc.Controls.DeleteObligationMapping(ctx, input.ControlID, input.ResourceID); err != nil { return nil, types.UnlinkControlOutput{}, fmt.Errorf("failed to unlink control from obligation: %w", err) } @@ -1689,16 +1703,19 @@ func (r *Resolver) LinkRiskTool(ctx context.Context, req *mcp.CallToolRequest, i switch input.ResourceID.EntityType() { case coredata.DocumentEntityType: r.MustAuthorize(ctx, input.RiskID, probo.ActionRiskDocumentMappingCreate) + if _, _, err := svc.Risks.CreateDocumentMapping(ctx, input.RiskID, input.ResourceID); err != nil { return nil, types.LinkRiskOutput{}, fmt.Errorf("failed to link risk to document: %w", err) } case coredata.MeasureEntityType: r.MustAuthorize(ctx, input.RiskID, probo.ActionRiskMeasureMappingCreate) + if _, _, err := svc.Risks.CreateMeasureMapping(ctx, input.RiskID, input.ResourceID); err != nil { return nil, types.LinkRiskOutput{}, fmt.Errorf("failed to link risk to measure: %w", err) } case coredata.ObligationEntityType: r.MustAuthorize(ctx, input.RiskID, probo.ActionRiskObligationMappingCreate) + if _, _, err := svc.Risks.CreateObligationMapping(ctx, input.RiskID, input.ResourceID); err != nil { return nil, types.LinkRiskOutput{}, fmt.Errorf("failed to link risk to obligation: %w", err) } @@ -1715,16 +1732,19 @@ func (r *Resolver) UnlinkRiskTool(ctx context.Context, req *mcp.CallToolRequest, switch input.ResourceID.EntityType() { case coredata.DocumentEntityType: r.MustAuthorize(ctx, input.RiskID, probo.ActionRiskDocumentMappingDelete) + if _, _, err := svc.Risks.DeleteDocumentMapping(ctx, input.RiskID, input.ResourceID); err != nil { return nil, types.UnlinkRiskOutput{}, fmt.Errorf("failed to unlink risk from document: %w", err) } case coredata.MeasureEntityType: r.MustAuthorize(ctx, input.RiskID, probo.ActionRiskMeasureMappingDelete) + if _, _, err := svc.Risks.DeleteMeasureMapping(ctx, input.RiskID, input.ResourceID); err != nil { return nil, types.UnlinkRiskOutput{}, fmt.Errorf("failed to unlink risk from measure: %w", err) } case coredata.ObligationEntityType: r.MustAuthorize(ctx, input.RiskID, probo.ActionRiskObligationMappingDelete) + if _, _, err := svc.Risks.DeleteObligationMapping(ctx, input.RiskID, input.ResourceID); err != nil { return nil, types.UnlinkRiskOutput{}, fmt.Errorf("failed to unlink risk from obligation: %w", err) } @@ -1770,6 +1790,7 @@ func (r *Resolver) GetTaskTool(ctx context.Context, req *mcp.CallToolRequest, in if err != nil { return nil, types.GetTaskOutput{}, fmt.Errorf("failed to get task: %w", err) } + return nil, types.GetTaskOutput{ Task: types.NewTask(task), }, nil @@ -1801,6 +1822,7 @@ func (r *Resolver) AddTaskTool(ctx context.Context, req *mcp.CallToolRequest, in if err != nil { return nil, types.AddTaskOutput{}, fmt.Errorf("failed to create task: %w", err) } + return nil, types.AddTaskOutput{ Task: types.NewTask(task), }, nil @@ -1829,6 +1851,7 @@ func (r *Resolver) UpdateTaskTool(ctx context.Context, req *mcp.CallToolRequest, if err != nil { return nil, types.UpdateTaskOutput{}, fmt.Errorf("failed to update task: %w", err) } + return nil, types.UpdateTaskOutput{ Task: types.NewTask(task), }, nil @@ -1858,6 +1881,7 @@ func (r *Resolver) UnassignTaskTool(ctx context.Context, req *mcp.CallToolReques if err != nil { return nil, types.UnassignTaskOutput{}, fmt.Errorf("failed to unassign task: %w", err) } + return nil, types.UnassignTaskOutput{ Task: types.NewTask(task), }, nil @@ -1898,6 +1922,7 @@ func (r *Resolver) ListDocumentsTool(ctx context.Context, req *mcp.CallToolReque documentFilter := coredata.NewDocumentFilter(nil). WithStatus([]coredata.DocumentStatus{coredata.DocumentStatusActive}) + if input.Filter != nil { var query *string if input.Filter.Query != nil && *input.Filter.Query != "" { @@ -1983,11 +2008,13 @@ func (r *Resolver) UpdateDocumentTool(ctx context.Context, req *mcp.CallToolRequ } var content *string + if input.Content != nil { c, err := markdownToProseMirrorJSON(*input.Content) if err != nil { panic(fmt.Errorf("cannot convert markdown to prosemirror: %w", err)) } + content = &c } @@ -2081,16 +2108,21 @@ func (r *Resolver) ListDocumentVersionSignaturesTool(ctx context.Context, req *m cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) - var signatureStates []coredata.DocumentVersionSignatureState - var activeContract *bool + var ( + signatureStates []coredata.DocumentVersionSignatureState + activeContract *bool + ) + if input.Filter != nil { if input.Filter.States != nil { signatureStates = input.Filter.States } + if input.Filter.ActiveContract != nil { activeContract = input.Filter.ActiveContract } } + signatureFilter := coredata.NewDocumentVersionSignatureFilter(signatureStates, activeContract) page, err := prb.Documents.ListSignatures(ctx, input.DocumentVersionID, cursor, signatureFilter) @@ -2301,16 +2333,19 @@ func (r *Resolver) LinkMeasureTool(ctx context.Context, req *mcp.CallToolRequest switch input.ResourceID.EntityType() { case coredata.ControlEntityType: r.MustAuthorize(ctx, input.MeasureID, probo.ActionControlMeasureMappingCreate) + if _, _, err := svc.Controls.CreateMeasureMapping(ctx, input.ResourceID, input.MeasureID); err != nil { return nil, types.LinkMeasureOutput{}, fmt.Errorf("failed to link measure to control: %w", err) } case coredata.RiskEntityType: r.MustAuthorize(ctx, input.MeasureID, probo.ActionRiskMeasureMappingCreate) + if _, _, err := svc.Risks.CreateMeasureMapping(ctx, input.ResourceID, input.MeasureID); err != nil { return nil, types.LinkMeasureOutput{}, fmt.Errorf("failed to link measure to risk: %w", err) } case coredata.DocumentEntityType: r.MustAuthorize(ctx, input.MeasureID, probo.ActionMeasureDocumentMappingCreate) + if _, _, err := svc.Measures.CreateDocumentMapping(ctx, input.MeasureID, input.ResourceID); err != nil { return nil, types.LinkMeasureOutput{}, fmt.Errorf("failed to link measure to document: %w", err) } @@ -2327,16 +2362,19 @@ func (r *Resolver) UnlinkMeasureTool(ctx context.Context, req *mcp.CallToolReque switch input.ResourceID.EntityType() { case coredata.ControlEntityType: r.MustAuthorize(ctx, input.MeasureID, probo.ActionControlMeasureMappingDelete) + if _, _, err := svc.Controls.DeleteMeasureMapping(ctx, input.ResourceID, input.MeasureID); err != nil { return nil, types.UnlinkMeasureOutput{}, fmt.Errorf("failed to unlink measure from control: %w", err) } case coredata.RiskEntityType: r.MustAuthorize(ctx, input.MeasureID, probo.ActionRiskMeasureMappingDelete) + if _, _, err := svc.Risks.DeleteMeasureMapping(ctx, input.ResourceID, input.MeasureID); err != nil { return nil, types.UnlinkMeasureOutput{}, fmt.Errorf("failed to unlink measure from risk: %w", err) } case coredata.DocumentEntityType: r.MustAuthorize(ctx, input.MeasureID, probo.ActionMeasureDocumentMappingDelete) + if _, _, err := svc.Measures.DeleteDocumentMapping(ctx, input.MeasureID, input.ResourceID); err != nil { return nil, types.UnlinkMeasureOutput{}, fmt.Errorf("failed to unlink measure from document: %w", err) } @@ -2360,6 +2398,7 @@ func (r *Resolver) ListUsersTool(ctx context.Context, req *mcp.CallToolRequest, Direction: input.OrderBy.Direction, } } + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) filter := coredata.NewMembershipProfileFilter(nil).WithMembership() @@ -2379,11 +2418,14 @@ func (r *Resolver) ListUsersTool(ctx context.Context, req *mcp.CallToolRequest, for _, p := range pageResult.Data { users = append(users, types.NewProfile(p)) } + var nextCursor *page.CursorKey + if len(pageResult.Data) > 0 && pageResult.Cursor != nil { cursorKey := pageResult.Data[len(pageResult.Data)-1].CursorKey(pageResult.Cursor.OrderBy.Field) nextCursor = &cursorKey } + return nil, types.ListUsersOutput{ Users: users, NextCursor: nextCursor, @@ -2397,9 +2439,12 @@ func (r *Resolver) GetUserTool(ctx context.Context, req *mcp.CallToolRequest, in if errors.As(err, &errNotFound) { return nil, types.GetUserOutput{}, fmt.Errorf("user not found: %w", err) } + return nil, types.GetUserOutput{}, fmt.Errorf("get user: %w", err) } + r.MustAuthorize(ctx, profile.OrganizationID, iam.ActionMembershipProfileGet) + return nil, types.GetUserOutput{User: types.NewProfile(profile)}, nil } @@ -2410,9 +2455,11 @@ func (r *Resolver) CreateUserTool(ctx context.Context, req *mcp.CallToolRequest, if input.ContractStartDate != nil { contractStart = &input.ContractStartDate } + if input.ContractEndDate != nil { contractEnd = &input.ContractEndDate } + profile, err := r.iamSvc.OrganizationService.CreateUser(ctx, &iam.CreateUserRequest{ OrganizationID: input.OrganizationID, EmailAddress: input.EmailAddress, @@ -2429,8 +2476,10 @@ func (r *Resolver) CreateUserTool(ctx context.Context, req *mcp.CallToolRequest, if errors.As(err, &errAlreadyExists) { return nil, types.CreateUserOutput{}, fmt.Errorf("user with email already exists: %w", err) } + return nil, types.CreateUserOutput{}, fmt.Errorf("create user: %w", err) } + return nil, types.CreateUserOutput{User: types.NewProfile(profile)}, nil } @@ -2442,16 +2491,22 @@ func (r *Resolver) InviteUserTool(ctx context.Context, req *mcp.CallToolRequest, ProfileID: input.ProfileID, }) if err != nil { - var errOrgNotFound *iam.ErrOrganizationNotFound - var errUserExists *iam.ErrUserAlreadyExists + var ( + errOrgNotFound *iam.ErrOrganizationNotFound + errUserExists *iam.ErrUserAlreadyExists + ) + if errors.As(err, &errOrgNotFound) { return nil, types.InviteUserOutput{}, fmt.Errorf("organization not found: %w", err) } + if errors.As(err, &errUserExists) { return nil, types.InviteUserOutput{}, fmt.Errorf("user already in organization: %w", err) } + return nil, types.InviteUserOutput{}, fmt.Errorf("invite user: %w", err) } + return nil, types.InviteUserOutput{InvitationID: invitation.ID}, nil } @@ -2462,17 +2517,21 @@ func (r *Resolver) UpdateUserTool(ctx context.Context, req *mcp.CallToolRequest, if input.AdditionalEmailAddresses != nil { additionalEmails = *input.AdditionalEmailAddresses } + var position *string if p := UnwrapOmittable(input.Position); p != nil { position = *p } + var contractStart, contractEnd **time.Time if p := UnwrapOmittable(input.ContractStartDate); p != nil { contractStart = p } + if p := UnwrapOmittable(input.ContractEndDate); p != nil { contractEnd = p } + profile, err := r.iamSvc.OrganizationService.UpdateUser(ctx, &iam.UpdateUserRequest{ ID: input.ID, FullName: input.FullName, @@ -2485,11 +2544,13 @@ func (r *Resolver) UpdateUserTool(ctx context.Context, req *mcp.CallToolRequest, if err != nil { return nil, types.UpdateUserOutput{}, fmt.Errorf("update user: %w", err) } + return nil, types.UpdateUserOutput{User: types.NewProfile(profile)}, nil } func (r *Resolver) UpdateMembershipTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateMembershipInput) (*mcp.CallToolResult, types.UpdateMembershipOutput, error) { r.MustAuthorize(ctx, input.MembershipID, iam.ActionMembershipUpdate) + if input.Role == coredata.MembershipRoleOwner { r.MustAuthorize(ctx, input.MembershipID, iam.ActionMembershipRoleSetOwner) } @@ -2498,6 +2559,7 @@ func (r *Resolver) UpdateMembershipTool(ctx context.Context, req *mcp.CallToolRe if err != nil { return nil, types.UpdateMembershipOutput{}, fmt.Errorf("update membership: %w", err) } + return nil, types.UpdateMembershipOutput{ Membership: &types.Membership{ ID: membership.ID, @@ -2512,16 +2574,22 @@ func (r *Resolver) RemoveUserTool(ctx context.Context, req *mcp.CallToolRequest, err := r.iamSvc.OrganizationService.RemoveUser(ctx, input.OrganizationID, input.ProfileID) if err != nil { - var errManagedBySCIM *iam.ErrUserManagedBySCIM - var errLastOwner *iam.ErrLastActiveOwner + var ( + errManagedBySCIM *iam.ErrUserManagedBySCIM + errLastOwner *iam.ErrLastActiveOwner + ) + if errors.As(err, &errManagedBySCIM) { return nil, types.RemoveUserOutput{}, fmt.Errorf("user is managed by SCIM and cannot be removed: %w", err) } + if errors.As(err, &errLastOwner) { return nil, types.RemoveUserOutput{}, fmt.Errorf("cannot remove last active owner: %w", err) } + return nil, types.RemoveUserOutput{}, fmt.Errorf("remove user: %w", err) } + return nil, types.RemoveUserOutput{DeletedUserID: input.ProfileID}, nil } @@ -2932,6 +3000,7 @@ func (r *Resolver) ListAccessEntriesTool(ctx context.Context, req *mcp.CallToolR if input.AccessSourceID != nil { var err error + p, err = r.accessReview.Entries(scope).ListForCampaignIDAndSourceID( ctx, input.CampaignID, @@ -2944,6 +3013,7 @@ func (r *Resolver) ListAccessEntriesTool(ctx context.Context, req *mcp.CallToolR } } else { var err error + p, err = r.accessReview.Entries(scope).ListForCampaignID(ctx, input.CampaignID, cursor, filter) if err != nil { panic(fmt.Errorf("cannot list access entries: %w", err)) @@ -3039,6 +3109,7 @@ func (r *Resolver) RecordAccessEntryDecisionsTool(ctx context.Context, req *mcp. decisions := make([]accessreview.RecordAccessEntryDecisionRequest, len(input.Decisions)) for i, d := range input.Decisions { var decidedByID *gid.GID + organizationID, err := r.accessReview.ResolveEntryOrganizationID(ctx, d.AccessEntryID) if err == nil { if cached, ok := profileCache[organizationID]; ok { @@ -3048,6 +3119,7 @@ func (r *Resolver) RecordAccessEntryDecisionsTool(ctx context.Context, req *mcp. if err == nil { decidedByID = &profile.ID } + profileCache[organizationID] = decidedByID } } @@ -3161,10 +3233,12 @@ func (r *Resolver) UpdateAccessSourceTool(ctx context.Context, req *mcp.CallTool if err != nil { return nil, types.UpdateAccessSourceOutput{}, fmt.Errorf("cannot parse connector_id: %w", err) } + idPtr := &id updateReq.ConnectorID = &idPtr } else { var nilGID *gid.GID + updateReq.ConnectorID = &nilGID } } @@ -3248,6 +3322,7 @@ func (r *Resolver) UpdateAccessReviewCampaignTool(ctx context.Context, req *mcp. controls = append(controls, s) } } + updateReq.FrameworkControls = &controls } else { empty := []string{} @@ -3485,16 +3560,20 @@ func (r *Resolver) ListAuditLogEntriesTool(ctx context.Context, req *mcp.CallToo cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) filter := coredata.NewAuditLogEntryFilter() + if input.Filter != nil { if input.Filter.Action != nil { filter.WithAction(*input.Filter.Action) } + if input.Filter.ActorID != nil { filter.WithActorID(*input.Filter.ActorID) } + if input.Filter.ResourceType != nil { filter.WithResourceType(*input.Filter.ResourceType) } + if input.Filter.ResourceID != nil { filter.WithResourceID(*input.Filter.ResourceID) } @@ -3784,6 +3863,7 @@ func (r *Resolver) ListDocumentVersionApprovalDecisionsTool(ctx context.Context, if input.Filter != nil { states = input.Filter.States } + filter := coredata.NewDocumentVersionApprovalDecisionFilter(states) p, err := svc.DocumentApprovals.ListDecisions(ctx, input.QuorumID, cursor, filter) @@ -3917,6 +3997,7 @@ func (r *Resolver) UpdateThirdPartyContactTool(ctx context.Context, req *mcp.Cal if err != nil { return nil, types.UpdateThirdPartyContactOutput{}, fmt.Errorf("invalid email address: %w", err) } + emailPtr := &emailAddr updateReq.Email = &emailPtr } @@ -4262,6 +4343,7 @@ func (r *Resolver) UpdateTrustCenterTool(ctx context.Context, req *mcp.CallToolR if active := UnwrapOmittable(input.Active); active != nil { updateReq.Active = *active } + if sei := UnwrapOmittable(input.SearchEngineIndexing); sei != nil { updateReq.SearchEngineIndexing = *sei } @@ -4344,9 +4426,11 @@ func (r *Resolver) UpdateTrustCenterReferenceTool(ctx context.Context, req *mcp. if name := UnwrapOmittable(input.Name); name != nil { updateRefReq.Name = *name } + if websiteURL := UnwrapOmittable(input.WebsiteURL); websiteURL != nil { updateRefReq.WebsiteURL = *websiteURL } + if rank := UnwrapOmittable(input.Rank); rank != nil { updateRefReq.Rank = *rank } @@ -4406,6 +4490,7 @@ func (r *Resolver) ListTrustCenterFilesTool(ctx context.Context, req *mcp.CallTo if err != nil { return nil, types.ListTrustCenterFilesOutput{}, fmt.Errorf("cannot generate file URL: %w", err) } + files = append(files, types.NewTrustCenterFile(f, fileURL)) } @@ -4491,9 +4576,11 @@ func (r *Resolver) UpdateComplianceExternalURLTool(ctx context.Context, req *mcp if name := UnwrapOmittable(input.Name); name != nil && *name != nil { updateURLReq.Name = **name } + if u := UnwrapOmittable(input.URL); u != nil && *u != nil { updateURLReq.URL = **u } + if rank := UnwrapOmittable(input.Rank); rank != nil { updateURLReq.Rank = *rank } @@ -4692,27 +4779,33 @@ func (r *Resolver) ListCookieBannersTool(ctx context.Context, req *mcp.CallToolR r.MustAuthorize(ctx, input.OrganizationID, probo.ActionCookieBannerList) scope := coredata.NewScopeFromObjectID(input.OrganizationID) cursor := types.NewCursor(input.Size, input.Cursor, page.OrderBy[coredata.CookieBannerOrderField]{Field: coredata.CookieBannerOrderFieldCreatedAt, Direction: page.OrderDirectionDesc}) + banners, err := r.cookieBanner.ListCookieBannersForOrganization(ctx, scope, input.OrganizationID, cursor, coredata.NewCookieBannerFilter(nil)) if err != nil { panic(fmt.Errorf("cannot list cookie banners: %w", err)) } + p := page.NewPage(banners, cursor) + return nil, types.NewListCookieBannersOutput(p), nil } func (r *Resolver) GetCookieBannerTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetCookieBannerInput) (*mcp.CallToolResult, types.GetCookieBannerOutput, error) { r.MustAuthorize(ctx, input.ID, probo.ActionCookieBannerGet) scope := coredata.NewScopeFromObjectID(input.ID) + banner, err := r.cookieBanner.GetCookieBanner(ctx, scope, input.ID) if err != nil { return nil, types.GetCookieBannerOutput{}, fmt.Errorf("cannot get cookie banner: %w", err) } + return nil, types.GetCookieBannerOutput{CookieBanner: types.NewCookieBanner(banner)}, nil } func (r *Resolver) AddCookieBannerTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddCookieBannerInput) (*mcp.CallToolResult, types.AddCookieBannerOutput, error) { r.MustAuthorize(ctx, input.OrganizationID, probo.ActionCookieBannerCreate) scope := coredata.NewScopeFromObjectID(input.OrganizationID) + banner, err := r.cookieBanner.CreateCookieBanner(ctx, scope, cookiebanner.CreateCookieBannerRequest{ OrganizationID: input.OrganizationID, Name: input.Name, @@ -4724,6 +4817,7 @@ func (r *Resolver) AddCookieBannerTool(ctx context.Context, req *mcp.CallToolReq if err != nil { return nil, types.AddCookieBannerOutput{}, fmt.Errorf("cannot create cookie banner: %w", err) } + return nil, types.AddCookieBannerOutput{CookieBanner: types.NewCookieBanner(banner)}, nil } @@ -4735,15 +4829,19 @@ func (r *Resolver) UpdateCookieBannerTool(ctx context.Context, req *mcp.CallTool if v := UnwrapOmittable(input.Name); v != nil && *v != nil { updateReq.Name = *v } + if v := UnwrapOmittable(input.PrivacyPolicyURL); v != nil && *v != nil { updateReq.PrivacyPolicyURL = *v } + if v := UnwrapOmittable(input.CookiePolicyURL); v != nil && *v != nil { updateReq.CookiePolicyURL = *v } + if v := UnwrapOmittable(input.ConsentExpiryDays); v != nil && *v != nil { updateReq.ConsentExpiryDays = *v } + if v := UnwrapOmittable(input.DefaultLanguage); v != nil && *v != nil { updateReq.DefaultLanguage = *v } @@ -4752,35 +4850,42 @@ func (r *Resolver) UpdateCookieBannerTool(ctx context.Context, req *mcp.CallTool if err != nil { return nil, types.UpdateCookieBannerOutput{}, fmt.Errorf("cannot update cookie banner: %w", err) } + return nil, types.UpdateCookieBannerOutput{CookieBanner: types.NewCookieBanner(banner)}, nil } func (r *Resolver) DeleteCookieBannerTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteCookieBannerInput) (*mcp.CallToolResult, types.DeleteCookieBannerOutput, error) { r.MustAuthorize(ctx, input.ID, probo.ActionCookieBannerDelete) + scope := coredata.NewScopeFromObjectID(input.ID) if err := r.cookieBanner.DeleteCookieBanner(ctx, scope, input.ID); err != nil { return nil, types.DeleteCookieBannerOutput{}, fmt.Errorf("cannot delete cookie banner: %w", err) } + return nil, types.DeleteCookieBannerOutput{DeletedID: input.ID}, nil } func (r *Resolver) ActivateCookieBannerTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ActivateCookieBannerInput) (*mcp.CallToolResult, types.ActivateCookieBannerOutput, error) { r.MustAuthorize(ctx, input.ID, probo.ActionCookieBannerActivate) scope := coredata.NewScopeFromObjectID(input.ID) + banner, err := r.cookieBanner.ActivateCookieBanner(ctx, scope, input.ID) if err != nil { return nil, types.ActivateCookieBannerOutput{}, fmt.Errorf("cannot activate cookie banner: %w", err) } + return nil, types.ActivateCookieBannerOutput{CookieBanner: types.NewCookieBanner(banner)}, nil } func (r *Resolver) DeactivateCookieBannerTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeactivateCookieBannerInput) (*mcp.CallToolResult, types.DeactivateCookieBannerOutput, error) { r.MustAuthorize(ctx, input.ID, probo.ActionCookieBannerDeactivate) scope := coredata.NewScopeFromObjectID(input.ID) + banner, err := r.cookieBanner.DeactivateCookieBanner(ctx, scope, input.ID) if err != nil { return nil, types.DeactivateCookieBannerOutput{}, fmt.Errorf("cannot deactivate cookie banner: %w", err) } + return nil, types.DeactivateCookieBannerOutput{CookieBanner: types.NewCookieBanner(banner)}, nil } @@ -4788,27 +4893,33 @@ func (r *Resolver) ListCookieCategoriesTool(ctx context.Context, req *mcp.CallTo r.MustAuthorize(ctx, input.CookieBannerID, probo.ActionCookieCategoryList) scope := coredata.NewScopeFromObjectID(input.CookieBannerID) cursor := types.NewCursor(input.Size, input.Cursor, page.OrderBy[coredata.CookieCategoryOrderField]{Field: coredata.CookieCategoryOrderFieldRank, Direction: page.OrderDirectionAsc}) + categories, err := r.cookieBanner.ListCookieCategoriesForBanner(ctx, scope, input.CookieBannerID, cursor) if err != nil { panic(fmt.Errorf("cannot list cookie categories: %w", err)) } + p := page.NewPage(categories, cursor) + return nil, types.NewListCookieCategoriesOutput(p), nil } func (r *Resolver) GetCookieCategoryTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetCookieCategoryInput) (*mcp.CallToolResult, types.GetCookieCategoryOutput, error) { r.MustAuthorize(ctx, input.ID, probo.ActionCookieCategoryGet) scope := coredata.NewScopeFromObjectID(input.ID) + category, err := r.cookieBanner.GetCookieCategory(ctx, scope, input.ID) if err != nil { return nil, types.GetCookieCategoryOutput{}, fmt.Errorf("cannot get cookie category: %w", err) } + return nil, types.GetCookieCategoryOutput{CookieCategory: types.NewCookieCategory(category)}, nil } func (r *Resolver) AddCookieCategoryTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddCookieCategoryInput) (*mcp.CallToolResult, types.AddCookieCategoryOutput, error) { r.MustAuthorize(ctx, input.CookieBannerID, probo.ActionCookieCategoryCreate) scope := coredata.NewScopeFromObjectID(input.CookieBannerID) + category, err := r.cookieBanner.CreateCookieCategory(ctx, scope, cookiebanner.CreateCookieCategoryRequest{ CookieBannerID: input.CookieBannerID, Name: input.Name, @@ -4819,41 +4930,50 @@ func (r *Resolver) AddCookieCategoryTool(ctx context.Context, req *mcp.CallToolR if err != nil { return nil, types.AddCookieCategoryOutput{}, fmt.Errorf("cannot create cookie category: %w", err) } + return nil, types.AddCookieCategoryOutput{CookieCategory: types.NewCookieCategory(category)}, nil } func (r *Resolver) UpdateCookieCategoryTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateCookieCategoryInput) (*mcp.CallToolResult, types.UpdateCookieCategoryOutput, error) { r.MustAuthorize(ctx, input.ID, probo.ActionCookieCategoryUpdate) scope := coredata.NewScopeFromObjectID(input.ID) + updateReq := cookiebanner.UpdateCookieCategoryRequest{CookieCategoryID: input.ID} if v := UnwrapOmittable(input.Name); v != nil && *v != nil { updateReq.Name = *v } + if v := UnwrapOmittable(input.Slug); v != nil && *v != nil { updateReq.Slug = *v } + if v := UnwrapOmittable(input.Description); v != nil && *v != nil { updateReq.Description = *v } + category, err := r.cookieBanner.UpdateCookieCategory(ctx, scope, updateReq) if err != nil { return nil, types.UpdateCookieCategoryOutput{}, fmt.Errorf("cannot update cookie category: %w", err) } + return nil, types.UpdateCookieCategoryOutput{CookieCategory: types.NewCookieCategory(category)}, nil } func (r *Resolver) DeleteCookieCategoryTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteCookieCategoryInput) (*mcp.CallToolResult, types.DeleteCookieCategoryOutput, error) { r.MustAuthorize(ctx, input.ID, probo.ActionCookieCategoryDelete) + scope := coredata.NewScopeFromObjectID(input.ID) if err := r.cookieBanner.DeleteCookieCategory(ctx, scope, input.ID); err != nil { return nil, types.DeleteCookieCategoryOutput{}, fmt.Errorf("cannot delete cookie category: %w", err) } + return nil, types.DeleteCookieCategoryOutput{DeletedID: input.ID}, nil } func (r *Resolver) ReorderCookieCategoryTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ReorderCookieCategoryInput) (*mcp.CallToolResult, types.ReorderCookieCategoryOutput, error) { r.MustAuthorize(ctx, input.ID, probo.ActionCookieCategoryUpdate) scope := coredata.NewScopeFromObjectID(input.ID) + _, err := r.cookieBanner.ReorderCookieCategory(ctx, scope, cookiebanner.ReorderCookieCategoryRequest{ CookieCategoryID: input.ID, Rank: input.Rank, @@ -4861,10 +4981,12 @@ func (r *Resolver) ReorderCookieCategoryTool(ctx context.Context, req *mcp.CallT if err != nil { return nil, types.ReorderCookieCategoryOutput{}, fmt.Errorf("cannot reorder cookie category: %w", err) } + category, err := r.cookieBanner.GetCookieCategory(ctx, scope, input.ID) if err != nil { return nil, types.ReorderCookieCategoryOutput{}, fmt.Errorf("cannot get cookie category: %w", err) } + return nil, types.ReorderCookieCategoryOutput{CookieCategory: types.NewCookieCategory(category)}, nil } @@ -4872,27 +4994,33 @@ func (r *Resolver) ListTrackerPatternsTool(ctx context.Context, req *mcp.CallToo r.MustAuthorize(ctx, input.CookieCategoryID, probo.ActionTrackerPatternList) scope := coredata.NewScopeFromObjectID(input.CookieCategoryID) cursor := types.NewCursor(input.Size, input.Cursor, page.OrderBy[coredata.TrackerPatternOrderField]{Field: coredata.TrackerPatternOrderFieldCreatedAt, Direction: page.OrderDirectionAsc}) + patterns, err := r.cookieBanner.ListTrackerPatternsForCategory(ctx, scope, input.CookieCategoryID, cursor) if err != nil { panic(fmt.Errorf("cannot list tracker patterns: %w", err)) } + p := page.NewPage(patterns, cursor) + return nil, types.NewListTrackerPatternsOutput(p), nil } func (r *Resolver) GetTrackerPatternTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetTrackerPatternInput) (*mcp.CallToolResult, types.GetTrackerPatternOutput, error) { r.MustAuthorize(ctx, input.ID, probo.ActionTrackerPatternGet) scope := coredata.NewScopeFromObjectID(input.ID) + pattern, err := r.cookieBanner.GetTrackerPattern(ctx, scope, input.ID) if err != nil { return nil, types.GetTrackerPatternOutput{}, fmt.Errorf("cannot get tracker pattern: %w", err) } + return nil, types.GetTrackerPatternOutput{TrackerPattern: types.NewTrackerPattern(pattern)}, nil } func (r *Resolver) AddTrackerPatternTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddTrackerPatternInput) (*mcp.CallToolResult, types.AddTrackerPatternOutput, error) { r.MustAuthorize(ctx, input.CookieCategoryID, probo.ActionTrackerPatternCreate) scope := coredata.NewScopeFromObjectID(input.CookieCategoryID) + pattern, err := r.cookieBanner.CreateTrackerPattern(ctx, scope, cookiebanner.CreateTrackerPatternRequest{ CookieCategoryID: input.CookieCategoryID, TrackerType: coredata.TrackerType(input.TrackerType), @@ -4905,42 +5033,51 @@ func (r *Resolver) AddTrackerPatternTool(ctx context.Context, req *mcp.CallToolR if err != nil { return nil, types.AddTrackerPatternOutput{}, fmt.Errorf("cannot create tracker pattern: %w", err) } + return nil, types.AddTrackerPatternOutput{TrackerPattern: types.NewTrackerPattern(pattern)}, nil } func (r *Resolver) UpdateTrackerPatternTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateTrackerPatternInput) (*mcp.CallToolResult, types.UpdateTrackerPatternOutput, error) { r.MustAuthorize(ctx, input.ID, probo.ActionTrackerPatternUpdate) scope := coredata.NewScopeFromObjectID(input.ID) + updateReq := cookiebanner.UpdateTrackerPatternRequest{TrackerPatternID: input.ID} if input.MaxAgeSeconds.IsSet() { val, _ := input.MaxAgeSeconds.Value() updateReq.MaxAgeSeconds = &val } + if v := UnwrapOmittable(input.Description); v != nil && *v != nil { updateReq.Description = *v } + if v := UnwrapOmittable(input.Excluded); v != nil && *v != nil { updateReq.Excluded = *v } + pattern, err := r.cookieBanner.UpdateTrackerPattern(ctx, scope, updateReq) if err != nil { return nil, types.UpdateTrackerPatternOutput{}, fmt.Errorf("cannot update tracker pattern: %w", err) } + return nil, types.UpdateTrackerPatternOutput{TrackerPattern: types.NewTrackerPattern(pattern)}, nil } func (r *Resolver) DeleteTrackerPatternTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteTrackerPatternInput) (*mcp.CallToolResult, types.DeleteTrackerPatternOutput, error) { r.MustAuthorize(ctx, input.ID, probo.ActionTrackerPatternDelete) + scope := coredata.NewScopeFromObjectID(input.ID) if err := r.cookieBanner.DeleteTrackerPattern(ctx, scope, input.ID); err != nil { return nil, types.DeleteTrackerPatternOutput{}, fmt.Errorf("cannot delete tracker pattern: %w", err) } + return nil, types.DeleteTrackerPatternOutput{DeletedID: input.ID}, nil } func (r *Resolver) MoveTrackerPatternToCategoryTool(ctx context.Context, req *mcp.CallToolRequest, input *types.MoveTrackerPatternToCategoryInput) (*mcp.CallToolResult, types.MoveTrackerPatternToCategoryOutput, error) { r.MustAuthorize(ctx, input.TrackerPatternID, probo.ActionTrackerPatternUpdate) scope := coredata.NewScopeFromObjectID(input.TrackerPatternID) + result, err := r.cookieBanner.MoveTrackerPatternToCategory(ctx, scope, cookiebanner.MoveTrackerPatternToCategoryRequest{ TrackerPatternID: input.TrackerPatternID, TargetCookieCategoryID: input.TargetCookieCategoryID, @@ -4948,16 +5085,19 @@ func (r *Resolver) MoveTrackerPatternToCategoryTool(ctx context.Context, req *mc if err != nil { return nil, types.MoveTrackerPatternToCategoryOutput{}, fmt.Errorf("cannot move tracker pattern: %w", err) } + return nil, types.MoveTrackerPatternToCategoryOutput{TrackerPattern: types.NewTrackerPattern(result.TrackerPattern)}, nil } func (r *Resolver) PublishCookieBannerVersionTool(ctx context.Context, req *mcp.CallToolRequest, input *types.PublishCookieBannerVersionInput) (*mcp.CallToolResult, types.PublishCookieBannerVersionOutput, error) { r.MustAuthorize(ctx, input.CookieBannerID, probo.ActionCookieBannerVersionPublish) scope := coredata.NewScopeFromObjectID(input.CookieBannerID) + version, err := r.cookieBanner.PublishCookieBannerVersion(ctx, scope, input.CookieBannerID) if err != nil { return nil, types.PublishCookieBannerVersionOutput{}, fmt.Errorf("cannot publish cookie banner version: %w", err) } + return nil, types.PublishCookieBannerVersionOutput{CookieBannerVersion: types.NewCookieBannerVersion(version)}, nil } @@ -4965,17 +5105,21 @@ func (r *Resolver) ListCookieBannerVersionsTool(ctx context.Context, req *mcp.Ca r.MustAuthorize(ctx, input.CookieBannerID, probo.ActionCookieBannerVersionList) scope := coredata.NewScopeFromObjectID(input.CookieBannerID) cursor := types.NewCursor(input.Size, input.Cursor, page.OrderBy[coredata.CookieBannerVersionOrderField]{Field: coredata.CookieBannerVersionOrderFieldCreatedAt, Direction: page.OrderDirectionDesc}) + versions, err := r.cookieBanner.ListCookieBannerVersionsForBanner(ctx, scope, input.CookieBannerID, cursor) if err != nil { panic(fmt.Errorf("cannot list cookie banner versions: %w", err)) } + p := page.NewPage(versions, cursor) + return nil, types.NewListCookieBannerVersionsOutput(p), nil } func (r *Resolver) UpsertCookieBannerTranslationTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpsertCookieBannerTranslationInput) (*mcp.CallToolResult, types.UpsertCookieBannerTranslationOutput, error) { r.MustAuthorize(ctx, input.CookieBannerID, probo.ActionCookieBannerUpdate) scope := coredata.NewScopeFromObjectID(input.CookieBannerID) + translation, err := r.cookieBanner.UpsertCookieBannerTranslation(ctx, scope, cookiebanner.UpsertCookieBannerTranslationRequest{ CookieBannerID: input.CookieBannerID, Language: input.Language, @@ -4984,6 +5128,7 @@ func (r *Resolver) UpsertCookieBannerTranslationTool(ctx context.Context, req *m if err != nil { return nil, types.UpsertCookieBannerTranslationOutput{}, fmt.Errorf("cannot upsert cookie banner translation: %w", err) } + return nil, types.UpsertCookieBannerTranslationOutput{CookieBannerTranslation: types.NewCookieBannerTranslation(translation)}, nil } @@ -4993,27 +5138,33 @@ func (r *Resolver) ListCookieConsentRecordsTool(ctx context.Context, req *mcp.Ca cursor := types.NewCursor(input.Size, input.Cursor, page.OrderBy[coredata.CookieConsentRecordOrderField]{Field: coredata.CookieConsentRecordOrderFieldCreatedAt, Direction: page.OrderDirectionDesc}) var action *coredata.CookieConsentAction + if input.Action != nil { a := coredata.CookieConsentAction(*input.Action) action = &a } + filter := coredata.NewCookieConsentRecordFilter(action, input.VisitorID, input.Version) records, err := r.cookieBanner.ListCookieConsentRecordsForBanner(ctx, scope, input.CookieBannerID, cursor, filter) if err != nil { panic(fmt.Errorf("cannot list cookie consent records: %w", err)) } + p := page.NewPage(records, cursor) + return nil, types.NewListCookieConsentRecordsOutput(p), nil } func (r *Resolver) GetCookieConsentRecordTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetCookieConsentRecordInput) (*mcp.CallToolResult, types.GetCookieConsentRecordOutput, error) { r.MustAuthorize(ctx, input.ID, probo.ActionCookieConsentRecordList) scope := coredata.NewScopeFromObjectID(input.ID) + record, err := r.cookieBanner.GetCookieConsentRecord(ctx, scope, input.ID) if err != nil { return nil, types.GetCookieConsentRecordOutput{}, fmt.Errorf("cannot get cookie consent record: %w", err) } + return nil, types.GetCookieConsentRecordOutput{CookieConsentRecord: types.NewCookieConsentRecord(record)}, nil } @@ -5042,6 +5193,7 @@ func (r *Resolver) GetSCIMConfigurationTool(ctx context.Context, req *mcp.CallTo if errors.As(err, &errNotFound) { return nil, types.GetSCIMConfigurationOutput{}, fmt.Errorf("SCIM configuration not found") } + panic(fmt.Errorf("cannot get SCIM configuration: %w", err)) } @@ -5066,6 +5218,7 @@ func (r *Resolver) CreateSCIMConfigurationTool(ctx context.Context, req *mcp.Cal if err != nil { return nil, types.CreateSCIMConfigurationOutput{}, fmt.Errorf("cannot create SCIM bridge: %w", err) } + output.ScimBridge = types.NewSCIMBridge(bridge) } @@ -5106,6 +5259,7 @@ func (r *Resolver) GetSCIMBridgeTool(ctx context.Context, req *mcp.CallToolReque if errors.As(err, &errNotFound) { return nil, types.GetSCIMBridgeOutput{}, fmt.Errorf("SCIM bridge %s not found", input.ID) } + panic(fmt.Errorf("cannot get SCIM bridge: %w", err)) } @@ -5152,6 +5306,7 @@ func (r *Resolver) PublishDocumentTool(ctx context.Context, req *mcp.CallToolReq if !input.Minor && len(input.ApproverIds) > 0 { action = probo.ActionDocumentVersionRequestApproval } + r.MustAuthorize(ctx, input.DocumentID, action) svc := r.ProboService(ctx, input.DocumentID) @@ -5173,6 +5328,7 @@ func (r *Resolver) PublishDocumentTool(ctx context.Context, req *mcp.CallToolReq if result.Quorum != nil { output.ApprovalQuorum = types.NewDocumentVersionApprovalQuorum(result.Quorum) } + return nil, output, nil } @@ -5180,31 +5336,38 @@ func (r *Resolver) ListTrackerResourcesTool(ctx context.Context, req *mcp.CallTo r.MustAuthorize(ctx, input.CookieCategoryID, probo.ActionTrackerResourceList) scope := coredata.NewScopeFromObjectID(input.CookieCategoryID) cursor := types.NewCursor(input.Size, input.Cursor, page.OrderBy[coredata.TrackerResourceOrderField]{Field: coredata.TrackerResourceOrderFieldCreatedAt, Direction: page.OrderDirectionAsc}) + resources, err := r.cookieBanner.ListTrackerResourcesForCategory(ctx, scope, input.CookieCategoryID, cursor) if err != nil { panic(fmt.Errorf("cannot list tracker resources: %w", err)) } + p := page.NewPage(resources, cursor) + return nil, types.NewListTrackerResourcesOutput(p), nil } func (r *Resolver) GetTrackerResourceTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetTrackerResourceInput) (*mcp.CallToolResult, types.GetTrackerResourceOutput, error) { r.MustAuthorize(ctx, input.ID, probo.ActionTrackerResourceGet) scope := coredata.NewScopeFromObjectID(input.ID) + resource, err := r.cookieBanner.GetTrackerResource(ctx, scope, input.ID) if err != nil { return nil, types.GetTrackerResourceOutput{}, fmt.Errorf("cannot get tracker resource: %w", err) } + return nil, types.GetTrackerResourceOutput{TrackerResource: types.NewTrackerResource(resource)}, nil } func (r *Resolver) AddTrackerResourceTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddTrackerResourceInput) (*mcp.CallToolResult, types.AddTrackerResourceOutput, error) { r.MustAuthorize(ctx, input.CookieCategoryID, probo.ActionTrackerResourceCreate) scope := coredata.NewScopeFromObjectID(input.CookieCategoryID) + description := "" if input.Description != nil { description = *input.Description } + resource, err := r.cookieBanner.CreateTrackerResource(ctx, scope, cookiebanner.CreateTrackerResourceRequest{ CookieCategoryID: input.CookieCategoryID, ResourceType: coredata.TrackerResourceType(input.ResourceType), @@ -5216,41 +5379,50 @@ func (r *Resolver) AddTrackerResourceTool(ctx context.Context, req *mcp.CallTool if err != nil { return nil, types.AddTrackerResourceOutput{}, fmt.Errorf("cannot create tracker resource: %w", err) } + return nil, types.AddTrackerResourceOutput{TrackerResource: types.NewTrackerResource(resource)}, nil } func (r *Resolver) UpdateTrackerResourceTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateTrackerResourceInput) (*mcp.CallToolResult, types.UpdateTrackerResourceOutput, error) { r.MustAuthorize(ctx, input.ID, probo.ActionTrackerResourceUpdate) scope := coredata.NewScopeFromObjectID(input.ID) + updateReq := cookiebanner.UpdateTrackerResourceRequest{TrackerResourceID: input.ID} if v := UnwrapOmittable(input.DisplayName); v != nil && *v != nil { updateReq.DisplayName = *v } + if v := UnwrapOmittable(input.Description); v != nil && *v != nil { updateReq.Description = *v } + if v := UnwrapOmittable(input.Excluded); v != nil && *v != nil { updateReq.Excluded = *v } + resource, err := r.cookieBanner.UpdateTrackerResource(ctx, scope, updateReq) if err != nil { return nil, types.UpdateTrackerResourceOutput{}, fmt.Errorf("cannot update tracker resource: %w", err) } + return nil, types.UpdateTrackerResourceOutput{TrackerResource: types.NewTrackerResource(resource)}, nil } func (r *Resolver) DeleteTrackerResourceTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteTrackerResourceInput) (*mcp.CallToolResult, types.DeleteTrackerResourceOutput, error) { r.MustAuthorize(ctx, input.ID, probo.ActionTrackerResourceDelete) + scope := coredata.NewScopeFromObjectID(input.ID) if err := r.cookieBanner.DeleteTrackerResource(ctx, scope, input.ID); err != nil { return nil, types.DeleteTrackerResourceOutput{}, fmt.Errorf("cannot delete tracker resource: %w", err) } + return nil, types.DeleteTrackerResourceOutput{DeletedID: input.ID}, nil } func (r *Resolver) MoveTrackerResourceToCategoryTool(ctx context.Context, req *mcp.CallToolRequest, input *types.MoveTrackerResourceToCategoryInput) (*mcp.CallToolResult, types.MoveTrackerResourceToCategoryOutput, error) { r.MustAuthorize(ctx, input.TrackerResourceID, probo.ActionTrackerResourceUpdate) scope := coredata.NewScopeFromObjectID(input.TrackerResourceID) + result, err := r.cookieBanner.MoveTrackerResourceToCategory(ctx, scope, cookiebanner.MoveTrackerResourceToCategoryRequest{ TrackerResourceID: input.TrackerResourceID, TargetCookieCategoryID: input.TargetCookieCategoryID, @@ -5258,5 +5430,6 @@ func (r *Resolver) MoveTrackerResourceToCategoryTool(ctx context.Context, req *m if err != nil { return nil, types.MoveTrackerResourceToCategoryOutput{}, fmt.Errorf("cannot move tracker resource: %w", err) } + return nil, types.MoveTrackerResourceToCategoryOutput{TrackerResource: types.NewTrackerResource(result.TrackerResource)}, nil } diff --git a/pkg/server/api/mcp/v1/types/access_review.go b/pkg/server/api/mcp/v1/types/access_review.go index cbd9f6e38..5456fd39d 100644 --- a/pkg/server/api/mcp/v1/types/access_review.go +++ b/pkg/server/api/mcp/v1/types/access_review.go @@ -40,6 +40,7 @@ func NewListAccessSourcesOutput( } var nextCursor *page.CursorKey + if len(p.Data) > 0 { cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) nextCursor = &cursorKey @@ -105,6 +106,7 @@ func NewListAccessReviewCampaignsOutput( } var nextCursor *page.CursorKey + if len(p.Data) > 0 { cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) nextCursor = &cursorKey @@ -125,6 +127,7 @@ func NewListAccessEntriesOutput( } var nextCursor *page.CursorKey + if len(p.Data) > 0 { cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/asset.go b/pkg/server/api/mcp/v1/types/asset.go index a0e029c11..954c502f1 100644 --- a/pkg/server/api/mcp/v1/types/asset.go +++ b/pkg/server/api/mcp/v1/types/asset.go @@ -40,6 +40,7 @@ func NewListAssetsOutput(assetPage *page.Page[*coredata.Asset, coredata.AssetOrd } var nextCursor *page.CursorKey + if len(assetPage.Data) > 0 { cursorKey := assetPage.Data[len(assetPage.Data)-1].CursorKey(assetPage.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/audit.go b/pkg/server/api/mcp/v1/types/audit.go index 56e4568ad..263803e07 100644 --- a/pkg/server/api/mcp/v1/types/audit.go +++ b/pkg/server/api/mcp/v1/types/audit.go @@ -49,6 +49,7 @@ func NewListControlAuditsOutput(auditPage *page.Page[*coredata.Audit, coredata.A } var nextCursor *page.CursorKey + if len(auditPage.Data) > 0 { cursorKey := auditPage.Data[len(auditPage.Data)-1].CursorKey(auditPage.Cursor.OrderBy.Field) nextCursor = &cursorKey @@ -67,6 +68,7 @@ func NewListAuditsOutput(auditPage *page.Page[*coredata.Audit, coredata.AuditOrd } var nextCursor *page.CursorKey + if len(auditPage.Data) > 0 { cursorKey := auditPage.Data[len(auditPage.Data)-1].CursorKey(auditPage.Cursor.OrderBy.Field) nextCursor = &cursorKey @@ -85,6 +87,7 @@ func NewListFindingAuditsOutput(auditPage *page.Page[*coredata.Audit, coredata.A } var nextCursor *page.CursorKey + if len(auditPage.Data) > 0 { cursorKey := auditPage.Data[len(auditPage.Data)-1].CursorKey(auditPage.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/audit_log_entry.go b/pkg/server/api/mcp/v1/types/audit_log_entry.go index a279254fd..7e51662f4 100644 --- a/pkg/server/api/mcp/v1/types/audit_log_entry.go +++ b/pkg/server/api/mcp/v1/types/audit_log_entry.go @@ -50,6 +50,7 @@ func NewListAuditLogEntriesOutput(p *page.Page[*coredata.AuditLogEntry, coredata } var nextCursor *page.CursorKey + if len(p.Data) > 0 { cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/compliance_external_url.go b/pkg/server/api/mcp/v1/types/compliance_external_url.go index 558229b98..39a77ae10 100644 --- a/pkg/server/api/mcp/v1/types/compliance_external_url.go +++ b/pkg/server/api/mcp/v1/types/compliance_external_url.go @@ -37,6 +37,7 @@ func NewListComplianceExternalURLsOutput(p *page.Page[*coredata.ComplianceExtern } var nextCursor *page.CursorKey + if len(p.Data) > 0 { cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/control.go b/pkg/server/api/mcp/v1/types/control.go index 4c56e1018..f9fce63b9 100644 --- a/pkg/server/api/mcp/v1/types/control.go +++ b/pkg/server/api/mcp/v1/types/control.go @@ -41,6 +41,7 @@ func NewListMeasureControlsOutput(controlPage *page.Page[*coredata.Control, core } var nextCursor *page.CursorKey + if len(controlPage.Data) > 0 { cursorKey := controlPage.Data[len(controlPage.Data)-1].CursorKey(controlPage.Cursor.OrderBy.Field) nextCursor = &cursorKey @@ -59,6 +60,7 @@ func NewListControlsOutput(controlPage *page.Page[*coredata.Control, coredata.Co } var nextCursor *page.CursorKey + if len(controlPage.Data) > 0 { cursorKey := controlPage.Data[len(controlPage.Data)-1].CursorKey(controlPage.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/cookie_banner.go b/pkg/server/api/mcp/v1/types/cookie_banner.go index 16b13c673..9bd901cb1 100644 --- a/pkg/server/api/mcp/v1/types/cookie_banner.go +++ b/pkg/server/api/mcp/v1/types/cookie_banner.go @@ -43,6 +43,7 @@ func NewListCookieBannersOutput(p *page.Page[*coredata.CookieBanner, coredata.Co } var nextCursor *page.CursorKey + if len(p.Data) > 0 { cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/cookie_banner_version.go b/pkg/server/api/mcp/v1/types/cookie_banner_version.go index c7f953652..286e397c5 100644 --- a/pkg/server/api/mcp/v1/types/cookie_banner_version.go +++ b/pkg/server/api/mcp/v1/types/cookie_banner_version.go @@ -37,6 +37,7 @@ func NewListCookieBannerVersionsOutput(p *page.Page[*coredata.CookieBannerVersio } var nextCursor *page.CursorKey + if len(p.Data) > 0 { cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/cookie_category.go b/pkg/server/api/mcp/v1/types/cookie_category.go index b40a0bd55..ba479252e 100644 --- a/pkg/server/api/mcp/v1/types/cookie_category.go +++ b/pkg/server/api/mcp/v1/types/cookie_category.go @@ -45,6 +45,7 @@ func NewListCookieCategoriesOutput(p *page.Page[*coredata.CookieCategory, coreda } var nextCursor *page.CursorKey + if len(p.Data) > 0 { cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/cookie_consent_record.go b/pkg/server/api/mcp/v1/types/cookie_consent_record.go index c47b2a5dc..bf63ca88b 100644 --- a/pkg/server/api/mcp/v1/types/cookie_consent_record.go +++ b/pkg/server/api/mcp/v1/types/cookie_consent_record.go @@ -48,6 +48,7 @@ func NewListCookieConsentRecordsOutput(p *page.Page[*coredata.CookieConsentRecor } var nextCursor *page.CursorKey + if len(p.Data) > 0 { cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/data_protection_impact_assessment.go b/pkg/server/api/mcp/v1/types/data_protection_impact_assessment.go index e4bc104ee..917892d96 100644 --- a/pkg/server/api/mcp/v1/types/data_protection_impact_assessment.go +++ b/pkg/server/api/mcp/v1/types/data_protection_impact_assessment.go @@ -42,11 +42,14 @@ func NewListDataProtectionImpactAssessmentsOutput( for _, v := range pg.Data { items = append(items, NewDataProtectionImpactAssessment(v)) } + var nextCursor *page.CursorKey + if len(pg.Data) > 0 { cursorKey := pg.Data[len(pg.Data)-1].CursorKey(pg.Cursor.OrderBy.Field) nextCursor = &cursorKey } + return ListDataProtectionImpactAssessmentsOutput{ NextCursor: nextCursor, DataProtectionImpactAssessments: items, diff --git a/pkg/server/api/mcp/v1/types/datum.go b/pkg/server/api/mcp/v1/types/datum.go index 8b2ef0129..93e4e9daf 100644 --- a/pkg/server/api/mcp/v1/types/datum.go +++ b/pkg/server/api/mcp/v1/types/datum.go @@ -38,6 +38,7 @@ func NewListDataOutput(datumPage *page.Page[*coredata.Datum, coredata.DatumOrder } var nextCursor *page.CursorKey + if len(datumPage.Data) > 0 { cursorKey := datumPage.Data[len(datumPage.Data)-1].CursorKey(datumPage.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/document.go b/pkg/server/api/mcp/v1/types/document.go index ec0dda7a9..35ef9147f 100644 --- a/pkg/server/api/mcp/v1/types/document.go +++ b/pkg/server/api/mcp/v1/types/document.go @@ -63,6 +63,7 @@ func NewListControlDocumentsOutput(documentPage *page.Page[*coredata.Document, c } var nextCursor *page.CursorKey + if len(documentPage.Data) > 0 { cursorKey := documentPage.Data[len(documentPage.Data)-1].CursorKey(documentPage.Cursor.OrderBy.Field) nextCursor = &cursorKey @@ -81,6 +82,7 @@ func NewListMeasureDocumentsOutput(documentPage *page.Page[*coredata.Document, c } var nextCursor *page.CursorKey + if len(documentPage.Data) > 0 { cursorKey := documentPage.Data[len(documentPage.Data)-1].CursorKey(documentPage.Cursor.OrderBy.Field) nextCursor = &cursorKey @@ -99,6 +101,7 @@ func NewListDocumentsOutput(documentPage *page.Page[*coredata.Document, coredata } var nextCursor *page.CursorKey + if len(documentPage.Data) > 0 { cursorKey := documentPage.Data[len(documentPage.Data)-1].CursorKey(documentPage.Cursor.OrderBy.Field) nextCursor = &cursorKey @@ -148,6 +151,7 @@ func NewListDocumentVersionsOutput(versionPage *page.Page[*coredata.DocumentVers } var nextCursor *page.CursorKey + if len(versionPage.Data) > 0 { cursorKey := versionPage.Data[len(versionPage.Data)-1].CursorKey(versionPage.Cursor.OrderBy.Field) nextCursor = &cursorKey @@ -180,6 +184,7 @@ func NewListDocumentVersionSignaturesOutput(signaturePage *page.Page[*coredata.D } var nextCursor *page.CursorKey + if len(signaturePage.Data) > 0 { cursorKey := signaturePage.Data[len(signaturePage.Data)-1].CursorKey(signaturePage.Cursor.OrderBy.Field) nextCursor = &cursorKey @@ -209,6 +214,7 @@ func NewListDocumentVersionApprovalQuorumsOutput(quorumPage *page.Page[*coredata } var nextCursor *page.CursorKey + if len(quorumPage.Data) > 0 { cursorKey := quorumPage.Data[len(quorumPage.Data)-1].CursorKey(quorumPage.Cursor.OrderBy.Field) nextCursor = &cursorKey @@ -241,6 +247,7 @@ func NewListDocumentVersionApprovalDecisionsOutput(decisionPage *page.Page[*core } var nextCursor *page.CursorKey + if len(decisionPage.Data) > 0 { cursorKey := decisionPage.Data[len(decisionPage.Data)-1].CursorKey(decisionPage.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/evidence.go b/pkg/server/api/mcp/v1/types/evidence.go index ccfabfa58..c4dbf99e2 100644 --- a/pkg/server/api/mcp/v1/types/evidence.go +++ b/pkg/server/api/mcp/v1/types/evidence.go @@ -42,6 +42,7 @@ func NewListMeasureEvidencesOutput(evidencePage *page.Page[*coredata.Evidence, c } var nextCursor *page.CursorKey + if len(evidencePage.Data) > 0 { cursorKey := evidencePage.Data[len(evidencePage.Data)-1].CursorKey(evidencePage.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/finding.go b/pkg/server/api/mcp/v1/types/finding.go index 60430804a..d98afb6ce 100644 --- a/pkg/server/api/mcp/v1/types/finding.go +++ b/pkg/server/api/mcp/v1/types/finding.go @@ -50,6 +50,7 @@ func NewListFindingsOutput(findingPage *page.Page[*coredata.Finding, coredata.Fi } var nextCursor *page.CursorKey + if len(findingPage.Data) > 0 { cursorKey := findingPage.Data[len(findingPage.Data)-1].CursorKey(findingPage.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/framework.go b/pkg/server/api/mcp/v1/types/framework.go index c46eb841f..433c9d692 100644 --- a/pkg/server/api/mcp/v1/types/framework.go +++ b/pkg/server/api/mcp/v1/types/framework.go @@ -37,6 +37,7 @@ func NewListFrameworksOutput(frameworkPage *page.Page[*coredata.Framework, cored } var nextCursor *page.CursorKey + if len(frameworkPage.Data) > 0 { cursorKey := frameworkPage.Data[len(frameworkPage.Data)-1].CursorKey(frameworkPage.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/measure.go b/pkg/server/api/mcp/v1/types/measure.go index 1de81e609..b88508677 100644 --- a/pkg/server/api/mcp/v1/types/measure.go +++ b/pkg/server/api/mcp/v1/types/measure.go @@ -38,6 +38,7 @@ func NewListControlMeasuresOutput(measurePage *page.Page[*coredata.Measure, core } var nextCursor *page.CursorKey + if len(measurePage.Data) > 0 { cursorKey := measurePage.Data[len(measurePage.Data)-1].CursorKey(measurePage.Cursor.OrderBy.Field) nextCursor = &cursorKey @@ -56,6 +57,7 @@ func NewListMeasuresOutput(measurePage *page.Page[*coredata.Measure, coredata.Me } var nextCursor *page.CursorKey + if len(measurePage.Data) > 0 { cursorKey := measurePage.Data[len(measurePage.Data)-1].CursorKey(measurePage.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/obligation.go b/pkg/server/api/mcp/v1/types/obligation.go index 8349c6614..c6aa7e6f6 100644 --- a/pkg/server/api/mcp/v1/types/obligation.go +++ b/pkg/server/api/mcp/v1/types/obligation.go @@ -47,6 +47,7 @@ func NewListObligationsOutput(obligationPage *page.Page[*coredata.Obligation, co } var nextCursor *page.CursorKey + if len(obligationPage.Data) > 0 { cursorKey := obligationPage.Data[len(obligationPage.Data)-1].CursorKey(obligationPage.Cursor.OrderBy.Field) nextCursor = &cursorKey @@ -65,6 +66,7 @@ func NewListControlObligationsOutput(obligationPage *page.Page[*coredata.Obligat } var nextCursor *page.CursorKey + if len(obligationPage.Data) > 0 { cursorKey := obligationPage.Data[len(obligationPage.Data)-1].CursorKey(obligationPage.Cursor.OrderBy.Field) nextCursor = &cursorKey @@ -83,6 +85,7 @@ func NewListRiskObligationsOutput(obligationPage *page.Page[*coredata.Obligation } var nextCursor *page.CursorKey + if len(obligationPage.Data) > 0 { cursorKey := obligationPage.Data[len(obligationPage.Data)-1].CursorKey(obligationPage.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/processing_activity.go b/pkg/server/api/mcp/v1/types/processing_activity.go index d77a63244..705505fe4 100644 --- a/pkg/server/api/mcp/v1/types/processing_activity.go +++ b/pkg/server/api/mcp/v1/types/processing_activity.go @@ -51,11 +51,14 @@ func NewListProcessingActivitiesOutput(pg *page.Page[*coredata.ProcessingActivit for _, v := range pg.Data { items = append(items, NewProcessingActivity(v)) } + var nextCursor *page.CursorKey + if len(pg.Data) > 0 { cursorKey := pg.Data[len(pg.Data)-1].CursorKey(pg.Cursor.OrderBy.Field) nextCursor = &cursorKey } + return ListProcessingActivitiesOutput{ NextCursor: nextCursor, ProcessingActivities: items, diff --git a/pkg/server/api/mcp/v1/types/rights_request.go b/pkg/server/api/mcp/v1/types/rights_request.go index 1f414988c..a1c41ed01 100644 --- a/pkg/server/api/mcp/v1/types/rights_request.go +++ b/pkg/server/api/mcp/v1/types/rights_request.go @@ -47,6 +47,7 @@ func NewListRightsRequestsOutput(rightsRequestPage *page.Page[*coredata.RightsRe } var nextCursor *page.CursorKey + if len(rightsRequestPage.Data) > 0 { cursorKey := rightsRequestPage.Data[len(rightsRequestPage.Data)-1].CursorKey(rightsRequestPage.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/risk.go b/pkg/server/api/mcp/v1/types/risk.go index 44189b0f9..0c5370d95 100644 --- a/pkg/server/api/mcp/v1/types/risk.go +++ b/pkg/server/api/mcp/v1/types/risk.go @@ -47,6 +47,7 @@ func NewListMeasureRisksOutput(riskPage *page.Page[*coredata.Risk, coredata.Risk } var nextCursor *page.CursorKey + if len(riskPage.Data) > 0 { cursorKey := riskPage.Data[len(riskPage.Data)-1].CursorKey(riskPage.Cursor.OrderBy.Field) nextCursor = &cursorKey @@ -65,6 +66,7 @@ func NewListRisksOutput(riskPage *page.Page[*coredata.Risk, coredata.RiskOrderFi } var nextCursor *page.CursorKey + if len(riskPage.Data) > 0 { cursorKey := riskPage.Data[len(riskPage.Data)-1].CursorKey(riskPage.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/scim_configuration.go b/pkg/server/api/mcp/v1/types/scim_configuration.go index cec6ffdd2..2c34da421 100644 --- a/pkg/server/api/mcp/v1/types/scim_configuration.go +++ b/pkg/server/api/mcp/v1/types/scim_configuration.go @@ -68,6 +68,7 @@ func NewListSCIMEventsOutput(p *page.Page[*coredata.SCIMEvent, coredata.SCIMEven } var nextCursor *page.CursorKey + if len(p.Data) > 0 { cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/statement_of_applicability.go b/pkg/server/api/mcp/v1/types/statement_of_applicability.go index 958c33e49..137af79f0 100644 --- a/pkg/server/api/mcp/v1/types/statement_of_applicability.go +++ b/pkg/server/api/mcp/v1/types/statement_of_applicability.go @@ -34,11 +34,14 @@ func NewListStatementsOfApplicabilityOutput(pg *page.Page[*coredata.StatementOfA for _, v := range pg.Data { items = append(items, NewStatementOfApplicability(v)) } + var nextCursor *page.CursorKey + if len(pg.Data) > 0 { cursorKey := pg.Data[len(pg.Data)-1].CursorKey(pg.Cursor.OrderBy.Field) nextCursor = &cursorKey } + return ListStatementsOfApplicabilityOutput{ NextCursor: nextCursor, StatementsOfApplicability: items, @@ -63,11 +66,14 @@ func NewListApplicabilityStatementsOutput(pg *page.Page[*coredata.ApplicabilityS for _, v := range pg.Data { items = append(items, NewApplicabilityStatement(v)) } + var nextCursor *page.CursorKey + if len(pg.Data) > 0 { cursorKey := pg.Data[len(pg.Data)-1].CursorKey(pg.Cursor.OrderBy.Field) nextCursor = &cursorKey } + return ListApplicabilityStatementsOutput{ NextCursor: nextCursor, ApplicabilityStatements: items, diff --git a/pkg/server/api/mcp/v1/types/task.go b/pkg/server/api/mcp/v1/types/task.go index 2b4fe1763..148de56ab 100644 --- a/pkg/server/api/mcp/v1/types/task.go +++ b/pkg/server/api/mcp/v1/types/task.go @@ -44,6 +44,7 @@ func NewListMeasureTasksOutput(taskPage *page.Page[*coredata.Task, coredata.Task } var nextCursor *page.CursorKey + if len(taskPage.Data) > 0 { cursorKey := taskPage.Data[len(taskPage.Data)-1].CursorKey(taskPage.Cursor.OrderBy.Field) nextCursor = &cursorKey @@ -62,6 +63,7 @@ func NewListTasksOutput(taskPage *page.Page[*coredata.Task, coredata.TaskOrderFi } var nextCursor *page.CursorKey + if len(taskPage.Data) > 0 { cursorKey := taskPage.Data[len(taskPage.Data)-1].CursorKey(taskPage.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/third_party.go b/pkg/server/api/mcp/v1/types/third_party.go index e1e7a77c1..83e4862b3 100644 --- a/pkg/server/api/mcp/v1/types/third_party.go +++ b/pkg/server/api/mcp/v1/types/third_party.go @@ -41,6 +41,7 @@ func NewListThirdPartyRiskAssessmentsOutput(p *page.Page[*coredata.ThirdPartyRis } var nextCursor *page.CursorKey + if len(p.Data) > 0 { cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) nextCursor = &cursorKey @@ -98,6 +99,7 @@ func NewListThirdPartiesOutput(thirdPartyPage *page.Page[*coredata.ThirdParty, c } var nextCursor *page.CursorKey + if len(thirdPartyPage.Data) > 0 { cursorKey := thirdPartyPage.Data[len(thirdPartyPage.Data)-1].CursorKey(thirdPartyPage.Cursor.OrderBy.Field) nextCursor = &cursorKey @@ -161,6 +163,7 @@ func NewListThirdPartyContactsOutput(p *page.Page[*coredata.ThirdPartyContact, c } var nextCursor *page.CursorKey + if len(p.Data) > 0 { cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) nextCursor = &cursorKey @@ -195,6 +198,7 @@ func NewListThirdPartyServicesOutput(p *page.Page[*coredata.ThirdPartyService, c } var nextCursor *page.CursorKey + if len(p.Data) > 0 { cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) nextCursor = &cursorKey @@ -215,6 +219,7 @@ func NewThirdPartySubprocessors(sps []probo.Subprocessor) []*ThirdPartySubproces Purpose: sp.Purpose, } } + return result } diff --git a/pkg/server/api/mcp/v1/types/tracker_pattern.go b/pkg/server/api/mcp/v1/types/tracker_pattern.go index a7baf90f3..86115e41e 100644 --- a/pkg/server/api/mcp/v1/types/tracker_pattern.go +++ b/pkg/server/api/mcp/v1/types/tracker_pattern.go @@ -51,6 +51,7 @@ func NewListTrackerPatternsOutput(pg *page.Page[*coredata.TrackerPattern, coreda } var nextCursor *page.CursorKey + if len(pg.Data) > 0 { cursorKey := pg.Data[len(pg.Data)-1].CursorKey(pg.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/tracker_resource.go b/pkg/server/api/mcp/v1/types/tracker_resource.go index 8c7eec54b..ad407bc15 100644 --- a/pkg/server/api/mcp/v1/types/tracker_resource.go +++ b/pkg/server/api/mcp/v1/types/tracker_resource.go @@ -44,6 +44,7 @@ func NewListTrackerResourcesOutput(pg *page.Page[*coredata.TrackerResource, core } var nextCursor *page.CursorKey + if len(pg.Data) > 0 { cursorKey := pg.Data[len(pg.Data)-1].CursorKey(pg.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/transfer_impact_assessment.go b/pkg/server/api/mcp/v1/types/transfer_impact_assessment.go index 271843ee1..fb1b740a9 100644 --- a/pkg/server/api/mcp/v1/types/transfer_impact_assessment.go +++ b/pkg/server/api/mcp/v1/types/transfer_impact_assessment.go @@ -22,6 +22,7 @@ func NewTransferImpactAssessment(t *coredata.TransferImpactAssessment) *Transfer if t == nil { return nil } + return &TransferImpactAssessment{ ID: t.ID, OrganizationID: t.OrganizationID, @@ -41,11 +42,14 @@ func NewListTransferImpactAssessmentsOutput(pg *page.Page[*coredata.TransferImpa for _, v := range pg.Data { items = append(items, NewTransferImpactAssessment(v)) } + var nextCursor *page.CursorKey + if len(pg.Data) > 0 { cursorKey := pg.Data[len(pg.Data)-1].CursorKey(pg.Cursor.OrderBy.Field) nextCursor = &cursorKey } + return ListTransferImpactAssessmentsOutput{ NextCursor: nextCursor, TransferImpactAssessments: items, diff --git a/pkg/server/api/mcp/v1/types/trust_center.go b/pkg/server/api/mcp/v1/types/trust_center.go index 111e69d12..b823e123b 100644 --- a/pkg/server/api/mcp/v1/types/trust_center.go +++ b/pkg/server/api/mcp/v1/types/trust_center.go @@ -49,6 +49,7 @@ func NewListTrustCenterReferencesOutput(p *page.Page[*coredata.TrustCenterRefere } var nextCursor *page.CursorKey + if len(p.Data) > 0 { cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) nextCursor = &cursorKey @@ -75,6 +76,7 @@ func NewTrustCenterFile(f *coredata.TrustCenterFile, fileURL string) *TrustCente func NewListTrustCenterFilesOutput(files []*TrustCenterFile, p *page.Page[*coredata.TrustCenterFile, coredata.TrustCenterFileOrderField]) ListTrustCenterFilesOutput { var nextCursor *page.CursorKey + if len(p.Data) > 0 { cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/types/webhook_subscription.go b/pkg/server/api/mcp/v1/types/webhook_subscription.go index 0cd6fe44b..497f855a7 100644 --- a/pkg/server/api/mcp/v1/types/webhook_subscription.go +++ b/pkg/server/api/mcp/v1/types/webhook_subscription.go @@ -42,6 +42,7 @@ func NewListWebhookSubscriptionsOutput(p *page.Page[*coredata.WebhookSubscriptio } var nextCursor *page.CursorKey + if len(p.Data) > 0 { cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) nextCursor = &cursorKey @@ -55,6 +56,7 @@ func NewListWebhookSubscriptionsOutput(p *page.Page[*coredata.WebhookSubscriptio func NewWebhookEvent(e *coredata.WebhookEvent) *WebhookEvent { var response *string + if len(e.Response) > 0 && string(e.Response) != "null" { s := string(json.RawMessage(e.Response)) response = &s @@ -76,6 +78,7 @@ func NewListWebhookEventsOutput(p *page.Page[*coredata.WebhookEvent, coredata.We } var nextCursor *page.CursorKey + if len(p.Data) > 0 { cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) nextCursor = &cursorKey diff --git a/pkg/server/api/mcp/v1/v1_handler.go b/pkg/server/api/mcp/v1/v1_handler.go index 6ddc96143..8d4462842 100644 --- a/pkg/server/api/mcp/v1/v1_handler.go +++ b/pkg/server/api/mcp/v1/v1_handler.go @@ -80,6 +80,8 @@ func UnwrapOmittable[T any](field mcpgenmcp.Omittable[T]) *T { if !field.IsSet() { return nil } + value, _ := field.Value() + return &value } diff --git a/pkg/server/api/slack/v1/resolver.go b/pkg/server/api/slack/v1/resolver.go index 6de9e050c..b5718d684 100644 --- a/pkg/server/api/slack/v1/resolver.go +++ b/pkg/server/api/slack/v1/resolver.go @@ -27,6 +27,7 @@ func NewMux( trustSvc *trust.Service, ) *chi.Mux { r := chi.NewMux() + logger.Info("Registering Slack interactive endpoint") r.Post("/interactive", SlackHandler( diff --git a/pkg/server/api/slack/v1/slack_handler.go b/pkg/server/api/slack/v1/slack_handler.go index 2388b388e..0b7345fa0 100644 --- a/pkg/server/api/slack/v1/slack_handler.go +++ b/pkg/server/api/slack/v1/slack_handler.go @@ -70,6 +70,7 @@ func SlackHandler(slackSvc *slack.Service, slackSigningSecret string, logger *lo r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) timestamp := r.Header.Get("X-Slack-Request-Timestamp") + signature := r.Header.Get("X-Slack-Signature") if timestamp == "" || signature == "" { httpserver.RenderJSON(w, http.StatusBadRequest, SlackInteractiveResponse{Success: false, Message: "missing Slack signature headers"}) @@ -79,10 +80,12 @@ func SlackHandler(slackSvc *slack.Service, slackSigningSecret string, logger *lo if err := slack.VerifySignature(slackSigningSecret, timestamp, signature, bodyBytes); err != nil { logger.ErrorCtx(ctx, "invalid Slack signature", log.Error(err)) httpserver.RenderJSON(w, http.StatusUnauthorized, SlackInteractiveResponse{Success: false, Message: "invalid Slack signature"}) + return } var slackPayload SlackInteractivePayload + if ct := r.Header.Get("Content-Type"); ct != "application/x-www-form-urlencoded" { httpserver.RenderJSON(w, http.StatusBadRequest, SlackInteractiveResponse{Success: false, Message: "unsupported content type"}) return @@ -102,6 +105,7 @@ func SlackHandler(slackSvc *slack.Service, slackSigningSecret string, logger *lo if err := json.NewDecoder(strings.NewReader(raw)).Decode(&slackPayload); err != nil { logger.ErrorCtx(ctx, "cannot parse Slack payload", log.Error(err)) httpserver.RenderJSON(w, http.StatusBadRequest, SlackInteractiveResponse{Success: false, Message: "cannot parse Slack payload"}) + return } @@ -136,6 +140,7 @@ func SlackHandler(slackSvc *slack.Service, slackSigningSecret string, logger *lo if err != nil { logger.ErrorCtx(ctx, "cannot load slack message", log.Error(err)) httpserver.RenderJSON(w, http.StatusInternalServerError, SlackInteractiveResponse{Success: false, Message: "internal server error"}) + return } @@ -149,16 +154,21 @@ func SlackHandler(slackSvc *slack.Service, slackSigningSecret string, logger *lo if initialSlackMessage.RequesterEmail == nil { logger.ErrorCtx(ctx, "missing requester email", log.String("slack_message_id", initialSlackMessage.ID.String())) httpserver.RenderJSON(w, http.StatusInternalServerError, SlackInteractiveResponse{Success: false, Message: "internal server error"}) + return } + requesterEmail := *initialSlackMessage.RequesterEmail tenantSvc := trustSvc.WithTenant(initialSlackMessage.OrganizationID.TenantID()) - var documentIDs []gid.GID - var reportIDs []gid.GID - var fileIDs []gid.GID - var statusAction string + var ( + documentIDs []gid.GID + reportIDs []gid.GID + fileIDs []gid.GID + statusAction string + ) + tenantSlackSvc := slackSvc.WithTenant(initialSlackMessage.OrganizationID.TenantID()) // accept_all, reject_all @@ -173,6 +183,7 @@ func SlackHandler(slackSvc *slack.Service, slackSigningSecret string, logger *lo if err != nil { logger.ErrorCtx(ctx, "cannot load slack message document ids", log.Error(err)) httpserver.RenderJSON(w, http.StatusInternalServerError, SlackInteractiveResponse{Success: false, Message: "internal server error"}) + return } @@ -195,6 +206,7 @@ func SlackHandler(slackSvc *slack.Service, slackSigningSecret string, logger *lo } statusAction = params[0] + gID, err = gid.ParseGID(params[1]) if err != nil { httpserver.RenderJSON(w, http.StatusBadRequest, SlackInteractiveResponse{Success: false, Message: "invalid ID"}) @@ -225,6 +237,7 @@ func SlackHandler(slackSvc *slack.Service, slackSigningSecret string, logger *lo default: logger.ErrorCtx(ctx, "unknown entity type", log.Error(err)) httpserver.RenderJSON(w, http.StatusInternalServerError, SlackInteractiveResponse{Success: false, Message: "internal server error"}) + return } } @@ -241,6 +254,7 @@ func SlackHandler(slackSvc *slack.Service, slackSigningSecret string, logger *lo ); err != nil { logger.ErrorCtx(ctx, "cannot grant access", log.Error(err)) httpserver.RenderJSON(w, http.StatusInternalServerError, SlackInteractiveResponse{Success: false, Message: "internal server error"}) + return } case StatusReject: @@ -254,11 +268,13 @@ func SlackHandler(slackSvc *slack.Service, slackSigningSecret string, logger *lo ); err != nil { logger.ErrorCtx(ctx, "cannot reject access", log.Error(err)) httpserver.RenderJSON(w, http.StatusInternalServerError, SlackInteractiveResponse{Success: false, Message: "internal server error"}) + return } default: logger.ErrorCtx(ctx, "unknown status action", log.String("status_action", statusAction)) httpserver.RenderJSON(w, http.StatusInternalServerError, SlackInteractiveResponse{Success: false, Message: "internal server error"}) + return } @@ -270,6 +286,7 @@ func SlackHandler(slackSvc *slack.Service, slackSigningSecret string, logger *lo ); err != nil { logger.ErrorCtx(ctx, "cannot update Slack message", log.Error(err)) httpserver.RenderJSON(w, http.StatusInternalServerError, SlackInteractiveResponse{Success: false, Message: "internal server error"}) + return } diff --git a/pkg/server/api/trust/v1/auth_resolvers.go b/pkg/server/api/trust/v1/auth_resolvers.go index d675cd9f8..1fd89299e 100644 --- a/pkg/server/api/trust/v1/auth_resolvers.go +++ b/pkg/server/api/trust/v1/auth_resolvers.go @@ -69,6 +69,7 @@ func (r *mutationResolver) VerifyMagicLink(ctx context.Context, input types.Veri } r.logger.ErrorCtx(ctx, "cannot get magic link email", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -77,6 +78,7 @@ func (r *mutationResolver) VerifyMagicLink(ctx context.Context, input types.Veri switch { case session == nil: var err error + identity, session, continueURL, err = r.iam.AuthService.OpenSessionWithMagicLink(ctx, input.Token) if err != nil { var errExpiredToken *iam.ErrExpiredToken @@ -90,6 +92,7 @@ func (r *mutationResolver) VerifyMagicLink(ctx context.Context, input types.Veri } r.logger.ErrorCtx(ctx, "cannot open session with magic link", log.Error(err)) + return nil, gqlutils.Internal(ctx) } case identity.EmailAddress != email: @@ -99,6 +102,7 @@ func (r *mutationResolver) VerifyMagicLink(ctx context.Context, input types.Veri } var err error + identity, session, continueURL, err = r.iam.AuthService.OpenSessionWithMagicLink(ctx, input.Token) if err != nil { var errExpiredToken *iam.ErrExpiredToken @@ -112,6 +116,7 @@ func (r *mutationResolver) VerifyMagicLink(ctx context.Context, input types.Veri } r.logger.ErrorCtx(ctx, "cannot open session with magic link", log.Error(err)) + return nil, gqlutils.Internal(ctx) } } diff --git a/pkg/server/api/trust/v1/base_resolvers.go b/pkg/server/api/trust/v1/base_resolvers.go index b352b52f3..c3c9c34f6 100644 --- a/pkg/server/api/trust/v1/base_resolvers.go +++ b/pkg/server/api/trust/v1/base_resolvers.go @@ -50,6 +50,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err)) return nil, gqlutils.Internal(ctx) } + return types.NewOrganization(organization), nil case coredata.DocumentEntityType: @@ -60,12 +61,16 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if errors.Is(err, trust.ErrDocumentNotFound) || errors.Is(err, trust.ErrDocumentNotVisible) || errors.Is(err, coredata.ErrResourceNotFound) { return nil, gqlutils.NotFoundf(ctx, "node %q not found", id) } + if _, ok := errors.AsType[*trust.ErrDocumentArchived](err); ok { return nil, gqlutils.NotFoundf(ctx, "node %q not found", id) } + r.logger.ErrorCtx(ctx, "cannot get document", log.Error(err)) + return nil, gqlutils.Internal(ctx) } + return types.NewDocument(document), nil case coredata.FrameworkEntityType: @@ -74,6 +79,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error r.logger.ErrorCtx(ctx, "cannot get framework", log.Error(err)) return nil, gqlutils.Internal(ctx) } + return types.NewFramework(framework), nil case coredata.ReportEntityType: @@ -84,9 +90,12 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if errors.Is(err, trust.ErrReportNotFound) || errors.Is(err, coredata.ErrResourceNotFound) { return nil, gqlutils.NotFoundf(ctx, "node %q not found", id) } + r.logger.ErrorCtx(ctx, "cannot get report", log.Error(err)) + return nil, gqlutils.Internal(ctx) } + return types.NewReport(report), nil case coredata.AuditEntityType: @@ -95,6 +104,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error r.logger.ErrorCtx(ctx, "cannot get audit", log.Error(err)) return nil, gqlutils.Internal(ctx) } + return types.NewAudit(audit), nil case coredata.ThirdPartyEntityType: @@ -103,6 +113,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error r.logger.ErrorCtx(ctx, "cannot get thirdParty", log.Error(err)) return nil, gqlutils.Internal(ctx) } + return types.NewSubprocessor(thirdParty), nil case coredata.TrustCenterEntityType: @@ -111,6 +122,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error r.logger.ErrorCtx(ctx, "cannot get trust center", log.Error(err)) return nil, gqlutils.Internal(ctx) } + return types.NewTrustCenter(trustCenter), nil case coredata.TrustCenterReferenceEntityType: @@ -119,6 +131,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error r.logger.ErrorCtx(ctx, "cannot get trust center reference", log.Error(err)) return nil, gqlutils.Internal(ctx) } + return types.NewTrustCenterReference(reference), nil case coredata.TrustCenterFileEntityType: @@ -129,7 +142,9 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error if errors.Is(err, trust.ErrTrustCenterFileNotFound) || errors.Is(err, trust.ErrTrustCenterFileNotVisible) { return nil, gqlutils.NotFoundf(ctx, "node %q not found", id) } + r.logger.ErrorCtx(ctx, "cannot get trust center file", log.Error(err)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/trust/v1/mailing_list_resolvers.go b/pkg/server/api/trust/v1/mailing_list_resolvers.go index 9331c61c7..b1799df6e 100644 --- a/pkg/server/api/trust/v1/mailing_list_resolvers.go +++ b/pkg/server/api/trust/v1/mailing_list_resolvers.go @@ -39,10 +39,13 @@ func (r *mutationResolver) SubscribeToMailingList(ctx context.Context) (*types.S if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) } + if errors.Is(err, mailman.ErrSubscriberAlreadyExist) { return nil, gqlutils.Conflictf(ctx, "already subscribed to this mailing list") } + r.logger.ErrorCtx(ctx, "cannot subscribe to mailing list", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -65,6 +68,7 @@ func (r *mutationResolver) UnsubscribeFromMailingList(ctx context.Context) (*typ r.logger.ErrorCtx(ctx, "cannot get mailing list subscription", log.Error(err)) return nil, gqlutils.Internal(ctx) } + if subscriber == nil { return nil, gqlutils.NotFoundf(ctx, "not subscribed to this mailing list") } @@ -73,7 +77,9 @@ func (r *mutationResolver) UnsubscribeFromMailingList(ctx context.Context) (*typ if errors.Is(err, mailman.ErrSubscriberNotFound) { return nil, gqlutils.NotFoundf(ctx, "not subscribed to this mailing list") } + r.logger.ErrorCtx(ctx, "cannot unsubscribe from mailing list", log.Error(err)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/trust/v1/resolver.go b/pkg/server/api/trust/v1/resolver.go index b276b6355..06c18a2ea 100644 --- a/pkg/server/api/trust/v1/resolver.go +++ b/pkg/server/api/trust/v1/resolver.go @@ -97,6 +97,7 @@ func NewMux( r.Method(http.MethodGet, "/session-transfer", sessionTransferHandler) graphqlHandler := NewGraphQLHandler(iamSvc, trustSvc, esignSvc, mailmanSvc, logger, baseURL, cookieConfig, tokenSecret) + r.Group( func(r chi.Router) { r.Use(authn.NewSessionMiddleware(iamSvc, cookieConfig)) diff --git a/pkg/server/api/trust/v1/session_transfer_handler.go b/pkg/server/api/trust/v1/session_transfer_handler.go index d96e533dd..aece1f197 100644 --- a/pkg/server/api/trust/v1/session_transfer_handler.go +++ b/pkg/server/api/trust/v1/session_transfer_handler.go @@ -63,6 +63,7 @@ func (h *SessionTransferHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques if err != nil { h.logger.WarnCtx(ctx, "invalid session transfer token", log.Error(err)) httpserver.RenderError(w, http.StatusBadRequest, errors.New("invalid or expired token")) + return } @@ -81,6 +82,7 @@ func (h *SessionTransferHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques if err != nil { h.logger.ErrorCtx(ctx, "cannot get session for transfer", log.Error(err)) httpserver.RenderError(w, http.StatusBadRequest, errors.New("invalid or expired token")) + return } diff --git a/pkg/server/api/trust/v1/trust_center_resolvers.go b/pkg/server/api/trust/v1/trust_center_resolvers.go index f6d9a503e..5347d3868 100644 --- a/pkg/server/api/trust/v1/trust_center_resolvers.go +++ b/pkg/server/api/trust/v1/trust_center_resolvers.go @@ -91,10 +91,13 @@ func (r *documentResolver) IsUserAuthorized(ctx context.Context, obj *types.Docu if errors.Is(err, trust.ErrDocumentNotFound) || errors.Is(err, trust.ErrDocumentNotVisible) || errors.Is(err, coredata.ErrResourceNotFound) { return false, gqlutils.NotFoundf(ctx, "document %q not found", obj.ID) } + if _, ok := errors.AsType[*trust.ErrDocumentArchived](err); ok { return false, gqlutils.NotFoundf(ctx, "document %q not found", obj.ID) } + r.logger.ErrorCtx(ctx, "cannot load document", log.Error(err)) + return false, gqlutils.Internal(ctx) } @@ -122,6 +125,7 @@ func (r *documentResolver) IsUserAuthorized(ctx context.Context, obj *types.Docu } r.logger.ErrorCtx(ctx, "cannot check document access", log.Error(err)) + return false, gqlutils.Internal(ctx) } @@ -156,6 +160,7 @@ func (r *documentResolver) Access(ctx context.Context, obj *types.Document) (*ty } r.logger.ErrorCtx(ctx, "cannot get document access", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -222,10 +227,13 @@ func (r *mutationResolver) ExportDocumentPDF(ctx context.Context, input types.Ex if errors.Is(err, trust.ErrDocumentNotFound) || errors.Is(err, trust.ErrDocumentNotVisible) || errors.Is(err, coredata.ErrResourceNotFound) { return nil, gqlutils.NotFoundf(ctx, "document %q not found", input.DocumentID) } + if _, ok := errors.AsType[*trust.ErrDocumentArchived](err); ok { return nil, gqlutils.NotFoundf(ctx, "document %q not found", input.DocumentID) } + r.logger.ErrorCtx(ctx, "cannot load document", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -335,7 +343,9 @@ func (r *mutationResolver) ExportTrustCenterFile(ctx context.Context, input type if errors.Is(err, trust.ErrTrustCenterFileNotFound) || errors.Is(err, trust.ErrTrustCenterFileNotVisible) { return nil, gqlutils.NotFoundf(ctx, "trust center file %q not found", input.TrustCenterFileID) } + r.logger.ErrorCtx(ctx, "cannot load trust center file", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -390,12 +400,16 @@ func (r *mutationResolver) RequestDocumentAccess(ctx context.Context, input type if errors.Is(err, trust.ErrDocumentNotFound) || errors.Is(err, trust.ErrDocumentNotVisible) || errors.Is(err, coredata.ErrResourceNotFound) { return nil, gqlutils.NotFoundf(ctx, "document %q not found", input.DocumentID) } + if _, ok := errors.AsType[*trust.ErrDocumentArchived](err); ok { return nil, gqlutils.NotFoundf(ctx, "document %q not found", input.DocumentID) } + r.logger.ErrorCtx(ctx, "cannot load document", log.Error(err)) + return nil, gqlutils.Internal(ctx) } + if document.TrustCenterVisibility == coredata.TrustCenterVisibilityPublic { return nil, gqlutils.Invalidf( ctx, @@ -479,7 +493,9 @@ func (r *mutationResolver) RequestTrustCenterFileAccess(ctx context.Context, inp if errors.Is(err, trust.ErrTrustCenterFileNotFound) || errors.Is(err, trust.ErrTrustCenterFileNotVisible) { return nil, gqlutils.NotFoundf(ctx, "trust center file %q not found", input.TrustCenterFileID) } + r.logger.ErrorCtx(ctx, "cannot load trust center file", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -549,6 +565,7 @@ func (r *reportResolver) IsUserAuthorized(ctx context.Context, obj *types.Report } r.logger.ErrorCtx(ctx, "cannot check report access", log.Error(err)) + return false, gqlutils.Internal(ctx) } @@ -583,6 +600,7 @@ func (r *reportResolver) Access(ctx context.Context, obj *types.Report) (*types. } r.logger.ErrorCtx(ctx, "cannot get audit report access", log.Error(err)) + return nil, gqlutils.Internal(ctx) } @@ -603,10 +621,12 @@ func (r *subprocessorConnectionResolver) TotalCount(ctx context.Context, obj *ty r.logger.ErrorCtx(ctx, "cannot count subprocessors", log.Error(err)) return 0, gqlutils.Internal(ctx) } + return count, nil } r.logger.ErrorCtx(ctx, "not implemented: TotalCount for parent type") + return 0, gqlutils.Internal(ctx) } @@ -638,6 +658,7 @@ func (r *trustCenterResolver) NonDisclosureAgreement(ctx context.Context, obj *t r.logger.ErrorCtx(ctx, "cannot load NDA file", log.Error(err)) return nil, gqlutils.Internal(ctx) } + if file == nil { return nil, nil } @@ -767,6 +788,7 @@ func (r *trustCenterResolver) TrustCenterFiles(ctx context.Context, obj *types.T coredata.TrustCenterVisibilityPrivate, ), ) + trustCenterFilePage, err := trustService.TrustCenterFiles.ListForOrganizationId(ctx, obj.Organization.ID, cursor, filter) if err != nil { r.logger.ErrorCtx(ctx, "cannot list public trust center files", log.Error(err)) @@ -854,7 +876,9 @@ func (r *trustCenterFileResolver) IsUserAuthorized(ctx context.Context, obj *typ if errors.Is(err, trust.ErrTrustCenterFileNotFound) || errors.Is(err, trust.ErrTrustCenterFileNotVisible) { return false, gqlutils.NotFoundf(ctx, "trust center file %q not found", obj.ID) } + r.logger.ErrorCtx(ctx, "cannot load trust center file", log.Error(err)) + return false, gqlutils.Internal(ctx) } @@ -881,6 +905,7 @@ func (r *trustCenterFileResolver) IsUserAuthorized(ctx context.Context, obj *typ } r.logger.ErrorCtx(ctx, "cannot check trust center file access", log.Error(err)) + return false, gqlutils.Internal(ctx) } @@ -915,6 +940,7 @@ func (r *trustCenterFileResolver) Access(ctx context.Context, obj *types.TrustCe } r.logger.ErrorCtx(ctx, "cannot get file access", log.Error(err)) + return nil, gqlutils.Internal(ctx) } diff --git a/pkg/server/api/trust/v1/types/pageinfo.go b/pkg/server/api/trust/v1/types/pageinfo.go index 52593eaee..83e2009f6 100644 --- a/pkg/server/api/trust/v1/types/pageinfo.go +++ b/pkg/server/api/trust/v1/types/pageinfo.go @@ -21,6 +21,7 @@ import ( func NewPageInfo[T page.Paginable[O], O page.OrderField](p *page.Page[T, O]) *PageInfo { data := pageinfo.NewPageInfo(p) + return &PageInfo{ HasNextPage: data.HasNextPage, HasPreviousPage: data.HasPreviousPage, diff --git a/pkg/server/gqlutils/directives/session/session.go b/pkg/server/gqlutils/directives/session/session.go index bea101f58..1579fd0c9 100644 --- a/pkg/server/gqlutils/directives/session/session.go +++ b/pkg/server/gqlutils/directives/session/session.go @@ -45,6 +45,7 @@ func (e SessionRequirement) IsValid() bool { case SessionRequirementPresent, SessionRequirementNone, SessionRequirementOptional: return true } + return false } @@ -62,6 +63,7 @@ func (e *SessionRequirement) UnmarshalGQL(v any) error { if !e.IsValid() { return fmt.Errorf("%s is not a valid SessionRequirement", str) } + return nil } @@ -74,12 +76,14 @@ func (e *SessionRequirement) UnmarshalJSON(b []byte) error { if err != nil { return err } + return e.UnmarshalGQL(s) } func (e SessionRequirement) MarshalJSON() ([]byte, error) { var buf bytes.Buffer e.MarshalGQL(&buf) + return buf.Bytes(), nil } diff --git a/pkg/server/gqlutils/errors.go b/pkg/server/gqlutils/errors.go index 932a10961..64443bb78 100644 --- a/pkg/server/gqlutils/errors.go +++ b/pkg/server/gqlutils/errors.go @@ -154,6 +154,7 @@ func Invalid(ctx context.Context, err error) *gqlerror.Error { "value": errValidation.Value, } } + extensions := map[string]any{"code": "INVALID"} if details != nil { maps.Copy(extensions, details) @@ -175,6 +176,7 @@ func InvalidValidationErrors(ctx context.Context, errs validator.ValidationError for _, ve := range errs { gqlErrors = append(gqlErrors, Invalid(ctx, ve)) } + return gqlErrors } diff --git a/pkg/server/gqlutils/httpctx.go b/pkg/server/gqlutils/httpctx.go index e6b022687..5ff408ce6 100644 --- a/pkg/server/gqlutils/httpctx.go +++ b/pkg/server/gqlutils/httpctx.go @@ -29,7 +29,6 @@ var ( ) func WithHTTPContext(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context { - ctx = context.WithValue(ctx, httpResponseWriterKey, w) ctx = context.WithValue(ctx, httpRequestKey, r) diff --git a/pkg/server/gqlutils/tracing.go b/pkg/server/gqlutils/tracing.go index 551f2906b..3a5fa0db6 100644 --- a/pkg/server/gqlutils/tracing.go +++ b/pkg/server/gqlutils/tracing.go @@ -59,7 +59,6 @@ func (t TracingExtension) InterceptField(ctx context.Context, next graphql.Resol ) result, err := next(ctx) - if err != nil { span.RecordError(err) } @@ -76,10 +75,12 @@ func (t TracingExtension) InterceptOperation(ctx context.Context, next graphql.O rootSpan := trace.SpanFromContext(ctx) spanCtx := ctx + var operationSpan trace.Span if rootSpan.IsRecording() { tracer := otel.Tracer("graphql-operation") + operationName := "GraphQL Operation" if requestContext.OperationName != "" { operationName = "GraphQL " + requestContext.OperationName @@ -108,6 +109,7 @@ func (t TracingExtension) InterceptOperation(ctx context.Context, next graphql.O duration := time.Since(startTime) operationType := string(requestContext.Operation.Operation) + operationName := requestContext.OperationName if operationName == "" { operationName = "unnamed" diff --git a/pkg/server/gqlutils/types/bigint/bigint.go b/pkg/server/gqlutils/types/bigint/bigint.go index 236750ed6..e95508a56 100644 --- a/pkg/server/gqlutils/types/bigint/bigint.go +++ b/pkg/server/gqlutils/types/bigint/bigint.go @@ -37,6 +37,7 @@ func UnmarshalBigIntScalar(v any) (int64, error) { if err != nil { return 0, fmt.Errorf("invalid BigInt value: %v", err) } + return i, nil case int: return int64(val), nil @@ -48,11 +49,13 @@ func UnmarshalBigIntScalar(v any) (int64, error) { if val != float32(int64(val)) { return 0, fmt.Errorf("BigInt cannot represent non-integer value: %v", val) } + return int64(val), nil case float64: if val != float64(int64(val)) { return 0, fmt.Errorf("BigInt cannot represent non-integer value: %v", val) } + return int64(val), nil default: return 0, fmt.Errorf("cannot unmarshal %T into BigInt", v) diff --git a/pkg/server/mailactions/confirm.go b/pkg/server/mailactions/confirm.go index c66e21903..287c6569d 100644 --- a/pkg/server/mailactions/confirm.go +++ b/pkg/server/mailactions/confirm.go @@ -36,6 +36,7 @@ func confirmGetHandler() http.HandlerFunc { Body: "This confirmation link is missing required information. Please use the link from your email.", }, ) + return } @@ -72,6 +73,7 @@ func confirmPostHandler(mailmanSvc *mailman.Service, tokenSecret string) http.Ha Body: "This confirmation link is missing required information. Please use the link from your email.", }, ) + return } @@ -86,6 +88,7 @@ func confirmPostHandler(mailmanSvc *mailman.Service, tokenSecret string) http.Ha Body: "This confirmation link is invalid or has expired. Confirmation links are valid for 30 days — please re-subscribe to get a new one.", }, ) + return } @@ -99,6 +102,7 @@ func confirmPostHandler(mailmanSvc *mailman.Service, tokenSecret string) http.Ha Body: "We could not find your subscription. It may have already been cancelled or this link was already used.", }, ) + return } @@ -111,6 +115,7 @@ func confirmPostHandler(mailmanSvc *mailman.Service, tokenSecret string) http.Ha Body: "We could not confirm your subscription. Please try again later.", }, ) + return } diff --git a/pkg/server/mailactions/unsubscribe.go b/pkg/server/mailactions/unsubscribe.go index 5750cea5f..3bdd466f9 100644 --- a/pkg/server/mailactions/unsubscribe.go +++ b/pkg/server/mailactions/unsubscribe.go @@ -36,6 +36,7 @@ func unsubscribeGetHandler() http.HandlerFunc { Body: "This unsubscribe link is missing required information. Please use the link from your email.", }, ) + return } @@ -69,6 +70,7 @@ func unsubscribePostHandler(mailmanSvc *mailman.Service, tokenSecret string) htt Body: "This unsubscribe link is missing required information. Please use the link from your email.", }, ) + return } @@ -83,6 +85,7 @@ func unsubscribePostHandler(mailmanSvc *mailman.Service, tokenSecret string) htt Body: "This unsubscribe link is invalid or has expired.", }, ) + return } @@ -97,6 +100,7 @@ func unsubscribePostHandler(mailmanSvc *mailman.Service, tokenSecret string) htt Body: "We could not process your request. Please try again later.", }, ) + return } } diff --git a/pkg/server/server.go b/pkg/server/server.go index 30469da10..62e6aa89f 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -188,6 +188,7 @@ func (s *Server) oidcDiscoveryHandler(w http.ResponseWriter, r *http.Request) { } metadata := s.iamService.OAuth2ServerService.Metadata(endpoints) + w.Header().Set("Cache-Control", "public, max-age=3600") httpserver.RenderJSON(w, http.StatusOK, metadata) } @@ -204,6 +205,7 @@ func (s *Server) stripTrustPrefix(next http.Handler) http.Handler { if r.URL.Path == prefix { cleanPath := path.Clean(prefix) + "/" http.Redirect(w, r, cleanPath, http.StatusMovedPermanently) + return } diff --git a/pkg/server/statichandler/statichandler.go b/pkg/server/statichandler/statichandler.go index 048d6d660..57f7bea85 100644 --- a/pkg/server/statichandler/statichandler.go +++ b/pkg/server/statichandler/statichandler.go @@ -86,10 +86,12 @@ func NewServer(staticFiles fs.FS, distPath string, gzipOptions GzipOptions, opts } content := make([]byte, info.Size()) + file, err := subFS.Open(path) if err != nil { return err } + defer func() { _ = file.Close() }() _, err = file.Read(content) @@ -104,7 +106,6 @@ func NewServer(staticFiles fs.FS, distPath string, gzipOptions GzipOptions, opts return nil }, ) - if err != nil { return nil, fmt.Errorf("cannot generate etags: %w", err) } @@ -153,6 +154,7 @@ func (s *Server) serveIndex(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write(buf.Bytes()) + return } @@ -200,6 +202,7 @@ func (s *Server) ServeSPA(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") w.WriteHeader(http.StatusOK) _, _ = w.Write(buf.Bytes()) + return } @@ -262,10 +265,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { if s.shouldCompressWithGzip(r) { w.Header().Set("Content-Encoding", "gzip") gz := gzip.NewWriter(w) + defer func() { _ = gz.Close() }() gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w} s.ServeSPA(gzw, r) + return } diff --git a/pkg/server/trustedproxy/trustedproxy.go b/pkg/server/trustedproxy/trustedproxy.go index 3fe2b0f88..5b4dde84b 100644 --- a/pkg/server/trustedproxy/trustedproxy.go +++ b/pkg/server/trustedproxy/trustedproxy.go @@ -46,6 +46,7 @@ func NewMiddleware(trusted []string) (func(http.Handler) http.Handler, error) { r.Header.Del(h) } } + next.ServeHTTP(w, r) }) }, nil @@ -53,6 +54,7 @@ func NewMiddleware(trusted []string) (func(http.Handler) http.Handler, error) { func parseTrusted(trusted []string) ([]net.IP, []*net.IPNet, error) { ips := make([]net.IP, 0, len(trusted)) + nets := make([]*net.IPNet, 0, len(trusted)) for _, entry := range trusted { if strings.Contains(entry, "/") { @@ -60,7 +62,9 @@ func parseTrusted(trusted []string) ([]net.IP, []*net.IPNet, error) { if err != nil { return nil, nil, fmt.Errorf("cannot parse CIDR %q: %w", entry, err) } + nets = append(nets, ipNet) + continue } @@ -68,8 +72,10 @@ func parseTrusted(trusted []string) ([]net.IP, []*net.IPNet, error) { if ip == nil { return nil, nil, fmt.Errorf("cannot parse IP address %q", entry) } + ips = append(ips, ip) } + return ips, nets, nil } diff --git a/pkg/server/trustedproxy/trustedproxy_test.go b/pkg/server/trustedproxy/trustedproxy_test.go index 42f914ed4..b046797c0 100644 --- a/pkg/server/trustedproxy/trustedproxy_test.go +++ b/pkg/server/trustedproxy/trustedproxy_test.go @@ -31,18 +31,22 @@ func runMiddleware(t *testing.T, trusted []string, remoteAddr string, headers ma require.NoError(t, err) var captured *http.Request + handler := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { captured = r })) req := httptest.NewRequest(http.MethodGet, "/", nil) + req.RemoteAddr = remoteAddr for k, v := range headers { req.Header.Set(k, v) } + handler.ServeHTTP(httptest.NewRecorder(), req) require.NotNil(t, captured) + return captured } diff --git a/pkg/slack/client.go b/pkg/slack/client.go index 4c581e44f..fcb8c3fd4 100644 --- a/pkg/slack/client.go +++ b/pkg/slack/client.go @@ -87,6 +87,7 @@ func (c *Client) CreateMessage(ctx context.Context, accessToken string, channelI if err != nil { return nil, fmt.Errorf("cannot send request: %w", err) } + defer func() { _ = resp.Body.Close() }() responseBody, err := io.ReadAll(resp.Body) @@ -138,6 +139,7 @@ func (c *Client) UpdateInteractiveMessage(ctx context.Context, responseURL strin if err != nil { return fmt.Errorf("cannot send interactive message update request: %w", err) } + defer func() { _ = resp.Body.Close() }() responseBody, err := io.ReadAll(resp.Body) @@ -160,6 +162,7 @@ func (c *Client) UpdateInteractiveMessage(ctx context.Context, responseURL strin if slackResponse.OK { return nil } + if slackResponse.Error != "" { return fmt.Errorf("slack error: %s", slackResponse.Error) } @@ -193,6 +196,7 @@ func (c *Client) UpdateMessage(ctx context.Context, accessToken string, channelI if err != nil { return fmt.Errorf("cannot send request: %w", err) } + defer func() { _ = resp.Body.Close() }() responseBody, err := io.ReadAll(resp.Body) @@ -238,6 +242,7 @@ func (c *Client) JoinChannel(ctx context.Context, accessToken string, channelID if err != nil { return fmt.Errorf("cannot send request: %w", err) } + defer func() { _ = resp.Body.Close() }() responseBody, err := io.ReadAll(resp.Body) diff --git a/pkg/slack/sender.go b/pkg/slack/sender.go index 2fb257c3f..c42730b81 100644 --- a/pkg/slack/sender.go +++ b/pkg/slack/sender.go @@ -87,6 +87,7 @@ func (s *Sender) batchSendMessages(ctx context.Context) error { } s.logger.ErrorCtx(ctx, "panic while sending slack message", log.String("error", panicErr), log.String("message_id", message.ID.String())) + err = fmt.Errorf("panic recovered: %v", r) } }() @@ -121,6 +122,7 @@ func (s *Sender) batchSendMessages(ctx context.Context) error { } s.logger.ErrorCtx(ctx, "error sending slack message", log.Error(sendErr), log.String("message_id", message.ID.String())) + return nil } @@ -160,6 +162,7 @@ func (s *Sender) sendMessage(ctx context.Context, tx pg.Querier, message *coreda if errors.Is(err, coredata.ErrResourceNotFound) { return nil, nil, fmt.Errorf("cannot send slack message: no connector configured for organization") } + return nil, nil, fmt.Errorf("cannot send slack message: %w", err) } @@ -216,6 +219,7 @@ func (s *Sender) batchUpdateMessages(ctx context.Context) error { } s.logger.ErrorCtx(ctx, "panic while updating slack message", log.String("error", panicErr), log.String("message_id", updateMessage.ID.String())) + err = fmt.Errorf("panic recovered: %v", r) } }() @@ -241,6 +245,7 @@ func (s *Sender) batchUpdateMessages(ctx context.Context) error { } s.logger.ErrorCtx(ctx, "error updating slack message", log.Error(updateErr), log.String("message_id", updateMessage.ID.String())) + return nil } @@ -284,6 +289,7 @@ func (s *Sender) updateMessage(ctx context.Context, tx pg.Querier, updateMessage if errors.Is(err, coredata.ErrResourceNotFound) { return fmt.Errorf("cannot update slack message: no connector configured for organization") } + return fmt.Errorf("cannot update slack message: %w", err) } diff --git a/pkg/slack/service.go b/pkg/slack/service.go index 367db715d..e87cc992b 100644 --- a/pkg/slack/service.go +++ b/pkg/slack/service.go @@ -96,7 +96,6 @@ func (s *Service) GetInitialSlackMessageByChannelAndTS( return nil }) - if err != nil { return nil, err } diff --git a/pkg/slack/signature.go b/pkg/slack/signature.go index aa3fa1783..6966b4951 100644 --- a/pkg/slack/signature.go +++ b/pkg/slack/signature.go @@ -52,5 +52,6 @@ func abs(n int64) int64 { if n < 0 { return -n } + return n } diff --git a/pkg/slack/slack_message_service.go b/pkg/slack/slack_message_service.go index f4d6ec0ba..71089a04c 100644 --- a/pkg/slack/slack_message_service.go +++ b/pkg/slack/slack_message_service.go @@ -90,7 +90,6 @@ func (s *SlackMessageService) GetSlackMessageDocumentIDs( return nil }) - if err != nil { return nil, nil, nil, err } @@ -113,6 +112,7 @@ func (s *SlackMessageService) UpdateSlackAccessMessage( if err := slackMessage.LoadById(ctx, tx, s.svc.scope, slackMessageID); err != nil { return fmt.Errorf("cannot load slack message: %w", err) } + var trustCenter coredata.TrustCenter if err := trustCenter.LoadByOrganizationID(ctx, tx, s.svc.scope, slackMessage.OrganizationID); err != nil { return fmt.Errorf("cannot load trust center: %w", err) @@ -218,6 +218,7 @@ func (s *SlackMessageService) QueueSlackNotification( } hasSlackConnector := false + for _, connector := range connectors { if connector.Provider == coredata.ConnectorProviderSlack { hasSlackConnector = true @@ -270,6 +271,7 @@ func (s *SlackMessageService) QueueSlackNotification( sevenDaysAgo := now.Add(-slackMessageDeduplicationWindow) var existingMessage coredata.SlackMessage + err = existingMessage.LoadLatestByRequesterEmailAndType( ctx, tx, @@ -290,6 +292,7 @@ func (s *SlackMessageService) QueueSlackNotification( return nil } + var notFoundErr coredata.ErrSlackMessageNotFound if !errors.Is(err, notFoundErr) { return fmt.Errorf("cannot load existing slack message: %w", err) @@ -329,6 +332,7 @@ func (s *SlackMessageService) loadDocumentsReportsAndFilesFromAccesses( if err := doc.LoadByID(ctx, conn, s.svc.scope, *access.DocumentID); err != nil { return nil, nil, nil, fmt.Errorf("cannot load document: %w", err) } + documents = append(documents, SlackMessageDocument{ ID: access.DocumentID.String(), Title: doc.Title, @@ -356,6 +360,7 @@ func (s *SlackMessageService) loadDocumentsReportsAndFilesFromAccesses( if audit.Name != nil && *audit.Name != "" { label = label + " - " + *audit.Name } + reports = append(reports, SlackMessageReport{ ID: access.ReportID.String(), Title: label, @@ -369,6 +374,7 @@ func (s *SlackMessageService) loadDocumentsReportsAndFilesFromAccesses( if err := file.LoadByID(ctx, conn, s.svc.scope, *access.TrustCenterFileID); err != nil { return nil, nil, nil, fmt.Errorf("cannot load trust center file: %w", err) } + files = append(files, SlackMessageFile{ ID: access.TrustCenterFileID.String(), Name: file.Name, @@ -441,14 +447,17 @@ func extractIDsFromMetadata(metadata map[string]any, fieldName string) []gid.GID if !ok { continue } + idStr, ok := item["ID"].(string) if !ok { continue } + id, err := gid.ParseGID(idStr) if err != nil { continue } + ids = append(ids, id) } diff --git a/pkg/trust/audit_service.go b/pkg/trust/audit_service.go index 79f44d88a..319958b72 100644 --- a/pkg/trust/audit_service.go +++ b/pkg/trust/audit_service.go @@ -45,7 +45,6 @@ func (s AuditService) Get( return nil }, ) - if err != nil { return nil, err } @@ -70,7 +69,6 @@ func (s AuditService) GetByReportID( return nil }, ) - if err != nil { return nil, err } @@ -89,6 +87,7 @@ func (s AuditService) ListForOrganizationId( ctx, func(ctx context.Context, conn pg.Querier) error { filter := coredata.NewAuditTrustCenterFilter() + err := audits.LoadByOrganizationID(ctx, conn, s.svc.scope, organizationID, cursor, filter) if err != nil { return fmt.Errorf("cannot load audits: %w", err) @@ -97,7 +96,6 @@ func (s AuditService) ListForOrganizationId( return nil }, ) - if err != nil { return nil, err } diff --git a/pkg/trust/compliance_external_url_service.go b/pkg/trust/compliance_external_url_service.go index 85e049e14..81e81cea3 100644 --- a/pkg/trust/compliance_external_url_service.go +++ b/pkg/trust/compliance_external_url_service.go @@ -46,7 +46,6 @@ func (s ComplianceExternalURLService) ListForTrustCenterID( return nil }, ) - if err != nil { return nil, err } diff --git a/pkg/trust/compliance_framework_service.go b/pkg/trust/compliance_framework_service.go index d37264aa8..f155b57c2 100644 --- a/pkg/trust/compliance_framework_service.go +++ b/pkg/trust/compliance_framework_service.go @@ -46,7 +46,6 @@ func (s ComplianceFrameworkService) ListByTrustCenterID( return nil }, ) - if err != nil { return nil, err } diff --git a/pkg/trust/compliance_page_service.go b/pkg/trust/compliance_page_service.go index e6e5bea70..81e62faac 100644 --- a/pkg/trust/compliance_page_service.go +++ b/pkg/trust/compliance_page_service.go @@ -44,6 +44,7 @@ var complianceTmpl = template.Must( s = strings.ReplaceAll(s, `|`, `\|`) s = strings.ReplaceAll(s, "\n", " ") s = strings.ReplaceAll(s, "\r", "") + return s }, }). @@ -136,9 +137,11 @@ func (s *Service) RenderCompliancePageMarkdown( if org.WebsiteURL != nil && *org.WebsiteURL != "" { data.Details = append(data.Details, compliancePageDetail{Label: "Website", Value: *org.WebsiteURL}) } + if org.Email != nil && *org.Email != "" { data.Details = append(data.Details, compliancePageDetail{Label: "Email", Value: *org.Email}) } + if org.HeadquarterAddress != nil && *org.HeadquarterAddress != "" { data.Details = append(data.Details, compliancePageDetail{Label: "Headquarters", Value: *org.HeadquarterAddress}) } @@ -264,6 +267,7 @@ func (s *Service) fetchDocumentIDs(ctx context.Context, tenantSvc *TenantService if doc.TrustCenterVisibility == coredata.TrustCenterVisibilityNone { continue } + ids = append(ids, doc.ID.String()) } @@ -313,6 +317,7 @@ func (s *Service) fetchComplianceFrameworks(ctx context.Context, tenantSvc *Tena if fw.Description != nil { fi.Description = *fw.Description } + frameworks = append(frameworks, fi) } @@ -352,6 +357,7 @@ func (s *Service) fetchDocuments(ctx context.Context, tenantSvc *TenantService, if doc.TrustCenterVisibility == coredata.TrustCenterVisibilityNone { continue } + docs = append( docs, compliancePageDocument{ @@ -399,6 +405,7 @@ func (s *Service) fetchAudits(ctx context.Context, tenantSvc *TenantService, org } frameworkName := "" + fw, err := tenantSvc.Frameworks.Get(ctx, audit.FrameworkID) if err == nil { frameworkName = fw.Name @@ -411,9 +418,11 @@ func (s *Service) fetchAudits(ctx context.Context, tenantSvc *TenantService, org if audit.ValidFrom != nil { ai.ValidFrom = audit.ValidFrom.Format("2006-01-02") } + if audit.ValidUntil != nil { ai.ValidUntil = audit.ValidUntil.Format("2006-01-02") } + audits = append(audits, ai) } @@ -506,6 +515,7 @@ func (s *Service) fetchReferences(ctx context.Context, tenantSvc *TenantService, if r.Description != nil { ri.Description = *r.Description } + refs = append(refs, ri) } diff --git a/pkg/trust/document_service.go b/pkg/trust/document_service.go index 1f7728516..42fa80087 100644 --- a/pkg/trust/document_service.go +++ b/pkg/trust/document_service.go @@ -63,7 +63,6 @@ func (s *DocumentService) ListForOrganizationId( return nil }, ) - if err != nil { return nil, err } @@ -118,7 +117,6 @@ func (s DocumentService) Get( return nil }, ) - if err != nil { return nil, err } @@ -172,7 +170,6 @@ func (s *DocumentService) exportPDFData( return nil }, ) - if err != nil { return nil, err } @@ -204,6 +201,7 @@ func (s *DocumentService) generatePDFOnTheFly( version *coredata.DocumentVersion, ) ([]byte, error) { organization := &coredata.Organization{} + var approverNames []string err := s.svc.pg.WithConn( @@ -216,6 +214,7 @@ func (s *DocumentService) generatePDFOnTheFly( } } else if lastQuorum.Status == coredata.DocumentVersionApprovalQuorumStatusApproved { approvedDecisions := &coredata.DocumentVersionApprovalDecisions{} + approvedFilter := coredata.NewDocumentVersionApprovalDecisionFilter( coredata.DocumentVersionApprovalDecisionStates{coredata.DocumentVersionApprovalDecisionStateApproved}, ) @@ -262,12 +261,12 @@ func (s *DocumentService) generatePDFOnTheFly( return nil }, ) - if err != nil { return nil, err } classification := docgen.ClassificationSecret + switch version.Classification { case coredata.DocumentClassificationPublic: classification = docgen.ClassificationPublic @@ -278,8 +277,10 @@ func (s *DocumentService) generatePDFOnTheFly( } horizontalLogoBase64 := "" + if organization.HorizontalLogoFileID != nil { fileRecord := &coredata.File{} + fileErr := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { return fileRecord.LoadByID(ctx, conn, s.svc.scope, *organization.HorizontalLogoFileID) }) diff --git a/pkg/trust/framework_service.go b/pkg/trust/framework_service.go index 2b89d1cb3..fd2ce503d 100644 --- a/pkg/trust/framework_service.go +++ b/pkg/trust/framework_service.go @@ -43,7 +43,6 @@ func (s FrameworkService) Get( return nil }) - if err != nil { return nil, err } diff --git a/pkg/trust/organization_service.go b/pkg/trust/organization_service.go index ccd453462..106044edf 100644 --- a/pkg/trust/organization_service.go +++ b/pkg/trust/organization_service.go @@ -52,7 +52,6 @@ func (s OrganizationService) Get( return nil }, ) - if err != nil { return nil, err } @@ -86,7 +85,6 @@ func (s OrganizationService) GetOrganizationCustomDomain( return nil }, ) - if err != nil { return nil, err } @@ -109,6 +107,7 @@ func (s OrganizationService) GenerateLogoURL( } file := &coredata.File{} + err = s.svc.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { diff --git a/pkg/trust/report_service.go b/pkg/trust/report_service.go index b4af57c0c..f21c01fb1 100644 --- a/pkg/trust/report_service.go +++ b/pkg/trust/report_service.go @@ -66,7 +66,6 @@ func (s ReportService) loadByID( return nil }, ) - if err != nil { return nil, err } @@ -143,6 +142,7 @@ func (s ReportService) exportPDFData( if err != nil { return nil, fmt.Errorf("cannot download PDF from S3: %w", err) } + defer func() { _ = result.Body.Close() }() pdfData, err := io.ReadAll(result.Body) diff --git a/pkg/trust/service.go b/pkg/trust/service.go index 5618185b6..43987dd88 100644 --- a/pkg/trust/service.go +++ b/pkg/trust/service.go @@ -152,13 +152,13 @@ func (s *Service) Get( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrPageNotFound } + return fmt.Errorf("cannot load trust center: %w", err) } return nil }, ) - if err != nil { return nil, err } @@ -180,13 +180,13 @@ func (s *Service) GetBySlug( if errors.Is(err, coredata.ErrResourceNotFound) { return ErrPageNotFound } + return fmt.Errorf("cannot load trust center: %w", err) } return nil }, ) - if err != nil { return nil, err } @@ -230,7 +230,6 @@ func (s *Service) GetByDomainName(ctx context.Context, domain string) (*coredata return nil }, ) - if err != nil { return nil, err } @@ -263,13 +262,16 @@ func (s *Service) GetCustomDomainByOrganizationID(ctx context.Context, organizat // esign certificate worker which needs per-org branding at render time. func (s *Service) EmailPresenterConfigByOrganizationID(ctx context.Context, orgID gid.GID) (emails.PresenterConfig, error) { var trustCenter coredata.TrustCenter + scope := coredata.NewScopeFromObjectID(orgID) + err := s.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { return trustCenter.LoadByOrganizationID(ctx, conn, scope, orgID) }) if err != nil { return emails.PresenterConfig{}, fmt.Errorf("cannot load trust center for org %s: %w", orgID, err) } + return s.WithTenant(orgID.TenantID()).TrustCenters.EmailPresenterConfig(ctx, trustCenter.ID) } @@ -404,8 +406,10 @@ func (s *Service) ProvisionMember( } var sig *coredata.ElectronicSignature + if compliancePage.NonDisclosureAgreementFileID != nil && s.esign != nil { var err error + sig, err = s.esign.CreateSignature( ctx, tx, diff --git a/pkg/trust/third_party_service.go b/pkg/trust/third_party_service.go index a919b0bef..87f1819bf 100644 --- a/pkg/trust/third_party_service.go +++ b/pkg/trust/third_party_service.go @@ -45,7 +45,6 @@ func (s ThirdPartyService) Get( return nil }, ) - if err != nil { return nil, err } @@ -74,7 +73,6 @@ func (s ThirdPartyService) ListForOrganizationId( return nil }, ) - if err != nil { return nil, err } @@ -99,6 +97,7 @@ func (s ThirdPartyService) CountForTrustCenterId( thirdParties := &coredata.ThirdParties{} showOnTrustCenter := true filter := coredata.NewThirdPartyFilter(&showOnTrustCenter) + count, err = thirdParties.CountByOrganizationID(ctx, conn, s.svc.scope, trustCenter.OrganizationID, filter) if err != nil { return fmt.Errorf("cannot count thirdParties: %w", err) @@ -107,7 +106,6 @@ func (s ThirdPartyService) CountForTrustCenterId( return nil }, ) - if err != nil { return 0, err } diff --git a/pkg/trust/trust_center_access_service.go b/pkg/trust/trust_center_access_service.go index ea4c07604..c00b0d23f 100644 --- a/pkg/trust/trust_center_access_service.go +++ b/pkg/trust/trust_center_access_service.go @@ -76,6 +76,7 @@ func (s TrustCenterAccessService) Request( documentIDs := req.DocumentIDs if req.DocumentIDs == nil { var allDocuments coredata.Documents + filter := coredata.NewDocumentTrustCenterFilter() if err := allDocuments.LoadAllByOrganizationID(ctx, tx, s.svc.scope, organizationID, filter); err != nil { @@ -90,6 +91,7 @@ func (s TrustCenterAccessService) Request( reportIDs := req.ReportIDs if req.ReportIDs == nil { var allAudits coredata.Audits + auditFilter := coredata.NewAuditTrustCenterFilter() if err := allAudits.LoadAllByOrganizationID(ctx, tx, s.svc.scope, organizationID, auditFilter); err != nil { @@ -106,6 +108,7 @@ func (s TrustCenterAccessService) Request( trustCenterFileIDs := req.TrustCenterFileIDs if req.TrustCenterFileIDs == nil { var allTrustCenterFiles coredata.TrustCenterFiles + filter := coredata.NewTrustCenterFileFilter( coredata.WithTrustCenterFileVisibilities(coredata.TrustCenterVisibilityPrivate, coredata.TrustCenterVisibilityNone), ) @@ -173,7 +176,6 @@ func (s TrustCenterAccessService) Request( return nil }, ) - if err != nil { return nil, err } @@ -209,6 +211,7 @@ func (s TrustCenterAccessService) GetDocumentAccess( err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { access := &coredata.TrustCenterAccess{} + err := access.LoadByTrustCenterIDAndIdentityID(ctx, conn, s.svc.scope, trustCenterID, identityID) if err != nil { if errors.Is(err, coredata.ErrResourceNotFound) { @@ -230,6 +233,7 @@ func (s TrustCenterAccessService) GetDocumentAccess( } documentAccess = &coredata.TrustCenterDocumentAccess{} + err = documentAccess.LoadByTrustCenterAccessIDAndDocumentID(ctx, conn, s.svc.scope, access.ID, documentID) if err != nil { if errors.Is(err, coredata.ErrResourceNotFound) { @@ -241,7 +245,6 @@ func (s TrustCenterAccessService) GetDocumentAccess( return nil }) - if err != nil { return nil, err } @@ -259,6 +262,7 @@ func (s TrustCenterAccessService) GetReportAccess( err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { access := &coredata.TrustCenterAccess{} + err := access.LoadByTrustCenterIDAndIdentityID(ctx, conn, s.svc.scope, trustCenterID, identityID) if err != nil { if errors.Is(err, coredata.ErrResourceNotFound) { @@ -280,6 +284,7 @@ func (s TrustCenterAccessService) GetReportAccess( } reportAccess = &coredata.TrustCenterDocumentAccess{} + err = reportAccess.LoadByTrustCenterAccessIDAndReportID(ctx, conn, s.svc.scope, access.ID, reportID) if err != nil { if errors.Is(err, coredata.ErrResourceNotFound) { @@ -291,7 +296,6 @@ func (s TrustCenterAccessService) GetReportAccess( return nil }) - if err != nil { return nil, err } @@ -309,6 +313,7 @@ func (s TrustCenterAccessService) GetTrustCenterFileAccess( err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { access := &coredata.TrustCenterAccess{} + err := access.LoadByTrustCenterIDAndIdentityID(ctx, conn, s.svc.scope, trustCenterID, identityID) if err != nil { if errors.Is(err, coredata.ErrResourceNotFound) { @@ -330,6 +335,7 @@ func (s TrustCenterAccessService) GetTrustCenterFileAccess( } fileAccess = &coredata.TrustCenterDocumentAccess{} + err = fileAccess.LoadByTrustCenterAccessIDAndTrustCenterFileID(ctx, conn, s.svc.scope, access.ID, trustCenterFileID) if err != nil { if errors.Is(err, coredata.ErrResourceNotFound) { @@ -341,7 +347,6 @@ func (s TrustCenterAccessService) GetTrustCenterFileAccess( return nil }) - if err != nil { return nil, err } @@ -392,11 +397,13 @@ func (s *TrustCenterAccessService) GrantByIDs( return fmt.Errorf("cannot grant document accesses: %w", err) } } + if len(reportIDs) > 0 { if err := coredata.GrantByReportIDs(ctx, tx, s.svc.scope, access.ID, reportIDs, now); err != nil { return fmt.Errorf("cannot grant report accesses: %w", err) } } + if len(fileIDs) > 0 { if err := coredata.GrantByTrustCenterFileIDs(ctx, tx, s.svc.scope, access.ID, fileIDs, now); err != nil { return fmt.Errorf("cannot grant trust center file accesses: %w", err) @@ -405,6 +412,7 @@ func (s *TrustCenterAccessService) GrantByIDs( if shouldSendEmail { profile.State = coredata.ProfileStateActive + profile.UpdatedAt = now if err := profile.Update(ctx, tx, s.svc.scope); err != nil { return fmt.Errorf("cannot update profile: %w", err) @@ -458,6 +466,7 @@ func (s *TrustCenterAccessService) sendAccessEmail(ctx context.Context, tx pg.Tx if err := accessEmail.Insert(ctx, tx); err != nil { return fmt.Errorf("cannot insert access email: %w", err) } + return nil } @@ -495,18 +504,23 @@ func (s *TrustCenterAccessService) RejectOrRevokeByIDs( if len(documentIDs) > 0 { shouldSendEmail = true + if err := coredata.RejectOrRevokeByDocumentIDs(ctx, tx, s.svc.scope, access.ID, documentIDs, now); err != nil { return fmt.Errorf("cannot reject/revoke document accesses: %w", err) } } + if len(reportIDs) > 0 { shouldSendEmail = true + if err := coredata.RejectOrRevokeByReportIDs(ctx, tx, s.svc.scope, access.ID, reportIDs, now); err != nil { return fmt.Errorf("cannot reject/revoke report accesses: %w", err) } } + if len(fileIDs) > 0 { shouldSendEmail = true + if err := coredata.RejectOrRevokeByTrustCenterFileIDs(ctx, tx, s.svc.scope, access.ID, fileIDs, now); err != nil { return fmt.Errorf("cannot reject/revoke trust center file accesses: %w", err) } @@ -536,30 +550,38 @@ func (s *TrustCenterAccessService) sendDocumentAccessRejectedEmail( return fmt.Errorf("cannot load organization: %w", err) } - var fileNames []string - var documents coredata.Documents + var ( + fileNames []string + documents coredata.Documents + ) + if len(documentIDs) > 0 { if err := documents.LoadByIDs(ctx, tx, s.svc.scope, documentIDs); err != nil { return fmt.Errorf("cannot load documents by IDs: %w", err) } + for _, d := range documents { fileNames = append(fileNames, d.Title) } } + var reports coredata.Reports if len(reportIDs) > 0 { if err := reports.LoadByIDs(ctx, tx, s.svc.scope, reportIDs); err != nil { return fmt.Errorf("cannot load reports by IDs: %w", err) } + for _, r := range reports { fileNames = append(fileNames, r.Filename) } } + var files coredata.TrustCenterFiles if len(fileIDs) > 0 { if err := files.LoadByIDs(ctx, tx, s.svc.scope, fileIDs); err != nil { return fmt.Errorf("cannot load files by IDs: %w", err) } + for _, f := range files { fileNames = append(fileNames, f.Name) } @@ -595,21 +617,26 @@ func (s *TrustCenterAccessService) sendDocumentAccessRejectedEmail( if err := accessEmail.Insert(ctx, tx); err != nil { return fmt.Errorf("cannot insert access email: %w", err) } + return nil } func extractExistingIDs(accesses coredata.TrustCenterDocumentAccesses) ([]gid.GID, []gid.GID, []gid.GID) { - var documentIDs []gid.GID - var reportIDs []gid.GID - var trustCenterFileIDs []gid.GID + var ( + documentIDs []gid.GID + reportIDs []gid.GID + trustCenterFileIDs []gid.GID + ) for _, access := range accesses { if access.DocumentID != nil { documentIDs = append(documentIDs, *access.DocumentID) } + if access.ReportID != nil { reportIDs = append(reportIDs, *access.ReportID) } + if access.TrustCenterFileID != nil { trustCenterFileIDs = append(trustCenterFileIDs, *access.TrustCenterFileID) } @@ -625,6 +652,7 @@ func filterExistingIDs(allIDs []gid.GID, existingIDs []gid.GID) []gid.GID { } var newIDs []gid.GID + for _, id := range allIDs { if !existingMap[id] { newIDs = append(newIDs, id) diff --git a/pkg/trust/trust_center_file_service.go b/pkg/trust/trust_center_file_service.go index 0f129fa93..674a53fe4 100644 --- a/pkg/trust/trust_center_file_service.go +++ b/pkg/trust/trust_center_file_service.go @@ -50,7 +50,6 @@ func (s *TrustCenterFileService) Get( return nil }, ) - if err != nil { return nil, err } @@ -85,7 +84,6 @@ func (s *TrustCenterFileService) ListForOrganizationId( return nil }, ) - if err != nil { return nil, err } @@ -108,6 +106,7 @@ func (s *TrustCenterFileService) ExportFile( if err != nil { return nil, "", fmt.Errorf("cannot add watermark to PDF: %w", err) } + return watermarkedPDF, mimeType, nil } @@ -125,8 +124,10 @@ func (s *TrustCenterFileService) exportFileData( ctx context.Context, trustCenterFileID gid.GID, ) ([]byte, string, error) { - var trustCenterFile *coredata.TrustCenterFile - var file *coredata.File + var ( + trustCenterFile *coredata.TrustCenterFile + file *coredata.File + ) err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { trustCenterFile = &coredata.TrustCenterFile{} @@ -152,6 +153,7 @@ func (s *TrustCenterFileService) exportFileData( if err != nil { return nil, "", fmt.Errorf("cannot download file from S3: %w", err) } + defer func() { _ = result.Body.Close() }() fileData, err := io.ReadAll(result.Body) diff --git a/pkg/trust/trust_center_reference_service.go b/pkg/trust/trust_center_reference_service.go index 97ce2a4a7..906084825 100644 --- a/pkg/trust/trust_center_reference_service.go +++ b/pkg/trust/trust_center_reference_service.go @@ -46,7 +46,6 @@ func (s TrustCenterReferenceService) ListForTrustCenterID( return nil }) - if err != nil { return nil, err } @@ -61,6 +60,7 @@ func (s TrustCenterReferenceService) GenerateLogoURL( ) (string, error) { reference := &coredata.TrustCenterReference{} file := &coredata.File{} + err := s.svc.pg.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { err := reference.LoadByID(ctx, tx, s.svc.scope, referenceID) if err != nil { @@ -74,7 +74,6 @@ func (s TrustCenterReferenceService) GenerateLogoURL( return nil }) - if err != nil { return "", nil } @@ -117,7 +116,6 @@ func (s TrustCenterReferenceService) Get( return nil }, ) - if err != nil { return nil, err } diff --git a/pkg/trust/trust_center_service.go b/pkg/trust/trust_center_service.go index 289206efb..2f47c7551 100644 --- a/pkg/trust/trust_center_service.go +++ b/pkg/trust/trust_center_service.go @@ -48,7 +48,6 @@ func (s TrustCenterService) Get( return nil }, ) - if err != nil { return nil, fmt.Errorf("cannot load trust center: %w", err) } @@ -73,7 +72,6 @@ func (s TrustCenterService) GetByOrganizationID( return nil }, ) - if err != nil { return nil, err } diff --git a/pkg/uri/uri.go b/pkg/uri/uri.go index 57ceebb79..0440addb4 100644 --- a/pkg/uri/uri.go +++ b/pkg/uri/uri.go @@ -44,6 +44,7 @@ func (u *URI) UnmarshalText(text []byte) error { } *u = parsed + return nil } @@ -53,6 +54,7 @@ func (u URI) MarshalText() ([]byte, error) { func (u *URI) Scan(value any) error { var s string + switch v := value.(type) { case string: s = v @@ -68,6 +70,7 @@ func (u *URI) Scan(value any) error { } *u = parsed + return nil } diff --git a/pkg/uri/uri_test.go b/pkg/uri/uri_test.go index 54bf04ecd..7ff1cd7ca 100644 --- a/pkg/uri/uri_test.go +++ b/pkg/uri/uri_test.go @@ -116,6 +116,7 @@ func TestURIUnmarshalText(t *testing.T) { t.Parallel() var u URI + err := u.UnmarshalText([]byte("https://example.com/callback")) require.NoError(t, err) assert.Equal(t, URI("https://example.com/callback"), u) @@ -128,6 +129,7 @@ func TestURIUnmarshalText(t *testing.T) { t.Parallel() var u URI + err := u.UnmarshalText([]byte("not-a-url")) require.Error(t, err) }, @@ -200,6 +202,7 @@ func TestURIScan(t *testing.T) { t.Parallel() var u URI + err := u.Scan("https://example.com") require.NoError(t, err) assert.Equal(t, URI("https://example.com"), u) @@ -212,6 +215,7 @@ func TestURIScan(t *testing.T) { t.Parallel() var u URI + err := u.Scan([]byte("https://example.com")) require.NoError(t, err) assert.Equal(t, URI("https://example.com"), u) @@ -224,6 +228,7 @@ func TestURIScan(t *testing.T) { t.Parallel() var u URI + err := u.Scan("not-a-url") require.Error(t, err) }, @@ -235,6 +240,7 @@ func TestURIScan(t *testing.T) { t.Parallel() var u URI + err := u.Scan(123) require.Error(t, err) }, diff --git a/pkg/validator/checkeach_slice_test.go b/pkg/validator/checkeach_slice_test.go index c363fb772..c8c255223 100644 --- a/pkg/validator/checkeach_slice_test.go +++ b/pkg/validator/checkeach_slice_test.go @@ -44,6 +44,7 @@ func TestCheckEach_NonEmptyTypedSlice(t *testing.T) { slice := []CustomType{"abc", "def"} callCount := 0 + v.CheckEach(slice, "items", func(index int, item any) { callCount++ // Verify the item is the correct type @@ -51,9 +52,11 @@ func TestCheckEach_NonEmptyTypedSlice(t *testing.T) { if !ok { t.Errorf("expected CustomType, got %T", item) } + if index == 0 && str != "abc" { t.Errorf("expected 'abc', got %s", str) } + if index == 1 && str != "def" { t.Errorf("expected 'def', got %s", str) } @@ -92,12 +95,15 @@ func TestCheckEach_PointerToNonEmptySlice(t *testing.T) { ptrToSlice := &slice callCount := 0 + v.CheckEach(ptrToSlice, "items", func(index int, item any) { callCount++ + str, ok := item.(CustomType) if !ok { t.Errorf("expected CustomType, got %T", item) } + expectedValues := []CustomType{"abc", "def", "ghi"} if str != expectedValues[index] { t.Errorf("at index %d: expected %s, got %s", index, expectedValues[index], str) @@ -153,12 +159,15 @@ func TestCheckEach_DoublePointerToSlice(t *testing.T) { doublePtrToSlice := &ptrToSlice callCount := 0 + v.CheckEach(doublePtrToSlice, "items", func(index int, item any) { callCount++ + str, ok := item.(CustomType) if !ok { t.Errorf("expected CustomType, got %T", item) } + expectedValues := []CustomType{"x", "y"} if str != expectedValues[index] { t.Errorf("at index %d: expected %s, got %s", index, expectedValues[index], str) @@ -192,9 +201,11 @@ func TestCheckEach_NonSliceValue(t *testing.T) { if len(errors) != 1 { t.Errorf("expected 1 error, got %d", len(errors)) } + if errors[0].Code != ErrorCodeInvalidFormat { t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, errors[0].Code) } + if errors[0].Message != "expected a slice" { t.Errorf("expected message 'expected a slice', got '%s'", errors[0].Message) } diff --git a/pkg/validator/double_pointer_test.go b/pkg/validator/double_pointer_test.go index da189e96c..b360e2aca 100644 --- a/pkg/validator/double_pointer_test.go +++ b/pkg/validator/double_pointer_test.go @@ -62,6 +62,7 @@ func TestDoublePointerValidation(t *testing.T) { t.Run("optional double pointer - nil outer pointer", func(t *testing.T) { v := validator.New() + var doublePtr **string = nil v.Check(doublePtr, "name", validator.NotEmpty(), validator.MaxLen(1000)) @@ -73,7 +74,9 @@ func TestDoublePointerValidation(t *testing.T) { t.Run("optional double pointer - nil inner pointer", func(t *testing.T) { v := validator.New() + var ptr *string = nil + doublePtr := &ptr v.Check(doublePtr, "name", validator.NotEmpty(), validator.MaxLen(1000)) diff --git a/pkg/validator/errors.go b/pkg/validator/errors.go index 685b1d32c..429636cfd 100644 --- a/pkg/validator/errors.go +++ b/pkg/validator/errors.go @@ -57,6 +57,7 @@ func (ve ValidationErrors) Error() string { for _, err := range ve { messages = append(messages, err.Error()) } + return strings.Join(messages, "; ") } @@ -69,26 +70,31 @@ func (ve ValidationErrors) Fields() []string { for _, err := range ve { fields = append(fields, err.Field) } + return fields } func (ve ValidationErrors) ByField(field string) ValidationErrors { var errors ValidationErrors + for _, err := range ve { if err.Field == field { errors = append(errors, err) } } + return errors } func (ve ValidationErrors) ByCode(code ErrorCode) ValidationErrors { var errors ValidationErrors + for _, err := range ve { if err.Code == code { errors = append(errors, err) } } + return errors } @@ -96,6 +102,7 @@ func (ve ValidationErrors) First() *ValidationError { if len(ve) == 0 { return nil } + return ve[0] } diff --git a/pkg/validator/validation.go b/pkg/validator/validation.go index 0d1a7fb5f..d3e2a5797 100644 --- a/pkg/validator/validation.go +++ b/pkg/validator/validation.go @@ -42,6 +42,7 @@ func (v *Validator) Check(value any, field string, validators ...ValidatorFunc) val = val.Elem() actualValue = val.Interface() } + // If we ended up with a nil pointer at any level, set actualValue to nil if val.Kind() == reflect.Pointer && val.IsNil() { actualValue = nil @@ -69,6 +70,7 @@ func (v *Validator) CheckEach(items any, field string, fn func(index int, item a for i, item := range slice { fn(i, item) } + return } @@ -78,6 +80,7 @@ func (v *Validator) CheckEach(items any, field string, fn func(index int, item a if val.IsNil() { return } + val = val.Elem() } @@ -88,6 +91,7 @@ func (v *Validator) CheckEach(items any, field string, fn func(index int, item a Message: "expected a slice", Value: items, }) + return } @@ -100,6 +104,7 @@ func (v *Validator) Error() error { if len(v.errors) == 0 { return nil } + return v.errors } @@ -118,6 +123,7 @@ func dereferenceValue(value any) (any, bool) { if val.IsNil() { return nil, true } + val = val.Elem() } diff --git a/pkg/validator/validation_bench_test.go b/pkg/validator/validation_bench_test.go index d81b555b6..554ea3a7e 100644 --- a/pkg/validator/validation_bench_test.go +++ b/pkg/validator/validation_bench_test.go @@ -23,6 +23,7 @@ func BenchmarkValidate_SingleField(b *testing.B) { email := "test@example.com" b.ResetTimer() + for i := 0; i < b.N; i++ { v := New() v.Check(&email, "email", Required(), NotEmpty()) @@ -35,6 +36,7 @@ func BenchmarkValidate_MultipleFields(b *testing.B) { age := 25 b.ResetTimer() + for i := 0; i < b.N; i++ { v := New() v.Check(&email, "email", Required(), NotEmpty()) @@ -47,6 +49,7 @@ func BenchmarkValidate_OptionalField(b *testing.B) { var website *string b.ResetTimer() + for i := 0; i < b.N; i++ { v := New() v.Check(website, "website", URL()) @@ -58,6 +61,7 @@ func BenchmarkURL(b *testing.B) { validator := URL() b.ResetTimer() + for i := 0; i < b.N; i++ { _ = validator(&urlStr) } @@ -68,6 +72,7 @@ func BenchmarkMinLen(b *testing.B) { validator := MinLen(5) b.ResetTimer() + for i := 0; i < b.N; i++ { _ = validator(&str) } @@ -78,6 +83,7 @@ func BenchmarkMin(b *testing.B) { validator := Min(18) b.ResetTimer() + for i := 0; i < b.N; i++ { _ = validator(&num) } @@ -88,6 +94,7 @@ func BenchmarkNotEmpty(b *testing.B) { validator := NotEmpty() b.ResetTimer() + for i := 0; i < b.N; i++ { _ = validator(&str) } @@ -97,9 +104,11 @@ func BenchmarkValidate_WithErrors(b *testing.B) { email := "" b.ResetTimer() + for i := 0; i < b.N; i++ { v := New() v.Check(&email, "email", Required(), NotEmpty()) + if v.Error() == nil { b.Fatal("expected validation error") } @@ -139,6 +148,7 @@ func BenchmarkValidate_ComplexForm(b *testing.B) { } b.ResetTimer() + for i := 0; i < b.N; i++ { v := New() v.Check(&user.Email, "email", Required(), NotEmpty()) @@ -155,6 +165,7 @@ func BenchmarkAfter(b *testing.B) { validator := After(now) b.ResetTimer() + for i := 0; i < b.N; i++ { _ = validator(&future) } @@ -166,6 +177,7 @@ func BenchmarkBefore(b *testing.B) { validator := Before(now) b.ResetTimer() + for i := 0; i < b.N; i++ { _ = validator(&past) } @@ -176,6 +188,7 @@ func BenchmarkDomain(b *testing.B) { validator := Domain() b.ResetTimer() + for i := 0; i < b.N; i++ { _ = validator(&str) } @@ -186,6 +199,7 @@ func BenchmarkHTTPSUrl(b *testing.B) { validator := HTTPSUrl() b.ResetTimer() + for i := 0; i < b.N; i++ { _ = validator(&str) } diff --git a/pkg/validator/validation_test.go b/pkg/validator/validation_test.go index e9075604f..69a12449b 100644 --- a/pkg/validator/validation_test.go +++ b/pkg/validator/validation_test.go @@ -198,6 +198,7 @@ func TestDuplicateValidators(t *testing.T) { if errors[0].Message != "must be at least 5 characters" { t.Errorf("unexpected first error: %s", errors[0].Message) } + if errors[1].Message != "must be at least 5 characters" { t.Errorf("unexpected second error: %s", errors[1].Message) } @@ -238,6 +239,7 @@ func TestDuplicateValidators(t *testing.T) { if errors[0].Message != "must be at least 5 characters" { t.Errorf("unexpected first error: %s", errors[0].Message) } + if errors[1].Message != "must be at least 10 characters" { t.Errorf("unexpected second error: %s", errors[1].Message) } @@ -250,6 +252,7 @@ func TestStandardErrorPattern(t *testing.T) { v := New() v.Check(&email, "email", Required(), NotEmpty()) v.Check(&password, "password", Required(), MinLen(8)) + return v.Error() } diff --git a/pkg/validator/validator_common.go b/pkg/validator/validator_common.go index d66346201..624795929 100644 --- a/pkg/validator/validator_common.go +++ b/pkg/validator/validator_common.go @@ -70,6 +70,7 @@ func NoDuplicates() ValidatorFunc { if _, ok := seen[elem]; ok { return newValidationError(ErrorCodeInvalidFormat, "must not contain duplicates") } + seen[elem] = struct{}{} } diff --git a/pkg/validator/validator_common_test.go b/pkg/validator/validator_common_test.go index 6a75c5f6d..431151716 100644 --- a/pkg/validator/validator_common_test.go +++ b/pkg/validator/validator_common_test.go @@ -30,6 +30,7 @@ func TestOptionalByDefault(t *testing.T) { t.Run("nil pointer skips validation by default", func(t *testing.T) { v := New() + var str *string v.Check(str, "field", MinLen(5)) @@ -80,6 +81,7 @@ func TestOptionalByDefault(t *testing.T) { t.Run("Required() validates nil values", func(t *testing.T) { v := New() + var str *string v.Check(str, "field", Required()) @@ -92,6 +94,7 @@ func TestOptionalByDefault(t *testing.T) { func TestRequired(t *testing.T) { t.Run("valid string", func(t *testing.T) { str := "hello" + err := Required()(&str) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -100,6 +103,7 @@ func TestRequired(t *testing.T) { t.Run("empty string", func(t *testing.T) { str := "" + err := Required()(&str) if err == nil { t.Fatal("expected validation error") @@ -110,6 +114,7 @@ func TestRequired(t *testing.T) { t.Run("whitespace string", func(t *testing.T) { str := " " + err := Required()(&str) if err == nil { t.Error("expected validation error for whitespace") @@ -118,6 +123,7 @@ func TestRequired(t *testing.T) { t.Run("nil string pointer", func(t *testing.T) { var str *string + err := Required()(str) if err == nil { t.Error("expected validation error for nil pointer") @@ -126,6 +132,7 @@ func TestRequired(t *testing.T) { t.Run("valid string pointer", func(t *testing.T) { str := "hello" + err := Required()(&str) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -141,6 +148,7 @@ func TestRequired(t *testing.T) { t.Run("zero int", func(t *testing.T) { num := 0 + err := Required()(&num) if err != nil { t.Errorf("expected no error for zero int, got: %v", err) @@ -149,6 +157,7 @@ func TestRequired(t *testing.T) { t.Run("positive int", func(t *testing.T) { num := 42 + err := Required()(&num) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -157,6 +166,7 @@ func TestRequired(t *testing.T) { t.Run("nil int pointer", func(t *testing.T) { var num *int + err := Required()(num) if err == nil { t.Error("expected validation error for nil int pointer") @@ -165,6 +175,7 @@ func TestRequired(t *testing.T) { t.Run("valid int pointer", func(t *testing.T) { num := 42 + err := Required()(&num) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -173,6 +184,7 @@ func TestRequired(t *testing.T) { t.Run("empty slice", func(t *testing.T) { slice := []any{} + err := Required()(slice) if err == nil { t.Error("expected validation error for empty slice") @@ -181,6 +193,7 @@ func TestRequired(t *testing.T) { t.Run("non-empty slice", func(t *testing.T) { slice := []any{1, 2, 3} + err := Required()(slice) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -189,6 +202,7 @@ func TestRequired(t *testing.T) { t.Run("empty string slice", func(t *testing.T) { slice := []string{} + err := Required()(slice) if err == nil { t.Fatal("expected validation error for empty []string slice") @@ -199,6 +213,7 @@ func TestRequired(t *testing.T) { t.Run("non-empty string slice", func(t *testing.T) { slice := []string{"a", "b", "c"} + err := Required()(slice) if err != nil { t.Errorf("expected no error for non-empty []string, got: %v", err) @@ -207,6 +222,7 @@ func TestRequired(t *testing.T) { t.Run("empty int slice", func(t *testing.T) { slice := []int{} + err := Required()(slice) if err == nil { t.Fatal("expected validation error for empty []int slice") @@ -217,6 +233,7 @@ func TestRequired(t *testing.T) { t.Run("non-empty int slice", func(t *testing.T) { slice := []int{1, 2, 3} + err := Required()(slice) if err != nil { t.Errorf("expected no error for non-empty []int, got: %v", err) @@ -227,7 +244,9 @@ func TestRequired(t *testing.T) { type CustomType struct { ID int } + slice := []CustomType{} + err := Required()(slice) if err == nil { t.Fatal("expected validation error for empty custom type slice") @@ -240,7 +259,9 @@ func TestRequired(t *testing.T) { type CustomType struct { ID int } + slice := []CustomType{{ID: 1}, {ID: 2}} + err := Required()(slice) if err != nil { t.Errorf("expected no error for non-empty custom type slice, got: %v", err) @@ -249,6 +270,7 @@ func TestRequired(t *testing.T) { t.Run("empty pointer slice", func(t *testing.T) { slice := []*string{} + err := Required()(slice) if err == nil { t.Error("expected validation error for empty []*string slice") @@ -258,6 +280,7 @@ func TestRequired(t *testing.T) { t.Run("non-empty pointer slice", func(t *testing.T) { str1, str2 := "a", "b" slice := []*string{&str1, &str2} + err := Required()(slice) if err != nil { t.Errorf("expected no error for non-empty []*string, got: %v", err) @@ -268,6 +291,7 @@ func TestRequired(t *testing.T) { func TestNoDuplicates(t *testing.T) { t.Run("nil slice", func(t *testing.T) { var slice []string + err := NoDuplicates()(slice) if err != nil { t.Errorf("expected no error for nil slice, got: %v", err) @@ -276,6 +300,7 @@ func TestNoDuplicates(t *testing.T) { t.Run("empty slice", func(t *testing.T) { slice := []string{} + err := NoDuplicates()(slice) if err != nil { t.Errorf("expected no error for empty slice, got: %v", err) @@ -284,6 +309,7 @@ func TestNoDuplicates(t *testing.T) { t.Run("unique strings", func(t *testing.T) { slice := []string{"a", "b", "c"} + err := NoDuplicates()(slice) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -292,6 +318,7 @@ func TestNoDuplicates(t *testing.T) { t.Run("duplicate strings", func(t *testing.T) { slice := []string{"a", "b", "a"} + err := NoDuplicates()(slice) if err == nil { t.Fatal("expected validation error for duplicates") @@ -302,6 +329,7 @@ func TestNoDuplicates(t *testing.T) { t.Run("unique ints", func(t *testing.T) { slice := []int{1, 2, 3} + err := NoDuplicates()(slice) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -310,6 +338,7 @@ func TestNoDuplicates(t *testing.T) { t.Run("duplicate ints", func(t *testing.T) { slice := []int{1, 2, 1} + err := NoDuplicates()(slice) if err == nil { t.Fatal("expected validation error for duplicates") @@ -318,6 +347,7 @@ func TestNoDuplicates(t *testing.T) { t.Run("non-comparable elements", func(t *testing.T) { slice := []map[string]string{{"a": "b"}} + err := NoDuplicates()(slice) if err == nil { t.Fatal("expected validation error for non-comparable elements") diff --git a/pkg/validator/validator_format.go b/pkg/validator/validator_format.go index 222a0fb86..643c71800 100644 --- a/pkg/validator/validator_format.go +++ b/pkg/validator/validator_format.go @@ -119,6 +119,7 @@ func GID(entityTypes ...uint16) ValidatorFunc { if v == nil { return nil } + gidValue = *v default: return newValidationError(ErrorCodeInvalidGID, "value must be a GID") @@ -126,6 +127,7 @@ func GID(entityTypes ...uint16) ValidatorFunc { if len(entityTypes) > 0 { parsedEntityType := gidValue.EntityType() + valid := slices.Contains(entityTypes, parsedEntityType) if !valid { return newValidationError(ErrorCodeInvalidGID, "GID has invalid entity type") diff --git a/pkg/validator/validator_format_test.go b/pkg/validator/validator_format_test.go index 8a9383e8c..337d813c4 100644 --- a/pkg/validator/validator_format_test.go +++ b/pkg/validator/validator_format_test.go @@ -44,6 +44,7 @@ func TestURL(t *testing.T) { if (err != nil) != tt.wantError { t.Errorf("URL() error = %v, wantError %v", err, tt.wantError) } + if err != nil && err.Code != ErrorCodeInvalidURL { t.Errorf("Expected error code %s, got %s", ErrorCodeInvalidURL, err.Code) } @@ -54,6 +55,7 @@ func TestURL(t *testing.T) { func TestHTTPSUrl(t *testing.T) { t.Run("valid https URL", func(t *testing.T) { str := "https://example.com" + err := HTTPSUrl()(&str) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -62,6 +64,7 @@ func TestHTTPSUrl(t *testing.T) { t.Run("valid https URL with path", func(t *testing.T) { str := "https://example.com/path/to/resource" + err := HTTPSUrl()(&str) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -70,6 +73,7 @@ func TestHTTPSUrl(t *testing.T) { t.Run("valid https URL with query", func(t *testing.T) { str := "https://api.example.com/v1/users?page=1" + err := HTTPSUrl()(&str) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -78,10 +82,12 @@ func TestHTTPSUrl(t *testing.T) { t.Run("invalid - http scheme", func(t *testing.T) { str := "http://example.com" + err := HTTPSUrl()(&str) if err == nil { t.Fatal("expected validation error for http") } + if err.Message != "URL must use https scheme" { t.Errorf("unexpected error message: %s", err.Message) } @@ -89,6 +95,7 @@ func TestHTTPSUrl(t *testing.T) { t.Run("invalid - ftp scheme", func(t *testing.T) { str := "ftp://example.com" + err := HTTPSUrl()(&str) if err == nil { t.Error("expected validation error for ftp") @@ -97,6 +104,7 @@ func TestHTTPSUrl(t *testing.T) { t.Run("invalid - no scheme", func(t *testing.T) { str := "example.com" + err := HTTPSUrl()(&str) if err == nil { t.Error("expected validation error for missing scheme") @@ -105,6 +113,7 @@ func TestHTTPSUrl(t *testing.T) { t.Run("invalid - no host", func(t *testing.T) { str := "https://" + err := HTTPSUrl()(&str) if err == nil { t.Error("expected validation error for missing host") @@ -113,6 +122,7 @@ func TestHTTPSUrl(t *testing.T) { t.Run("empty string", func(t *testing.T) { str := "" + err := HTTPSUrl()(&str) if err != nil { t.Errorf("expected no error for empty string, got: %v", err) @@ -121,6 +131,7 @@ func TestHTTPSUrl(t *testing.T) { t.Run("nil pointer", func(t *testing.T) { var str *string + err := HTTPSUrl()(str) if err != nil { t.Errorf("expected no error for nil, got: %v", err) @@ -157,6 +168,7 @@ func TestOrigin(t *testing.T) { if (err != nil) != tt.wantError { t.Errorf("Origin() error = %v, wantError %v", err, tt.wantError) } + if err != nil && err.Code != ErrorCodeInvalidFormat { t.Errorf("Expected error code %s, got %s", ErrorCodeInvalidFormat, err.Code) } @@ -200,6 +212,7 @@ func TestSlug(t *testing.T) { if (err != nil) != tt.wantError { t.Errorf("Slug(%d) error = %v, wantError %v", tt.maxLen, err, tt.wantError) } + if err != nil && tt.wantCode != "" && err.Code != tt.wantCode { t.Errorf("Expected error code %s, got %s", tt.wantCode, err.Code) } @@ -210,6 +223,7 @@ func TestSlug(t *testing.T) { func TestDomain(t *testing.T) { t.Run("valid domain", func(t *testing.T) { str := "example.com" + err := Domain()(&str) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -218,6 +232,7 @@ func TestDomain(t *testing.T) { t.Run("valid subdomain", func(t *testing.T) { str := "api.example.com" + err := Domain()(&str) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -226,6 +241,7 @@ func TestDomain(t *testing.T) { t.Run("valid nested subdomain", func(t *testing.T) { str := "api.v1.example.com" + err := Domain()(&str) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -234,6 +250,7 @@ func TestDomain(t *testing.T) { t.Run("valid domain with hyphens", func(t *testing.T) { str := "my-api.example-site.com" + err := Domain()(&str) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -242,6 +259,7 @@ func TestDomain(t *testing.T) { t.Run("single word domain", func(t *testing.T) { str := "localhost" + err := Domain()(&str) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -250,6 +268,7 @@ func TestDomain(t *testing.T) { t.Run("invalid - starts with hyphen", func(t *testing.T) { str := "-example.com" + err := Domain()(&str) if err == nil { t.Error("expected validation error for domain starting with hyphen") @@ -258,6 +277,7 @@ func TestDomain(t *testing.T) { t.Run("invalid - ends with hyphen", func(t *testing.T) { str := "example-.com" + err := Domain()(&str) if err == nil { t.Error("expected validation error for domain ending with hyphen") @@ -266,6 +286,7 @@ func TestDomain(t *testing.T) { t.Run("invalid - contains underscore", func(t *testing.T) { str := "example_site.com" + err := Domain()(&str) if err == nil { t.Error("expected validation error for underscore") @@ -274,6 +295,7 @@ func TestDomain(t *testing.T) { t.Run("invalid - contains spaces", func(t *testing.T) { str := "example site.com" + err := Domain()(&str) if err == nil { t.Error("expected validation error for spaces") @@ -282,6 +304,7 @@ func TestDomain(t *testing.T) { t.Run("invalid - empty label", func(t *testing.T) { str := "example..com" + err := Domain()(&str) if err == nil { t.Error("expected validation error for empty label") @@ -290,10 +313,12 @@ func TestDomain(t *testing.T) { t.Run("invalid - too long", func(t *testing.T) { str := strings.Repeat("a", 254) + err := Domain()(&str) if err == nil { t.Fatal("expected validation error for domain too long") } + if err.Message != "domain name too long (max 253 characters)" { t.Errorf("unexpected error message: %s", err.Message) } @@ -301,6 +326,7 @@ func TestDomain(t *testing.T) { t.Run("empty string", func(t *testing.T) { str := "" + err := Domain()(&str) if err != nil { t.Errorf("expected no error for empty string, got: %v", err) @@ -309,6 +335,7 @@ func TestDomain(t *testing.T) { t.Run("nil pointer", func(t *testing.T) { var str *string + err := Domain()(str) if err != nil { t.Errorf("expected no error for nil, got: %v", err) @@ -347,9 +374,11 @@ func TestGID(t *testing.T) { if err == nil { t.Fatal("expected validation error for wrong entity type") } + if err.Code != ErrorCodeInvalidGID { t.Errorf("expected error code %s, got %s", ErrorCodeInvalidGID, err.Code) } + if err.Message != "GID has invalid entity type" { t.Errorf("unexpected error message: %s", err.Message) } @@ -371,6 +400,7 @@ func TestGID(t *testing.T) { t.Run("nil GID pointer", func(t *testing.T) { var gidPtr *gid.GID + err := GID()(gidPtr) if err != nil { t.Errorf("expected no error for nil GID pointer, got: %v", err) @@ -396,6 +426,7 @@ func TestGID(t *testing.T) { if err == nil { t.Fatal("expected validation error for non-GID type") } + if err.Message != "value must be a GID" { t.Errorf("unexpected error message: %s", err.Message) } @@ -406,6 +437,7 @@ func TestGID(t *testing.T) { if err == nil { t.Fatal("expected validation error for string type") } + if err.Message != "value must be a GID" { t.Errorf("unexpected error message: %s", err.Message) } diff --git a/pkg/validator/validator_numeric.go b/pkg/validator/validator_numeric.go index ab0614bfb..d1c2ecd81 100644 --- a/pkg/validator/validator_numeric.go +++ b/pkg/validator/validator_numeric.go @@ -25,6 +25,7 @@ func Min(min int) ValidatorFunc { } var num int + switch v := actualValue.(type) { case int: num = v @@ -56,6 +57,7 @@ func Max(max int) ValidatorFunc { } var num int + switch v := actualValue.(type) { case int: num = v diff --git a/pkg/validator/validator_prosemirror.go b/pkg/validator/validator_prosemirror.go index 42c7d1dc6..acb4302e8 100644 --- a/pkg/validator/validator_prosemirror.go +++ b/pkg/validator/validator_prosemirror.go @@ -30,16 +30,20 @@ func ProseMirrorDocumentContent() ValidatorFunc { if isNil { return nil } + s, ok := actualValue.(string) if !ok { return newValidationError(ErrorCodeInvalidFormat, "value must be a string") } + if strings.TrimSpace(s) == "" { return nil } + if err := prosemirror.ValidateDocumentContentJSON(s); err != nil { return newValidationError(ErrorCodeInvalidFormat, err.Error()) } + return nil } } @@ -55,23 +59,28 @@ func ProseMirrorDocumentMaxTextLength(maxLength int) ValidatorFunc { if isNil { return nil } + s, ok := actualValue.(string) if !ok { return newValidationError(ErrorCodeInvalidFormat, "value must be a string") } + if strings.TrimSpace(s) == "" { return nil } + n, err := prosemirror.Parse(s) if err != nil { return nil } + if n.TextLength() > maxLength { return newValidationError( ErrorCodeTooLong, fmt.Sprintf("text content must be at most %d characters", maxLength), ) } + return nil } } diff --git a/pkg/validator/validator_prosemirror_test.go b/pkg/validator/validator_prosemirror_test.go index 25a1cc742..66ca45796 100644 --- a/pkg/validator/validator_prosemirror_test.go +++ b/pkg/validator/validator_prosemirror_test.go @@ -40,6 +40,7 @@ func TestProseMirrorDocumentContent(t *testing.T) { } fn := ProseMirrorDocumentContent() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -48,6 +49,7 @@ func TestProseMirrorDocumentContent(t *testing.T) { if (err != nil) != tt.wantError { t.Errorf("ProseMirrorDocumentContent() error = %v, wantError %v", err, tt.wantError) } + if err != nil && err.Code != ErrorCodeInvalidFormat { t.Errorf("expected code %s, got %s", ErrorCodeInvalidFormat, err.Code) } @@ -82,6 +84,7 @@ func TestProseMirrorDocumentMaxTextLength(t *testing.T) { } fn := ProseMirrorDocumentMaxTextLength(maxLen) + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -90,6 +93,7 @@ func TestProseMirrorDocumentMaxTextLength(t *testing.T) { if (err != nil) != tt.wantError { t.Errorf("ProseMirrorDocumentMaxTextLength() error = %v, wantError %v", err, tt.wantError) } + if err != nil && tt.wantCode != "" && err.Code != tt.wantCode { t.Errorf("expected code %s, got %s", tt.wantCode, err.Code) } diff --git a/pkg/validator/validator_security.go b/pkg/validator/validator_security.go index 94ae1c119..11b19838b 100644 --- a/pkg/validator/validator_security.go +++ b/pkg/validator/validator_security.go @@ -177,6 +177,7 @@ func NoNewLine() ValidatorFunc { if r == '\n' { return newValidationError(ErrorCodeInvalidFormat, fmt.Sprintf("contains newline character at position %d", i)) } + if r == '\r' { return newValidationError(ErrorCodeInvalidFormat, fmt.Sprintf("contains carriage return character at position %d", i)) } @@ -204,6 +205,7 @@ func SafeText(maxLen int) ValidatorFunc { return err } } + return nil } } @@ -226,6 +228,7 @@ func SafeTextNoNewLine(maxLen int) ValidatorFunc { return err } } + return nil } } diff --git a/pkg/validator/validator_security_test.go b/pkg/validator/validator_security_test.go index 7458acd6e..30ed19fa2 100644 --- a/pkg/validator/validator_security_test.go +++ b/pkg/validator/validator_security_test.go @@ -22,6 +22,7 @@ import ( func TestNoHTML(t *testing.T) { t.Run("valid text without HTML", func(t *testing.T) { str := "This is a normal text" + err := NoHTML()(&str) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -30,6 +31,7 @@ func TestNoHTML(t *testing.T) { t.Run("valid text with special characters", func(t *testing.T) { str := "Price: $10.99 - 20% off!" + err := NoHTML()(&str) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -38,6 +40,7 @@ func TestNoHTML(t *testing.T) { t.Run("valid UTF-8 text", func(t *testing.T) { str := "José García 张伟" + err := NoHTML()(&str) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -46,6 +49,7 @@ func TestNoHTML(t *testing.T) { t.Run("valid text with emojis", func(t *testing.T) { str := "Hello World 🌍" + err := NoHTML()(&str) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -54,10 +58,12 @@ func TestNoHTML(t *testing.T) { t.Run("invalid - script tag XSS", func(t *testing.T) { str := "" + err := NoHTML()(&str) if err == nil { t.Fatal("expected validation error for script tag") } + if !strings.Contains(err.Message, "HTML tags") { t.Errorf("unexpected error message: %s", err.Message) } @@ -65,10 +71,12 @@ func TestNoHTML(t *testing.T) { t.Run("invalid - simple bold tag", func(t *testing.T) { str := "Hello World" + err := NoHTML()(&str) if err == nil { t.Fatal("expected validation error for bold tag") } + if !strings.Contains(err.Message, "HTML tags") { t.Errorf("unexpected error message: %s", err.Message) } @@ -76,6 +84,7 @@ func TestNoHTML(t *testing.T) { t.Run("invalid - div tag", func(t *testing.T) { str := "
    Content
    " + err := NoHTML()(&str) if err == nil { t.Error("expected validation error for div tag") @@ -84,6 +93,7 @@ func TestNoHTML(t *testing.T) { t.Run("invalid - self-closing tag", func(t *testing.T) { str := "Line break
    here" + err := NoHTML()(&str) if err == nil { t.Error("expected validation error for self-closing tag") @@ -92,6 +102,7 @@ func TestNoHTML(t *testing.T) { t.Run("invalid - img tag", func(t *testing.T) { str := `` + err := NoHTML()(&str) if err == nil { t.Error("expected validation error for img tag") @@ -100,6 +111,7 @@ func TestNoHTML(t *testing.T) { t.Run("invalid - anchor tag", func(t *testing.T) { str := `Click` + err := NoHTML()(&str) if err == nil { t.Error("expected validation error for anchor tag") @@ -108,6 +120,7 @@ func TestNoHTML(t *testing.T) { t.Run("valid - less than symbol", func(t *testing.T) { str := "5 < 10" + err := NoHTML()(&str) if err != nil { t.Errorf("expected no error for bare angle bracket, got: %v", err) @@ -116,6 +129,7 @@ func TestNoHTML(t *testing.T) { t.Run("valid - greater than symbol", func(t *testing.T) { str := "10 > 5" + err := NoHTML()(&str) if err != nil { t.Errorf("expected no error for bare angle bracket, got: %v", err) @@ -124,6 +138,7 @@ func TestNoHTML(t *testing.T) { t.Run("valid - both angle brackets", func(t *testing.T) { str := "5 < x > 10" + err := NoHTML()(&str) if err != nil { t.Errorf("expected no error for bare angle brackets, got: %v", err) @@ -132,6 +147,7 @@ func TestNoHTML(t *testing.T) { t.Run("valid - incomplete tag", func(t *testing.T) { str := "text ` + err := NoHTML()(&str) if err == nil { t.Error("expected validation error for svg tag") @@ -156,6 +174,7 @@ func TestNoHTML(t *testing.T) { t.Run("invalid - svg with slash", func(t *testing.T) { str := `` + err := NoHTML()(&str) if err == nil { t.Error("expected validation error for svg/onload tag") @@ -164,6 +183,7 @@ func TestNoHTML(t *testing.T) { t.Run("invalid - iframe tag", func(t *testing.T) { str := `` + err := NoHTML()(&str) if err == nil { t.Error("expected validation error for body tag") @@ -212,6 +237,7 @@ func TestNoHTML(t *testing.T) { t.Run("invalid - object tag", func(t *testing.T) { str := `` + err := NoHTML()(&str) if err == nil { t.Error("expected validation error for object tag") @@ -220,6 +246,7 @@ func TestNoHTML(t *testing.T) { t.Run("invalid - embed tag", func(t *testing.T) { str := `` + err := NoHTML()(&str) if err == nil { t.Error("expected validation error for embed tag") @@ -228,6 +255,7 @@ func TestNoHTML(t *testing.T) { t.Run("invalid - meta refresh", func(t *testing.T) { str := `` + err := NoHTML()(&str) if err == nil { t.Error("expected validation error for meta tag") @@ -236,6 +264,7 @@ func TestNoHTML(t *testing.T) { t.Run("invalid - input with autofocus XSS", func(t *testing.T) { str := `` + err := NoHTML()(&str) if err == nil { t.Error("expected validation error for input tag") @@ -244,6 +273,7 @@ func TestNoHTML(t *testing.T) { t.Run("invalid - tag with newlines in attributes", func(t *testing.T) { str := "" + err := NoHTML()(&str) if err == nil { t.Error("expected validation error for tag with newlines") @@ -252,6 +282,7 @@ func TestNoHTML(t *testing.T) { t.Run("valid - math expression", func(t *testing.T) { str := "if x < 10 then y = 20" + err := NoHTML()(&str) if err != nil { t.Errorf("expected no error for math expression, got: %v", err) @@ -260,6 +291,7 @@ func TestNoHTML(t *testing.T) { t.Run("valid - arrow notation", func(t *testing.T) { str := "use -> or => for arrows" + err := NoHTML()(&str) if err != nil { t.Errorf("expected no error for arrow notation, got: %v", err) @@ -268,6 +300,7 @@ func TestNoHTML(t *testing.T) { t.Run("empty string", func(t *testing.T) { str := "" + err := NoHTML()(&str) if err != nil { t.Errorf("expected no error for empty string, got: %v", err) @@ -276,6 +309,7 @@ func TestNoHTML(t *testing.T) { t.Run("nil pointer", func(t *testing.T) { var str *string + err := NoHTML()(str) if err != nil { t.Errorf("expected no error for nil, got: %v", err) @@ -284,10 +318,12 @@ func TestNoHTML(t *testing.T) { t.Run("not a string", func(t *testing.T) { num := 123 + err := NoHTML()(&num) if err == nil { t.Fatal("expected validation error for non-string") } + if !strings.Contains(err.Message, "must be a string") { t.Errorf("unexpected error message: %s", err.Message) } @@ -325,12 +361,14 @@ func TestNoHTML(t *testing.T) { // Should have error from NoHTML errors := v.Error().(ValidationErrors) found := false + for _, err := range errors { if strings.Contains(err.Message, "HTML tags") { found = true break } } + if !found { t.Error("expected error about HTML tags") } @@ -355,6 +393,7 @@ func TestNoHTML(t *testing.T) { func TestPrintableText(t *testing.T) { t.Run("valid UTF-8 text with accents", func(t *testing.T) { str := "José García" + err := PrintableText()(&str) if err != nil { t.Errorf("expected no error for valid UTF-8 name, got: %v", err) @@ -363,6 +402,7 @@ func TestPrintableText(t *testing.T) { t.Run("valid text with emojis", func(t *testing.T) { str := "Hello World 🌍" + err := PrintableText()(&str) if err != nil { t.Errorf("expected no error for emojis, got: %v", err) @@ -371,6 +411,7 @@ func TestPrintableText(t *testing.T) { t.Run("valid Chinese characters", func(t *testing.T) { str := "张伟" + err := PrintableText()(&str) if err != nil { t.Errorf("expected no error for Chinese characters, got: %v", err) @@ -379,6 +420,7 @@ func TestPrintableText(t *testing.T) { t.Run("valid Arabic text", func(t *testing.T) { str := "محمد" + err := PrintableText()(&str) if err != nil { t.Errorf("expected no error for Arabic text, got: %v", err) @@ -387,6 +429,7 @@ func TestPrintableText(t *testing.T) { t.Run("valid Cyrillic text", func(t *testing.T) { str := "Александр" + err := PrintableText()(&str) if err != nil { t.Errorf("expected no error for Cyrillic text, got: %v", err) @@ -395,6 +438,7 @@ func TestPrintableText(t *testing.T) { t.Run("valid text with apostrophe and hyphen", func(t *testing.T) { str := "O'Brien-Smith" + err := PrintableText()(&str) if err != nil { t.Errorf("expected no error for apostrophe and hyphen, got: %v", err) @@ -403,6 +447,7 @@ func TestPrintableText(t *testing.T) { t.Run("valid text with numbers", func(t *testing.T) { str := "Product 2024" + err := PrintableText()(&str) if err != nil { t.Errorf("expected no error for text with numbers, got: %v", err) @@ -411,6 +456,7 @@ func TestPrintableText(t *testing.T) { t.Run("valid text with punctuation", func(t *testing.T) { str := "Hello, World! How are you?" + err := PrintableText()(&str) if err != nil { t.Errorf("expected no error for punctuation, got: %v", err) @@ -419,6 +465,7 @@ func TestPrintableText(t *testing.T) { t.Run("valid text with angle brackets", func(t *testing.T) { str := "5 < 10 > 3" + err := PrintableText()(&str) if err != nil { t.Errorf("expected no error for angle brackets (HTML checking is separate), got: %v", err) @@ -427,10 +474,12 @@ func TestPrintableText(t *testing.T) { t.Run("invalid - RLO character", func(t *testing.T) { str := "test\u202Eexe.txt" + err := PrintableText()(&str) if err == nil { t.Fatal("expected validation error for RLO character") } + if !strings.Contains(err.Message, "bidirectional override") { t.Errorf("unexpected error message: %s", err.Message) } @@ -438,6 +487,7 @@ func TestPrintableText(t *testing.T) { t.Run("invalid - LRO character", func(t *testing.T) { str := "test\u202Dtext" + err := PrintableText()(&str) if err == nil { t.Error("expected validation error for LRO character") @@ -446,10 +496,12 @@ func TestPrintableText(t *testing.T) { t.Run("invalid - zero-width space", func(t *testing.T) { str := "test\u200Btext" + err := PrintableText()(&str) if err == nil { t.Fatal("expected validation error for zero-width space") } + if !strings.Contains(err.Message, "zero-width") { t.Errorf("unexpected error message: %s", err.Message) } @@ -457,6 +509,7 @@ func TestPrintableText(t *testing.T) { t.Run("invalid - zero-width non-joiner", func(t *testing.T) { str := "test\u200Ctext" + err := PrintableText()(&str) if err == nil { t.Error("expected validation error for zero-width non-joiner") @@ -465,6 +518,7 @@ func TestPrintableText(t *testing.T) { t.Run("invalid - zero-width joiner", func(t *testing.T) { str := "test\u200Dtext" + err := PrintableText()(&str) if err == nil { t.Error("expected validation error for zero-width joiner") @@ -473,6 +527,7 @@ func TestPrintableText(t *testing.T) { t.Run("invalid - BOM character", func(t *testing.T) { str := "\uFEFFtest" + err := PrintableText()(&str) if err == nil { t.Error("expected validation error for BOM character") @@ -481,10 +536,12 @@ func TestPrintableText(t *testing.T) { t.Run("invalid - null byte", func(t *testing.T) { str := "test\x00text" + err := PrintableText()(&str) if err == nil { t.Fatal("expected validation error for null byte") } + if !strings.Contains(err.Message, "control character") { t.Errorf("unexpected error message: %s", err.Message) } @@ -492,6 +549,7 @@ func TestPrintableText(t *testing.T) { t.Run("invalid - tab character", func(t *testing.T) { str := "test\ttext" + err := PrintableText()(&str) if err == nil { t.Error("expected validation error for tab character") @@ -500,6 +558,7 @@ func TestPrintableText(t *testing.T) { t.Run("valid - newline character", func(t *testing.T) { str := "test\ntext" + err := PrintableText()(&str) if err != nil { t.Errorf("expected no error for newline character, got: %v", err) @@ -508,6 +567,7 @@ func TestPrintableText(t *testing.T) { t.Run("valid - carriage return", func(t *testing.T) { str := "test\rtext" + err := PrintableText()(&str) if err != nil { t.Errorf("expected no error for carriage return, got: %v", err) @@ -516,6 +576,7 @@ func TestPrintableText(t *testing.T) { t.Run("valid - multiple newlines", func(t *testing.T) { str := "hello foo\nbar\n\njd" + err := PrintableText()(&str) if err != nil { t.Errorf("expected no error for multiple newlines, got: %v", err) @@ -524,10 +585,12 @@ func TestPrintableText(t *testing.T) { t.Run("invalid - soft hyphen", func(t *testing.T) { str := "test\u00ADtext" + err := PrintableText()(&str) if err == nil { t.Fatal("expected validation error for soft hyphen") } + if !strings.Contains(err.Message, "invisible formatting") { t.Errorf("unexpected error message: %s", err.Message) } @@ -535,6 +598,7 @@ func TestPrintableText(t *testing.T) { t.Run("invalid - word joiner", func(t *testing.T) { str := "test\u2060text" + err := PrintableText()(&str) if err == nil { t.Error("expected validation error for word joiner") @@ -543,10 +607,12 @@ func TestPrintableText(t *testing.T) { t.Run("invalid - private use area character", func(t *testing.T) { str := "test\uE000text" + err := PrintableText()(&str) if err == nil { t.Fatal("expected validation error for private use area") } + if !strings.Contains(err.Message, "private use") { t.Errorf("unexpected error message: %s", err.Message) } @@ -554,10 +620,12 @@ func TestPrintableText(t *testing.T) { t.Run("invalid - replacement character", func(t *testing.T) { str := "test\uFFFDtext" + err := PrintableText()(&str) if err == nil { t.Fatal("expected validation error for replacement character") } + if !strings.Contains(err.Message, "replacement character") { t.Errorf("unexpected error message: %s", err.Message) } @@ -565,6 +633,7 @@ func TestPrintableText(t *testing.T) { t.Run("invalid - DEL control character", func(t *testing.T) { str := "test\x7Ftext" + err := PrintableText()(&str) if err == nil { t.Error("expected validation error for DEL control character") @@ -573,6 +642,7 @@ func TestPrintableText(t *testing.T) { t.Run("invalid - C1 control character", func(t *testing.T) { str := "test\u0080text" + err := PrintableText()(&str) if err == nil { t.Error("expected validation error for C1 control character") @@ -581,6 +651,7 @@ func TestPrintableText(t *testing.T) { t.Run("invalid - LTR mark", func(t *testing.T) { str := "test\u200Etext" + err := PrintableText()(&str) if err == nil { t.Error("expected validation error for LTR mark") @@ -589,6 +660,7 @@ func TestPrintableText(t *testing.T) { t.Run("invalid - RTL mark", func(t *testing.T) { str := "test\u200Ftext" + err := PrintableText()(&str) if err == nil { t.Error("expected validation error for RTL mark") @@ -597,6 +669,7 @@ func TestPrintableText(t *testing.T) { t.Run("valid with pointer", func(t *testing.T) { str := "Valid Name" + err := PrintableText()(&str) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -605,6 +678,7 @@ func TestPrintableText(t *testing.T) { t.Run("empty string", func(t *testing.T) { str := "" + err := PrintableText()(&str) if err != nil { t.Errorf("expected no error for empty string, got: %v", err) @@ -613,6 +687,7 @@ func TestPrintableText(t *testing.T) { t.Run("nil pointer", func(t *testing.T) { var str *string + err := PrintableText()(str) if err != nil { t.Errorf("expected no error for nil, got: %v", err) @@ -621,10 +696,12 @@ func TestPrintableText(t *testing.T) { t.Run("not a string", func(t *testing.T) { num := 123 + err := PrintableText()(&num) if err == nil { t.Fatal("expected validation error for non-string") } + if !strings.Contains(err.Message, "must be a string") { t.Errorf("unexpected error message: %s", err.Message) } @@ -642,10 +719,12 @@ func TestPrintableText(t *testing.T) { t.Run("position reported correctly", func(t *testing.T) { str := "abc\x00def" + err := PrintableText()(&str) if err == nil { t.Fatal("expected validation error") } + if !strings.Contains(err.Message, "position 3") { t.Errorf("expected position 3 in error message, got: %s", err.Message) } @@ -655,10 +734,12 @@ func TestPrintableText(t *testing.T) { // Test that position is counted correctly with UTF-8 characters // The range loop in Go iterates by runes, so position will be rune index str := "abc\x00" + err := PrintableText()(&str) if err == nil { t.Fatal("expected validation error") } + // The null byte is at rune position 3 (after 'a', 'b', 'c') if !strings.Contains(err.Message, "position 3") { t.Errorf("expected position 3 in error message, got: %s", err.Message) @@ -669,6 +750,7 @@ func TestPrintableText(t *testing.T) { func TestSafeText(t *testing.T) { t.Run("valid text", func(t *testing.T) { str := "Product Name 2024" + err := SafeText(100)(&str) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -677,6 +759,7 @@ func TestSafeText(t *testing.T) { t.Run("valid UTF-8 text", func(t *testing.T) { str := "José García" + err := SafeText(50)(&str) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -685,6 +768,7 @@ func TestSafeText(t *testing.T) { t.Run("valid text with emoji", func(t *testing.T) { str := "Hello World 🌍" + err := SafeText(50)(&str) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -693,6 +777,7 @@ func TestSafeText(t *testing.T) { t.Run("valid text with apostrophe and hyphen", func(t *testing.T) { str := "O'Brien-Smith" + err := SafeText(50)(&str) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -701,10 +786,12 @@ func TestSafeText(t *testing.T) { t.Run("invalid - empty string", func(t *testing.T) { str := "" + err := SafeText(100)(&str) if err == nil { t.Fatal("expected validation error for empty string") } + if !strings.Contains(err.Message, "empty") && !strings.Contains(err.Message, "required") { t.Errorf("unexpected error message: %s", err.Message) } @@ -712,10 +799,12 @@ func TestSafeText(t *testing.T) { t.Run("invalid - exceeds max length", func(t *testing.T) { str := "This is a very long string that exceeds the maximum length" + err := SafeText(10)(&str) if err == nil { t.Fatal("expected validation error for exceeding max length") } + if !strings.Contains(err.Message, "at most") { t.Errorf("unexpected error message: %s", err.Message) } @@ -723,10 +812,12 @@ func TestSafeText(t *testing.T) { t.Run("invalid - contains HTML tags", func(t *testing.T) { str := "Hello World" + err := SafeText(100)(&str) if err == nil { t.Fatal("expected validation error for HTML tags") } + if !strings.Contains(err.Message, "HTML tags") { t.Errorf("unexpected error message: %s", err.Message) } @@ -734,6 +825,7 @@ func TestSafeText(t *testing.T) { t.Run("invalid - contains script tag", func(t *testing.T) { str := "" + err := SafeText(100)(&str) if err == nil { t.Error("expected validation error for script tag") @@ -742,6 +834,7 @@ func TestSafeText(t *testing.T) { t.Run("valid - contains angle brackets", func(t *testing.T) { str := "5 < 10" + err := SafeText(100)(&str) if err != nil { t.Errorf("expected no error for bare angle brackets, got: %v", err) @@ -750,10 +843,12 @@ func TestSafeText(t *testing.T) { t.Run("invalid - contains null byte", func(t *testing.T) { str := "test\x00text" + err := SafeText(100)(&str) if err == nil { t.Fatal("expected validation error for null byte") } + if !strings.Contains(err.Message, "control character") { t.Errorf("unexpected error message: %s", err.Message) } @@ -761,6 +856,7 @@ func TestSafeText(t *testing.T) { t.Run("invalid - contains tab character", func(t *testing.T) { str := "test\ttext" + err := SafeText(100)(&str) if err == nil { t.Error("expected validation error for tab character") @@ -769,6 +865,7 @@ func TestSafeText(t *testing.T) { t.Run("valid - contains newline", func(t *testing.T) { str := "test\ntext" + err := SafeText(100)(&str) if err != nil { t.Errorf("expected no error for newline, got: %v", err) @@ -777,6 +874,7 @@ func TestSafeText(t *testing.T) { t.Run("valid - contains multiple newlines", func(t *testing.T) { str := "hello foo\nbar\n\njd" + err := SafeText(100)(&str) if err != nil { t.Errorf("expected no error for multiple newlines, got: %v", err) @@ -785,10 +883,12 @@ func TestSafeText(t *testing.T) { t.Run("invalid - contains zero-width space", func(t *testing.T) { str := "test\u200Btext" + err := SafeText(100)(&str) if err == nil { t.Fatal("expected validation error for zero-width space") } + if !strings.Contains(err.Message, "zero-width") { t.Errorf("unexpected error message: %s", err.Message) } @@ -796,10 +896,12 @@ func TestSafeText(t *testing.T) { t.Run("invalid - contains RLO character", func(t *testing.T) { str := "test\u202Eexe.txt" + err := SafeText(100)(&str) if err == nil { t.Fatal("expected validation error for RLO character") } + if !strings.Contains(err.Message, "bidirectional override") { t.Errorf("unexpected error message: %s", err.Message) } @@ -807,10 +909,12 @@ func TestSafeText(t *testing.T) { t.Run("invalid - contains private use area character", func(t *testing.T) { str := "test\uE000text" + err := SafeText(100)(&str) if err == nil { t.Fatal("expected validation error for private use area") } + if !strings.Contains(err.Message, "private use") { t.Errorf("unexpected error message: %s", err.Message) } @@ -818,6 +922,7 @@ func TestSafeText(t *testing.T) { t.Run("nil pointer", func(t *testing.T) { var str *string + err := SafeText(100)(str) if err != nil { t.Errorf("expected no error for nil pointer, got: %v", err) @@ -826,10 +931,12 @@ func TestSafeText(t *testing.T) { t.Run("not a string", func(t *testing.T) { num := 123 + err := SafeText(100)(&num) if err == nil { t.Fatal("expected validation error for non-string") } + if !strings.Contains(err.Message, "must be a string") { t.Errorf("unexpected error message: %s", err.Message) } @@ -856,12 +963,14 @@ func TestSafeText(t *testing.T) { errors := v.Error().(ValidationErrors) found := false + for _, err := range errors { if strings.Contains(err.Message, "HTML tags") { found = true break } } + if !found { t.Error("expected error about HTML tags") } @@ -869,6 +978,7 @@ func TestSafeText(t *testing.T) { t.Run("edge case - exactly at max length", func(t *testing.T) { str := "12345" + err := SafeText(5)(&str) if err != nil { t.Errorf("expected no error for string at max length, got: %v", err) @@ -877,6 +987,7 @@ func TestSafeText(t *testing.T) { t.Run("edge case - one character over max length", func(t *testing.T) { str := "123456" + err := SafeText(5)(&str) if err == nil { t.Error("expected validation error for string over max length") @@ -887,6 +998,7 @@ func TestSafeText(t *testing.T) { func TestNoNewLine(t *testing.T) { t.Run("valid text without newlines", func(t *testing.T) { str := "Product Name 2024" + err := NoNewLine()(&str) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -895,10 +1007,12 @@ func TestNoNewLine(t *testing.T) { t.Run("invalid - contains newline", func(t *testing.T) { str := "Line 1\nLine 2" + err := NoNewLine()(&str) if err == nil { t.Fatal("expected validation error for newline") } + if !strings.Contains(err.Message, "newline") { t.Errorf("unexpected error message: %s", err.Message) } @@ -906,10 +1020,12 @@ func TestNoNewLine(t *testing.T) { t.Run("invalid - contains carriage return", func(t *testing.T) { str := "Line 1\rLine 2" + err := NoNewLine()(&str) if err == nil { t.Fatal("expected validation error for carriage return") } + if !strings.Contains(err.Message, "carriage return") { t.Errorf("unexpected error message: %s", err.Message) } @@ -917,6 +1033,7 @@ func TestNoNewLine(t *testing.T) { t.Run("invalid - contains both newline and carriage return", func(t *testing.T) { str := "Line 1\n\rLine 3" + err := NoNewLine()(&str) if err == nil { t.Error("expected validation error for newline or carriage return") @@ -925,6 +1042,7 @@ func TestNoNewLine(t *testing.T) { t.Run("nil pointer", func(t *testing.T) { var str *string + err := NoNewLine()(str) if err != nil { t.Errorf("expected no error for nil pointer, got: %v", err) @@ -933,6 +1051,7 @@ func TestNoNewLine(t *testing.T) { t.Run("empty string", func(t *testing.T) { str := "" + err := NoNewLine()(&str) if err != nil { t.Errorf("expected no error for empty string, got: %v", err) @@ -943,6 +1062,7 @@ func TestNoNewLine(t *testing.T) { func TestSafeTextNoNewLine(t *testing.T) { t.Run("valid text", func(t *testing.T) { str := "Product Name 2024" + err := SafeTextNoNewLine(100)(&str) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -951,6 +1071,7 @@ func TestSafeTextNoNewLine(t *testing.T) { t.Run("valid UTF-8 text", func(t *testing.T) { str := "José García" + err := SafeTextNoNewLine(50)(&str) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -959,10 +1080,12 @@ func TestSafeTextNoNewLine(t *testing.T) { t.Run("invalid - contains newline", func(t *testing.T) { str := "Line 1\nLine 2" + err := SafeTextNoNewLine(100)(&str) if err == nil { t.Fatal("expected validation error for newline") } + if !strings.Contains(err.Message, "newline") { t.Errorf("unexpected error message: %s", err.Message) } @@ -970,10 +1093,12 @@ func TestSafeTextNoNewLine(t *testing.T) { t.Run("invalid - contains carriage return", func(t *testing.T) { str := "Line 1\rLine 2" + err := SafeTextNoNewLine(100)(&str) if err == nil { t.Fatal("expected validation error for carriage return") } + if !strings.Contains(err.Message, "carriage return") { t.Errorf("unexpected error message: %s", err.Message) } @@ -981,10 +1106,12 @@ func TestSafeTextNoNewLine(t *testing.T) { t.Run("invalid - empty string", func(t *testing.T) { str := "" + err := SafeTextNoNewLine(100)(&str) if err == nil { t.Fatal("expected validation error for empty string") } + if !strings.Contains(err.Message, "empty") && !strings.Contains(err.Message, "required") { t.Errorf("unexpected error message: %s", err.Message) } @@ -992,10 +1119,12 @@ func TestSafeTextNoNewLine(t *testing.T) { t.Run("invalid - exceeds max length", func(t *testing.T) { str := "This is a very long string that exceeds the maximum length" + err := SafeTextNoNewLine(10)(&str) if err == nil { t.Fatal("expected validation error for exceeding max length") } + if !strings.Contains(err.Message, "at most") { t.Errorf("unexpected error message: %s", err.Message) } @@ -1003,10 +1132,12 @@ func TestSafeTextNoNewLine(t *testing.T) { t.Run("invalid - contains HTML tags", func(t *testing.T) { str := "Hello World" + err := SafeTextNoNewLine(100)(&str) if err == nil { t.Fatal("expected validation error for HTML tags") } + if !strings.Contains(err.Message, "HTML tags") { t.Errorf("unexpected error message: %s", err.Message) } @@ -1014,6 +1145,7 @@ func TestSafeTextNoNewLine(t *testing.T) { t.Run("invalid - contains tab character", func(t *testing.T) { str := "test\ttext" + err := SafeTextNoNewLine(100)(&str) if err == nil { t.Error("expected validation error for tab character") @@ -1022,6 +1154,7 @@ func TestSafeTextNoNewLine(t *testing.T) { t.Run("nil pointer", func(t *testing.T) { var str *string + err := SafeTextNoNewLine(100)(str) if err != nil { t.Errorf("expected no error for nil pointer, got: %v", err) @@ -1030,6 +1163,7 @@ func TestSafeTextNoNewLine(t *testing.T) { t.Run("edge case - exactly at max length", func(t *testing.T) { str := "12345" + err := SafeTextNoNewLine(5)(&str) if err != nil { t.Errorf("expected no error for string at max length, got: %v", err) diff --git a/pkg/validator/validator_string.go b/pkg/validator/validator_string.go index e71c06d32..d97671c9e 100644 --- a/pkg/validator/validator_string.go +++ b/pkg/validator/validator_string.go @@ -113,11 +113,13 @@ func OneOfSlice[T any](allowed []T) ValidatorFunc { // Dereference all pointer levels actualValue := value + val := reflect.ValueOf(value) for val.Kind() == reflect.Pointer { if val.IsNil() { return nil } + val = val.Elem() actualValue = val.Interface() } diff --git a/pkg/validator/validator_time_test.go b/pkg/validator/validator_time_test.go index ce95f54f2..17f96b575 100644 --- a/pkg/validator/validator_time_test.go +++ b/pkg/validator/validator_time_test.go @@ -36,6 +36,7 @@ func TestAfter(t *testing.T) { if err == nil { t.Fatal("expected validation error") } + if err.Code != ErrorCodeOutOfRange { t.Errorf("expected error code %s, got %s", ErrorCodeOutOfRange, err.Code) } @@ -50,6 +51,7 @@ func TestAfter(t *testing.T) { t.Run("nil pointer", func(t *testing.T) { var timeVal *time.Time + err := After(now)(timeVal) if err != nil { t.Errorf("expected no error for nil, got: %v", err) @@ -74,6 +76,7 @@ func TestBefore(t *testing.T) { if err == nil { t.Fatal("expected validation error") } + if err.Code != ErrorCodeOutOfRange { t.Errorf("expected error code %s, got %s", ErrorCodeOutOfRange, err.Code) } @@ -88,6 +91,7 @@ func TestBefore(t *testing.T) { t.Run("nil pointer", func(t *testing.T) { var timeVal *time.Time + err := Before(now)(timeVal) if err != nil { t.Errorf("expected no error for nil, got: %v", err) @@ -101,6 +105,7 @@ func TestRangeDuration(t *testing.T) { t.Run("duration within range", func(t *testing.T) { duration := 30 * time.Minute + err := RangeDuration(minDuration, maxDuration)(&duration) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -109,6 +114,7 @@ func TestRangeDuration(t *testing.T) { t.Run("duration at minimum", func(t *testing.T) { duration := 10 * time.Minute + err := RangeDuration(minDuration, maxDuration)(&duration) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -117,6 +123,7 @@ func TestRangeDuration(t *testing.T) { t.Run("duration at maximum", func(t *testing.T) { duration := 1 * time.Hour + err := RangeDuration(minDuration, maxDuration)(&duration) if err != nil { t.Errorf("expected no error, got: %v", err) @@ -125,10 +132,12 @@ func TestRangeDuration(t *testing.T) { t.Run("duration below minimum", func(t *testing.T) { duration := 5 * time.Minute + err := RangeDuration(minDuration, maxDuration)(&duration) if err == nil { t.Fatal("expected validation error") } + if err.Code != ErrorCodeOutOfRange { t.Errorf("expected error code %s, got %s", ErrorCodeOutOfRange, err.Code) } @@ -136,10 +145,12 @@ func TestRangeDuration(t *testing.T) { t.Run("duration above maximum", func(t *testing.T) { duration := 2 * time.Hour + err := RangeDuration(minDuration, maxDuration)(&duration) if err == nil { t.Fatal("expected validation error") } + if err.Code != ErrorCodeOutOfRange { t.Errorf("expected error code %s, got %s", ErrorCodeOutOfRange, err.Code) } @@ -147,6 +158,7 @@ func TestRangeDuration(t *testing.T) { t.Run("nil pointer", func(t *testing.T) { var duration *time.Duration + err := RangeDuration(minDuration, maxDuration)(duration) if err != nil { t.Errorf("expected no error for nil, got: %v", err) diff --git a/pkg/version/version.go b/pkg/version/version.go index 34b5117e3..c3f737c05 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -90,6 +90,7 @@ func UserAgent(component string) string { if info.Commit != "unknown" { metadata = append(metadata, fmt.Sprintf("commit=%s", info.Commit)) } + if info.BuildDate != "unknown" { metadata = append(metadata, fmt.Sprintf("built=%s", info.BuildDate)) } diff --git a/pkg/vetting/assessment.go b/pkg/vetting/assessment.go index c27bd60a6..633b86fc0 100644 --- a/pkg/vetting/assessment.go +++ b/pkg/vetting/assessment.go @@ -178,9 +178,11 @@ func (a *Assessor) Assess(ctx context.Context, websiteURL string, procedure stri if err != nil { return nil, fmt.Errorf("cannot parse website URL %q: %w", websiteURL, err) } + if u.Scheme != "http" && u.Scheme != "https" { return nil, fmt.Errorf("website URL must use http or https, got %q", u.Scheme) } + if u.Hostname() == "" { return nil, fmt.Errorf("website URL %q has no host", websiteURL) } @@ -324,6 +326,7 @@ func thirdPartyInfoOutputType() (*agent.OutputType, error) { if !ok { return nil, fmt.Errorf("thirdParty info schema has no %q property", field) } + prop["enum"] = values } @@ -331,6 +334,7 @@ func thirdPartyInfoOutputType() (*agent.OutputType, error) { if err != nil { return nil, fmt.Errorf("cannot marshal decorated thirdParty info schema: %w", err) } + outputType.Schema = decorated return outputType, nil diff --git a/pkg/vetting/assessment_test.go b/pkg/vetting/assessment_test.go index 5273b0960..9107d453c 100644 --- a/pkg/vetting/assessment_test.go +++ b/pkg/vetting/assessment_test.go @@ -60,6 +60,7 @@ func TestThirdPartyInfoOutputType_DecoratesEnums(t *testing.T) { for i, v := range enumRaw { actual[i] = v.(string) } + assert.Equal(t, tt.expected, actual) }) } diff --git a/pkg/vetting/orchestrator.go b/pkg/vetting/orchestrator.go index 3becd5e42..a28c1d736 100644 --- a/pkg/vetting/orchestrator.go +++ b/pkg/vetting/orchestrator.go @@ -85,6 +85,7 @@ func newOrchestratorAgent( if reporter != nil { opts = append(opts, agent.WithHooks(newSubProgressHooks(reporter, step))) } + return opts } @@ -190,6 +191,7 @@ func newOrchestratorAgent( out := make([]agent.Tool, 0, len(extra)+len(researchBrowserTools)) out = append(out, extra...) out = append(out, researchBrowserTools...) + return out } @@ -232,12 +234,14 @@ func newOrchestratorAgent( if err != nil { return nil, fmt.Errorf("cannot create %s sub-agent: %w", e.toolName, err) } + tools = append(tools, ag.AsTool(e.toolName, e.description)) } if procedure == "" { procedure = defaultProcedure } + systemPrompt := strings.Replace(orchestratorBasePrompt, "{procedure}", procedure, 1) opts := []agent.Option{ diff --git a/pkg/vetting/sub_agent.go b/pkg/vetting/sub_agent.go index e63537ec8..3e331d8a7 100644 --- a/pkg/vetting/sub_agent.go +++ b/pkg/vetting/sub_agent.go @@ -73,9 +73,11 @@ func newSubAgent[T any]( if spec.thinkingBudget > 0 { opts = append(opts, agent.WithThinking(spec.thinkingBudget)) } + if spec.parallelTools { opts = append(opts, agent.WithParallelToolCalls(true)) } + opts = append(opts, extraOpts...) return agent.New(spec.name, client, opts...), nil diff --git a/pkg/webhook/data.go b/pkg/webhook/data.go index 65f1f9ed3..bab4a782f 100644 --- a/pkg/webhook/data.go +++ b/pkg/webhook/data.go @@ -43,6 +43,7 @@ func InsertData( data any, ) error { var configs coredata.WebhookSubscriptions + exists, err := configs.ExistsByOrganizationIDAndEventType(ctx, tx, scope, organizationID, eventType) if err != nil { return fmt.Errorf("cannot check webhook subscriptions: %w", err) diff --git a/pkg/webhook/sender.go b/pkg/webhook/sender.go index 6176f4a71..fc49db04d 100644 --- a/pkg/webhook/sender.go +++ b/pkg/webhook/sender.go @@ -123,6 +123,7 @@ func (s *Sender) processEvents(ctx context.Context) error { if errors.Is(err, coredata.ErrResourceNotFound) { return nil } + return fmt.Errorf("cannot claim next webhook data: %w", err) } @@ -131,8 +132,10 @@ func (s *Sender) processEvents(ctx context.Context) error { } func (s *Sender) claimNextWebhookData(ctx context.Context) (*coredata.WebhookData, []pendingDelivery, error) { - var webhookData coredata.WebhookData - var deliveries []pendingDelivery + var ( + webhookData coredata.WebhookData + deliveries []pendingDelivery + ) err := s.pg.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { if err := webhookData.LoadNextUnprocessedForUpdate(ctx, tx); err != nil { @@ -180,7 +183,6 @@ func (s *Sender) claimNextWebhookData(ctx context.Context) (*coredata.WebhookDat return nil }) - if err != nil { return nil, nil, err } @@ -207,6 +209,7 @@ func (s *Sender) deliver(ctx context.Context, webhookData *coredata.WebhookData, log.String("subscription_id", d.Config.ID.String()), ) s.updateEventStatus(ctx, d.Event, scope, coredata.WebhookEventStatusFailed, nil) + return } @@ -215,6 +218,7 @@ func (s *Sender) deliver(ctx context.Context, webhookData *coredata.WebhookData, eventStatus := coredata.WebhookEventStatusSucceeded if sendErr != nil { eventStatus = coredata.WebhookEventStatusFailed + s.logger.ErrorCtx( ctx, "error delivering webhook", @@ -292,6 +296,7 @@ func (s *Sender) doHTTPCall( CreatedAt: webhookData.CreatedAt, Data: webhookData.Data, } + body, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("cannot marshal webhook payload: %w", err) @@ -319,6 +324,7 @@ func (s *Sender) doHTTPCall( if err != nil { return nil, fmt.Errorf("cannot send request: %w", err) } + defer func() { _ = resp.Body.Close() }() respBody, _ := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize)) @@ -369,15 +375,18 @@ func buildResponseJSON(resp *http.Response, body []byte) json.RawMessage { trailers[k] = v } } + respObj["trailers"] = trailers } data, _ := json.Marshal(respObj) + return data } func computeSignature(signingSecret, timestamp string, body []byte) string { h := hmac.New(sha256.New, []byte(signingSecret)) _, _ = fmt.Fprintf(h, "%s:%s", timestamp, body) + return hex.EncodeToString(h.Sum(nil)) }