Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creates User Lookup #153

Merged
merged 3 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions cmd/server/pactasrv/conv/oapi_to_pacta.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,3 +314,35 @@ func AuditLogQueryFromOAPI(q *api.AuditLogQueryReq) (*db.AuditLogQuery, error) {
Sorts: sorts,
}, nil
}

func userQueryWhereFromOAPI(i api.UserQueryWhere) (*db.UserQueryWhere, error) {
result := &db.UserQueryWhere{}
if i.NameOrEmailLike != nil {
result.NameOrEmailLike = *i.NameOrEmailLike
}
return result, nil
}

func UserQueryFromOAPI(q *api.UserQueryReq) (*db.UserQuery, error) {
limit := 100
cursor := ""
if q.Cursor != nil {
cursor = *q.Cursor
}
wheres := []api.UserQueryWhere{}
if q.Wheres != nil {
wheres = append(wheres, *q.Wheres...)
}
ws, err := convAll(wheres, userQueryWhereFromOAPI)
if err != nil {
return nil, oapierr.BadRequest("error converting user query wheres", zap.Error(err))
}
return &db.UserQuery{
Cursor: db.Cursor(cursor),
Limit: limit,
Wheres: ws,
Sorts: []*db.UserQuerySort{
{By: db.UserQuerySortBy_CreatedAt, Ascending: false},
},
}, nil
}
4 changes: 4 additions & 0 deletions cmd/server/pactasrv/conv/pacta_to_oapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ func portfolioInitiativeMembershipToOAPIInitiative(in *pacta.PortfolioInitiative
return out, nil
}

func UsersToOAPI(users []*pacta.User) ([]*api.User, error) {
return convAll(users, UserToOAPI)
}

