various improvements to saved searches#63539
Conversation
dadf645 to
1288e3f
Compare
4029982 to
965bfe9
Compare
1288e3f to
f3c38f9
Compare
e89f99d to
34a18ab
Compare
f3c38f9 to
117daec
Compare
915302a to
600bab0
Compare
117daec to
87057f5
Compare
644de16 to
09a049e
Compare
Check notice
Code scanning / Semgrep OSS
Semgrep Finding: security-semgrep-rules.semgrep-rules.generic.comment-tagging-rule
2d59b24 to
460f423
Compare
19d47ec to
0821d14
Compare
460f423 to
40b67f5
Compare
| // 🚨 SECURITY: Make sure the current user has permission for the specified namespace. | ||
| if err := CheckAuthorizedForNamespaceByIDs(ctx, db, *namespace); err != nil { | ||
| return nil, err | ||
| } | ||
| return namespace, nil |
Check notice
Code scanning / Semgrep OSS
Semgrep Finding: security-semgrep-rules.semgrep-rules.generic.comment-tagging-rule
| // 🚨 SECURITY: Make sure the current user has permission to get the saved search. | ||
| if err := graphqlbackend.CheckAuthorizedForNamespaceByIDs(ctx, r.db, ss.Owner); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| savedSearch := &savedSearchResolver{ | ||
| db: r.db, | ||
| s: *ss, | ||
| } | ||
| return savedSearch, nil |
Check notice
Code scanning / Semgrep OSS
Semgrep Finding: security-semgrep-rules.semgrep-rules.generic.comment-tagging-rule
| // 🚨 SECURITY: Make sure the current user has permission to view saved searches of the | ||
| // specified owner. | ||
| owner, err := graphqlbackend.CheckAuthorizedForNamespace(ctx, r.db, *args.Owner) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| connectionStore.listArgs.Owner = owner |
Check notice
Code scanning / Semgrep OSS
Semgrep Finding: security-semgrep-rules.semgrep-rules.generic.comment-tagging-rule
| // 🚨 SECURITY: Make sure the current user has permission to create a saved search in the | ||
| // specified owner namespace. | ||
| namespace, err := graphqlbackend.CheckAuthorizedForNamespace(ctx, r.db, args.Input.Owner) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| if !queryHasPatternType(args.Input.Query) { | ||
| return nil, errMissingPatternType | ||
| } | ||
|
|
||
| ss, err := r.db.SavedSearches().Create(ctx, &types.SavedSearch{ | ||
| Description: args.Input.Description, | ||
| Query: args.Input.Query, | ||
| Owner: *namespace, | ||
| }) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return r.toSavedSearchResolver(*ss), nil |
Check notice
Code scanning / Semgrep OSS
Semgrep Finding: security-semgrep-rules.semgrep-rules.generic.comment-tagging-rule
| // 🚨 SECURITY: ...and check the user can administer saved searches in the new owner's | ||
| // namespace: | ||
| newOwner, err := graphqlbackend.CheckAuthorizedForNamespace(ctx, r.db, args.NewOwner) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| ss, err = r.db.SavedSearches().UpdateOwner(ctx, id, *newOwner) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return r.toSavedSearchResolver(*ss), nil |
Check notice
Code scanning / Semgrep OSS
Semgrep Finding: security-semgrep-rules.semgrep-rules.generic.comment-tagging-rule
| // 🚨 SECURITY: This method does NOT verify the user's identity or that the user is an admin. It is | ||
| // the caller's responsibility to ensure the user has proper permissions to perform the update. | ||
| func (s *savedSearchStore) Update(ctx context.Context, savedSearch *types.SavedSearch) (updated *types.SavedSearch, err error) { | ||
| tr, ctx := trace.New(ctx, "database.SavedSearches.Update") | ||
| defer tr.EndWithErr(&err) | ||
|
|
||
| q := sqlf.Sprintf(`SELECT | ||
| id, | ||
| description, | ||
| query, | ||
| notify_owner, | ||
| notify_slack, | ||
| user_id, | ||
| org_id, | ||
| slack_webhook_url FROM saved_searches | ||
| `) | ||
| rows, err := s.Query(ctx, q) | ||
| if err != nil { | ||
| return nil, errors.Wrap(err, "QueryContext") | ||
| } | ||
| return s.update(ctx, savedSearch.ID, []*sqlf.Query{ | ||
| sqlf.Sprintf("description=%s", savedSearch.Description), | ||
| sqlf.Sprintf("query=%s", savedSearch.Query), | ||
| }) | ||
| } | ||
|
|
||
| for rows.Next() { | ||
| var sq api.SavedQuerySpecAndConfig | ||
| if err := rows.Scan( | ||
| &sq.Config.Key, | ||
| &sq.Config.Description, | ||
| &sq.Config.Query, | ||
| &sq.Config.Notify, | ||
| &sq.Config.NotifySlack, | ||
| &sq.Config.UserID, | ||
| &sq.Config.OrgID, | ||
| &sq.Config.SlackWebhookURL); err != nil { | ||
| return nil, errors.Wrap(err, "Scan") | ||
| } | ||
| sq.Spec.Key = sq.Config.Key | ||
| if sq.Config.UserID != nil { | ||
| sq.Spec.Subject.User = sq.Config.UserID | ||
| } else if sq.Config.OrgID != nil { | ||
| sq.Spec.Subject.Org = sq.Config.OrgID | ||
| } | ||
| // UpdateOwner updates the owner of an existing saved search. | ||
| // | ||
| // 🚨 SECURITY: This method does NOT verify that the user has permissions to do this. The caller | ||
| // MUST do so. | ||
| func (s *savedSearchStore) UpdateOwner(ctx context.Context, id int32, newOwner types.Namespace) (updated *types.SavedSearch, err error) { | ||
| tr, ctx := trace.New(ctx, "database.SavedSearches.UpdateOwner") | ||
| defer tr.EndWithErr(&err) | ||
| return s.update(ctx, id, []*sqlf.Query{ | ||
| sqlf.Sprintf("user_id=%v", newOwner.User), | ||
| sqlf.Sprintf("org_id=%v", newOwner.Org), | ||
| }) | ||
| } | ||
|
|
||
| savedSearches = append(savedSearches, sq) | ||
| } | ||
| return savedSearches, nil | ||
| func (s *savedSearchStore) update(ctx context.Context, id int32, updates []*sqlf.Query) (updated *types.SavedSearch, err error) { | ||
| updates = append(updates, sqlf.Sprintf("updated_at=now()")) | ||
| return scanSavedSearch( | ||
| s.QueryRow(ctx, | ||
| sqlf.Sprintf( | ||
| `UPDATE saved_searches SET %s WHERE id=%v RETURNING id, %v`, |
Check notice
Code scanning / Semgrep OSS
Semgrep Finding: security-semgrep-rules.semgrep-rules.generic.comment-tagging-rule
659dc81 to
b9ab5c0
Compare
Check notice
Code scanning / Semgrep OSS
Semgrep Finding: security-semgrep-rules.semgrep-rules.generic.comment-tagging-rule
Check notice
Code scanning / Semgrep OSS
Semgrep Finding: security-semgrep-rules.semgrep-rules.generic.comment-tagging-rule
Check notice
Code scanning / Semgrep OSS
Semgrep Finding: security-semgrep-rules.semgrep-rules.generic.comment-tagging-rule
Check notice
Code scanning / Semgrep OSS
Semgrep Finding: security-semgrep-rules.semgrep-rules.generic.comment-tagging-rule
Check notice
Code scanning / Semgrep OSS
Semgrep Finding: security-semgrep-rules.semgrep-rules.generic.comment-tagging-rule
Check notice
Code scanning / Semgrep OSS
Semgrep Finding: security-semgrep-rules.semgrep-rules.generic.comment-tagging-rule
Check notice
Code scanning / Semgrep OSS
Semgrep Finding: security-semgrep-rules.semgrep-rules.generic.comment-tagging-rule
Check notice
Code scanning / Semgrep OSS
Semgrep Finding: security-semgrep-rules.semgrep-rules.generic.comment-tagging-rule
745d1b9 to
5f87978
Compare
camdencheek
left a comment
There was a problem hiding this comment.
I attempted to give this a real review (and also pulled it down and did some manual poking around), but it's also large, so relying a bit on your confidence 🙂
There was a problem hiding this comment.
Thank you for splitting this out into its own file!
| """ | ||
| The owner of the saved search, either a user or organization. | ||
| """ | ||
| owner: Namespace! |
There was a problem hiding this comment.
Maybe you came across this during the refactor: do you know why we have a special type for Namespace? Seems like it would be more ergonomic to just type it in GraphQL as User | Org.
There was a problem hiding this comment.
Because we’ll want it to also support global and team namespaces. That was part of the original idea from years ago, and I think it’s finally nearing time for that again (with multi-tenant private code this year and supporting groupings within an org).
| return &types.SavedSearches{ | ||
| TotalSavedSearches: int32(totalSavedSearches), | ||
| UniqueUsers: int32(uniqueUsers), | ||
| NotificationsSent: int32(notificationsSent), | ||
| NotificationsClicked: int32(notificationsClicked), | ||
| UniqueUserPageViews: int32(uniqueUserPageViews), | ||
| OrgSavedSearches: int32(orgSavedSearches), | ||
| }, nil |
There was a problem hiding this comment.
@dadlerj said:
That JSON file should be updated yes and then the changes have to be published to BigQuery. I'll file a ticket on my team for us to go do all this after your PR.
It's fine to merge now though, unless this is an urgent patch release going out to customers asap... It'll be a while til this goes live and our changes only need to be done on our side.
There was a problem hiding this comment.
Nice, thanks for following up 👍
| -- no default value | ||
| ALTER TABLE saved_searches ALTER COLUMN notify_owner DROP DEFAULT; | ||
| ALTER TABLE saved_searches ALTER COLUMN notify_slack DROP DEFAULT; |
There was a problem hiding this comment.
Thanks! I forgot to ever follow up on removing these columns.
1933074 to
67dafc0
Compare
Check notice
Code scanning / Semgrep OSS
Semgrep Finding: security-semgrep-rules.semgrep-rules.generic.comment-tagging-rule
Check notice
Code scanning / Semgrep OSS
Semgrep Finding: security-semgrep-rules.semgrep-rules.generic.comment-tagging-rule
1851293 to
1fbf274
Compare
Check notice
Code scanning / Semgrep OSS
Semgrep Finding: security-semgrep-rules.semgrep-rules.generic.comment-tagging-rule
5747699 to
63cb9ce
Compare
63cb9ce to
919aa0e
Compare
919aa0e to
aa65e93
Compare
- Remove long-deprecated and long-ineffective notifications for saved searches (removed in https://sourcegraph.com/github.com/sourcegraph/sourcegraph/-/commit/de8ae5ee28e5d942fe30c5493fd39b63fd1233f6 2.5 years ago). Note that code monitors were the replacement for saved searches and work great. - Clean up UI. - Make the UI global instead of in the user/org area. - Convert React class components to function components. - Add default `patterntype:` because it's required. - Use `useQuery` and `useMutation` instead of `requestGraphQL`. - Use a single namespace `owner` GraphQL arg instead of separating out `userID` and `orgID`. - Clean up GraphQL resolver code and factor out common auth checking. - Support transferring ownership of saved searches among owners (the user's own user account and the orgs they're a member of). (I know this is not in Svelte.) SECURITY: There is one substantive change. Site admins may now view any user's and any org's saved searches. This is so that they can audit and delete them if needed.     Test plan: Try creating, updating, and deleting saved searches, and transferring ownership of them. Changelog: - Improved the saved searches feature, which lets you save search queries to easily reuse them later and share them with other people in an organization. - Added the ability to transfer ownership of a saved search to a user's organizations or from an organization to a user's account. - Removed a long-deprecated and ineffective settings `search.savedQueries` field. You can manage saved searches in a user's or organization's profile area (e.g., at `/user/searches`).
aa65e93 to
37928cb
Compare
patterntype:because it's required.useQueryanduseMutationinstead ofrequestGraphQL.ownerGraphQL arg instead of separating outuserIDandorgID.(I know this is not in Svelte.)
SECURITY: There is one substantive change. Site admins may now view any user's and any org's saved searches. This is so that they can audit and delete them if needed.
Test plan
Try creating, updating, and deleting saved searches, and transferring ownership of them.
Changelog
search.savedQueriesfield. You can manage saved searches in a user's or organization's profile area (e.g., at/user/searches).