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<") + 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 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("