func UserToOAPI(user *pacta.User) (*api.User, error) {
if user == nil {
return nil, oapierr.Internal("userToOAPI: can't convert nil pointer")
Expand Down
1 change: 1 addition & 0 deletions cmd/server/pactasrv/pactasrv.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ type DB interface {
Users(tx db.Tx, ids []pacta.UserID) (map[pacta.UserID]*pacta.User, error)
UpdateUser(tx db.Tx, id pacta.UserID, mutations ...db.UpdateUserFn) error
DeleteUser(tx db.Tx, id pacta.UserID) ([]pacta.BlobURI, error)
QueryUsers(tx db.Tx, q *db.UserQuery) ([]*pacta.User, *db.PageInfo, error)

CreateAuditLog(tx db.Tx, a *pacta.AuditLog) (pacta.AuditLogID, error)
CreateAuditLogs(tx db.Tx, as []*pacta.AuditLog) error
Expand Down
28 changes: 28 additions & 0 deletions cmd/server/pactasrv/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,34 @@ func (s *Server) UserAuthenticationFollowup(ctx context.Context, _request api.Us
return api.UserAuthenticationFollowup200JSONResponse(*result), nil
}

// (GET /users)
func (s *Server) UserQuery(ctx context.Context, request api.UserQueryRequestObject) (api.UserQueryResponseObject, error) {
actorInfo, err := s.getActorInfoOrErrIfAnon(ctx)
if err != nil {
return nil, err
}
if !actorInfo.IsAdmin && !actorInfo.IsSuperAdmin {
return nil, oapierr.Unauthorized("only admins can list users")
}
q, err := conv.UserQueryFromOAPI(request.Body)
if err != nil {
return nil, err
}
us, pi, err := s.DB.QueryUsers(s.DB.NoTxn(ctx), q)
if err != nil {
return nil, oapierr.Internal("failed to query users", zap.Error(err))
}
users, err := dereference(conv.UsersToOAPI(us))
if err != nil {
return nil, err
}
return api.UserQuery200JSONResponse{
Users: users,
Cursor: string(pi.Cursor),
HasNextPage: pi.HasNextPage,
}, nil
}

func (s *Server) userDoAuthzAndAuditLog(ctx context.Context, targetUserID pacta.UserID, action pacta.AuditLogAction) error {
actorInfo, err := s.getActorInfoOrErrIfAnon(ctx)
if err != nil {
Expand Down
22 changes: 22 additions & 0 deletions db/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,25 @@ type AuditLogQuery struct {
Wheres []*AuditLogQueryWhere
Sorts []*AuditLogQuerySort
}

type UserQuerySortBy string

const (
UserQuerySortBy_CreatedAt UserQuerySortBy = "created_at"
)

type UserQuerySort struct {
By UserQuerySortBy
Ascending bool
}

type UserQueryWhere struct {
NameOrEmailLike string
}

type UserQuery struct {
Cursor Cursor
Limit int
Wheres []*UserQueryWhere
Sorts []*UserQuerySort
}
2 changes: 2 additions & 0 deletions db/sqldb/golden/human_readable_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ ALTER TABLE ONLY pacta_user ADD CONSTRAINT pacta_user_authn_mechanism_authn_id_k
ALTER TABLE ONLY pacta_user ADD CONSTRAINT pacta_user_canonical_email_key UNIQUE (canonical_email);
ALTER TABLE ONLY pacta_user ADD CONSTRAINT pacta_user_entered_email_key UNIQUE (entered_email);
ALTER TABLE ONLY pacta_user ADD CONSTRAINT pacta_user_pkey PRIMARY KEY (id);
CREATE INDEX user_canonical_email_gin_index ON pacta_user USING gin (canonical_email gin_trgm_ops);
CREATE INDEX user_name_gin_index ON pacta_user USING gin (name gin_trgm_ops);


CREATE TABLE pacta_version (
Expand Down
28 changes: 28 additions & 0 deletions db/sqldb/golden/schema_dump.sql
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;

--
-- Name: pg_trgm; Type: EXTENSION; Schema: -; Owner: -
--

CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public;


--
-- Name: EXTENSION pg_trgm; Type: COMMENT; Schema: -; Owner:
--

COMMENT ON EXTENSION pg_trgm IS 'text similarity measurement and index searching based on trigrams';


--
-- Name: analysis_type; Type: TYPE; Schema: public; Owner: postgres
--
Expand Down Expand Up @@ -749,6 +763,20 @@ CREATE INDEX owner_by_user_id ON public.owner USING btree (user_id);
CREATE INDEX portfolio_by_blob_id ON public.portfolio USING btree (blob_id);


--
-- Name: user_canonical_email_gin_index; Type: INDEX; Schema: public; Owner: postgres
--

CREATE INDEX user_canonical_email_gin_index ON public.pacta_user USING gin (canonical_email public.gin_trgm_ops);


--
-- Name: user_name_gin_index; Type: INDEX; Schema: public; Owner: postgres
--

CREATE INDEX user_name_gin_index ON public.pacta_user USING gin (name public.gin_trgm_ops);


--
-- Name: schema_migrations track_applied_migrations; Type: TRIGGER; Schema: public; Owner: postgres
--
Expand Down
6 changes: 6 additions & 0 deletions db/sqldb/migrations/0013_user_search.down.sql
gbdubs marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
BEGIN;

DROP INDEX user_name_gin_index;
DROP INDEX user_canonical_email_gin_index;

COMMIT;
9 changes: 9 additions & 0 deletions db/sqldb/migrations/0013_user_search.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
BEGIN;

CREATE EXTENSION IF NOT EXISTS pg_trgm;
gbdubs marked this conversation as resolved.
Show resolved Hide resolved
SET pg_trgm.similarity_threshold = 0.1;
gbdubs marked this conversation as resolved.
Show resolved Hide resolved

CREATE INDEX user_name_gin_index ON pacta_user USING gin (name gin_trgm_ops);
CREATE INDEX user_canonical_email_gin_index ON pacta_user USING gin (canonical_email gin_trgm_ops);

COMMIT;
1 change: 1 addition & 0 deletions db/sqldb/sqldb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ func TestSchemaHistory(t *testing.T) {
{ID: 10, Version: 10}, // 0010_audit_log_enum_values
{ID: 11, Version: 11}, // 0011_add_report_file_types
{ID: 12, Version: 12}, // 0012_portfolio_properties
{ID: 13, Version: 13}, // 0013_user_search
}

if diff := cmp.Diff(want, got); diff != "" {
Expand Down
79 changes: 79 additions & 0 deletions db/sqldb/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sqldb

import (
"fmt"
"strings"

"github.com/RMI/pacta/db"
"github.com/RMI/pacta/pacta"
Expand Down Expand Up @@ -105,6 +106,84 @@ func (d *DB) Users(tx db.Tx, ids []pacta.UserID) (map[pacta.UserID]*pacta.User,
return result, nil
}

func (d *DB) QueryUsers(tx db.Tx, q *db.UserQuery) ([]*pacta.User, *db.PageInfo, error) {
if q.Limit <= 0 {
return nil, nil, fmt.Errorf("limit must be greater than 0, was %d", q.Limit)
}
offset, err := offsetFromCursor(q.Cursor)
if err != nil {
return nil, nil, fmt.Errorf("converting cursor to offset: %w", err)
}
sql, args, err := userQuery(q)
if err != nil {
return nil, nil, fmt.Errorf("building user query: %w", err)
}
rows, err := d.query(tx, sql, args...)
if err != nil {
return nil, nil, fmt.Errorf("executing user query: %w", err)
}
us, err := rowsToUsers(rows)
if err != nil {
return nil, nil, fmt.Errorf("getting users from rows: %w", err)
}
// This will incorrectly say "yes there are more results" if we happen to hit the actual limit, but
// that's a pretty small performance loss.
hasNextPage := len(us) == q.Limit
cursor := offsetToCursor(offset + len(us))
return us, &db.PageInfo{HasNextPage: hasNextPage, Cursor: db.Cursor(cursor)}, nil
}

func userQuery(q *db.UserQuery) (string, []any, error) {
args := &queryArgs{}
selectFrom := `SELECT ` + userSelectColumns + ` FROM pacta_user`
where := userQueryWheresToSQL(q.Wheres, args)
order := userQuerySortsToSQL(q.Sorts)
limit := fmt.Sprintf("LIMIT %d", q.Limit)
offset := ""
if q.Cursor != "" {
o, err := offsetFromCursor(q.Cursor)
if err != nil {
return "", nil, fmt.Errorf("extracting offset from cursor in audit-log query: %w", err)
}
offset = fmt.Sprintf("OFFSET %d", o)
}
sql := fmt.Sprintf("%s %s %s %s %s;", selectFrom, where, order, limit, offset)
return sql, args.values, nil
}

func userQuerySortsToSQL(ss []*db.UserQuerySort) string {
sorts := []string{}
for _, s := range ss {
v := " DESC"
if s.Ascending {
v = " ASC"
}
sorts = append(sorts, fmt.Sprintf("pacta_user.%s %s", s.By, v))
}
// Forces a deterministic sort for pagination.
sorts = append(sorts, "pacta_user.id ASC")
return "ORDER BY " + strings.Join(sorts, ", ")
}

func userQueryWheresToSQL(qs []*db.UserQueryWhere, args *queryArgs) string {
wheres := []string{}
for _, q := range qs {
if q.NameOrEmailLike != "" {
wheres = append(wheres,
fmt.Sprintf(
`name ILIKE ('%[1]s' || %[2]s || '%[1]s')
OR
canonical_email ILIKE ('%[1]s' || %[2]s || '%[1]s')`,
"%",
args.add(q.NameOrEmailLike)))
gbdubs marked this conversation as resolved.
Show resolved Hide resolved
}
}
if len(wheres) == 0 {
return ""
}
return "WHERE " + strings.Join(wheres, " AND ")
}

func (d *DB) createUser(tx db.Tx, u *pacta.User) (pacta.UserID, error) {
if err := validateUserForCreation(u); err != nil {
return "", fmt.Errorf("validating user for creation: %w", err)
Expand Down
Loading