Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
54b8114
Merge dev into master
google-oss-bot May 21, 2020
cef91ac
Merge dev into master
google-oss-bot Jun 16, 2020
77177c7
Merge dev into master
google-oss-bot Oct 22, 2020
a957589
Merge dev into master
google-oss-bot Jan 28, 2021
eb0d2a0
Merge dev into master
google-oss-bot Mar 24, 2021
05378ef
Merge dev into master
google-oss-bot Mar 29, 2021
4121c50
Merge dev into master
google-oss-bot Apr 14, 2021
928b104
Merge dev into master
google-oss-bot Jun 2, 2021
02cde4f
Merge dev into master
google-oss-bot Nov 4, 2021
6b40682
Merge dev into master
google-oss-bot Dec 15, 2021
e60757f
Merge dev into master
google-oss-bot Jan 20, 2022
bb055ed
Merge dev into master
google-oss-bot Apr 6, 2022
23a1f17
Merge dev into master
google-oss-bot Oct 6, 2022
1d24577
Merge dev into master
google-oss-bot Nov 10, 2022
61c6c04
Merge dev into master
google-oss-bot Apr 6, 2023
32af2b8
[chore] Release 4.12.0 (#561)
lahirumaramba Jun 22, 2023
02300a8
Revert "[chore] Release 4.12.0 (#561)" (#565)
lahirumaramba Jul 11, 2023
74c9bd5
Merge dev into master
google-oss-bot Jul 12, 2023
37c7936
Merge dev into master
google-oss-bot Sep 25, 2023
b04387e
Merge dev into master
google-oss-bot Nov 23, 2023
87b867c
Merge dev into master
google-oss-bot Apr 10, 2024
6a28190
Merge dev into master
google-oss-bot May 30, 2024
c3be6f2
Merge dev into master
google-oss-bot Oct 24, 2024
afeaa15
Merge dev into master
google-oss-bot Dec 5, 2024
570427a
Merge dev into master
google-oss-bot Feb 13, 2025
fe866a0
Merge dev into master
google-oss-bot Jun 5, 2025
db240e4
Merge dev into master
google-oss-bot Jun 11, 2025
d515faf
Merge dev into master
google-oss-bot Jul 17, 2025
26dec0b
Merge dev into master
google-oss-bot Jul 31, 2025
7a852d2
feat: Implement support for accounts:query
google-labs-jules[bot] Nov 18, 2025
008866f
fix: Correct tenant query test and request struct
google-labs-jules[bot] Nov 18, 2025
2fe0e14
refactor: Move tenant query test to tenant_mgt_test.go
google-labs-jules[bot] Nov 18, 2025
9d71ff3
style: Rename SqlExpression to SQLExpression
google-labs-jules[bot] Nov 18, 2025
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
28 changes: 28 additions & 0 deletions auth/tenant_mgt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,34 @@ func TestTenantGetUser(t *testing.T) {
}
}

func TestTenantQueryUsers(t *testing.T) {
resp := `{
"usersInfo": [],
"recordsCount": "0"
}`
s := echoServer([]byte(resp), t)
defer s.Close()

tenantClient, err := s.Client.TenantManager.AuthForTenant("test-tenant")
if err != nil {
t.Fatalf("Failed to create tenant client: %v", err)
}

query := &QueryUsersRequest{
ReturnUserInfo: true,
}

_, err = tenantClient.QueryUsers(context.Background(), query)
if err != nil {
t.Fatalf("QueryUsers() with tenant client = %v", err)
}

wantPath := "/projects/mock-project-id/tenants/test-tenant/accounts:query"
if s.Req[0].RequestURI != wantPath {
t.Errorf("QueryUsers() URL = %q; want = %q", s.Req[0].RequestURI, wantPath)
}
}

func TestTenantGetUserByEmail(t *testing.T) {
s := echoServer(testGetUserResponse, t)
defer s.Close()
Expand Down
82 changes: 82 additions & 0 deletions auth/user_mgt.go
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,61 @@ type getAccountInfoResponse struct {
Users []*userQueryResponse `json:"users"`
}

// QueryUserInfoResponse is the response structure for the accounts:query endpoint.
type QueryUserInfoResponse struct {
Users []*UserRecord
Count string
}

type queryUsersResponse struct {
Users []*userQueryResponse `json:"usersInfo,omitempty"`
Count string `json:"recordsCount,omitempty"`
}

// SQLExpression is a query condition used to filter results.
type SQLExpression struct {
Email string `json:"email,omitempty"`
UserID string `json:"userId,omitempty"`
PhoneNumber string `json:"phoneNumber,omitempty"`
}

// QueryUsersRequest is the request structure for the accounts:query endpoint.
type QueryUsersRequest struct {
ReturnUserInfo bool `json:"returnUserInfo"`
Limit string `json:"limit,omitempty"`
Offset string `json:"offset,omitempty"`
SortBy string `json:"sortBy,omitempty"`
Order string `json:"order,omitempty"`
TenantID string `json:"tenantId,omitempty"`
Expression []*SQLExpression `json:"expression,omitempty"`
}

// SortByField is a field to use for sorting user accounts.
type SortByField string

const (
// UserID sorts results by userId.
UserID SortByField = "USER_ID"
// Name sorts results by name.
Name SortByField = "NAME"
// CreatedAt sorts results by createdAt.
CreatedAt SortByField = "CREATED_AT"
// LastLoginAt sorts results by lastLoginAt.
LastLoginAt SortByField = "LAST_LOGIN_AT"
// UserEmail sorts results by userEmail.
UserEmail SortByField = "USER_EMAIL"
)

// Order is an order for sorting query results.
type Order string

const (
// Asc sorts in ascending order.
Asc Order = "ASC"
// Desc sorts in descending order.
Desc Order = "DESC"
)

func (c *baseClient) getUser(ctx context.Context, query *userQuery) (*UserRecord, error) {
var parsed getAccountInfoResponse
resp, err := c.post(ctx, "/accounts:lookup", query.build(), &parsed)
Expand Down Expand Up @@ -1311,6 +1366,33 @@ type DeleteUsersErrorInfo struct {
// array of errors that correspond to the failed deletions. An error is
// returned if any of the identifiers are invalid or if more than 1000
// identifiers are specified.
// QueryUsers queries for user accounts based on the provided query configuration.
func (c *baseClient) QueryUsers(ctx context.Context, query *QueryUsersRequest) (*QueryUserInfoResponse, error) {
if query == nil {
return nil, fmt.Errorf("query request must not be nil")
}

var parsed queryUsersResponse
_, err := c.post(ctx, "/accounts:query", query, &parsed)
if err != nil {
return nil, err
}

var userRecords []*UserRecord
for _, user := range parsed.Users {
userRecord, err := user.makeUserRecord()
if err != nil {
return nil, fmt.Errorf("error while parsing response: %w", err)
}
userRecords = append(userRecords, userRecord)
}

return &QueryUserInfoResponse{
Users: userRecords,
Count: parsed.Count,
}, nil
}

func (c *baseClient) DeleteUsers(ctx context.Context, uids []string) (*DeleteUsersResult, error) {
if len(uids) == 0 {
return &DeleteUsersResult{}, nil
Expand Down
106 changes: 106 additions & 0 deletions auth/user_mgt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1899,6 +1899,112 @@ func TestDeleteUsers(t *testing.T) {
})
}

func TestQueryUsers(t *testing.T) {
resp := `{
"usersInfo": [{
"localId": "testuser",
"email": "[email protected]",
"phoneNumber": "+1234567890",
"emailVerified": true,
"displayName": "Test User",
"photoUrl": "http://www.example.com/testuser/photo.png",
"validSince": "1494364393",
"disabled": false,
"createdAt": "1234567890000",
"lastLoginAt": "1233211232000",
"customAttributes": "{\"admin\": true, \"package\": \"gold\"}",
"tenantId": "testTenant",
"providerUserInfo": [{
"providerId": "password",
"displayName": "Test User",
"photoUrl": "http://www.example.com/testuser/photo.png",
"email": "[email protected]",
"rawId": "testuid"
}, {
"providerId": "phone",
"phoneNumber": "+1234567890",
"rawId": "testuid"
}],
"mfaInfo": [{
"phoneInfo": "+1234567890",
"mfaEnrollmentId": "enrolledPhoneFactor",
"displayName": "My MFA Phone",
"enrolledAt": "2021-03-03T13:06:20.542896Z"
}, {
"totpInfo": {},
"mfaEnrollmentId": "enrolledTOTPFactor",
"displayName": "My MFA TOTP",
"enrolledAt": "2021-03-03T13:06:20.542896Z"
}]
}],
"recordsCount": "1"
}`
s := echoServer([]byte(resp), t)
defer s.Close()

query := &QueryUsersRequest{
ReturnUserInfo: true,
Limit: "1",
SortBy: string(UserEmail),
Order: string(Asc),
Expression: []*SQLExpression{
{
Email: "[email protected]",
},
},
}

result, err := s.Client.QueryUsers(context.Background(), query)
if err != nil {
t.Fatalf("QueryUsers() = %v", err)
}

if len(result.Users) != 1 {
t.Fatalf("QueryUsers() returned %d users; want 1", len(result.Users))
}

if result.Count != "1" {
t.Errorf("QueryUsers() returned count %q; want '1'", result.Count)
}

if !reflect.DeepEqual(result.Users[0], testUser) {
t.Errorf("QueryUsers() = %#v; want = %#v", result.Users[0], testUser)
}

wantPath := "/projects/mock-project-id/accounts:query"
if s.Req[0].RequestURI != wantPath {
t.Errorf("QueryUsers() URL = %q; want = %q", s.Req[0].RequestURI, wantPath)
}
}

func TestQueryUsersError(t *testing.T) {
resp := `{
"error": {
"message": "INVALID_QUERY"
}
}`
s := echoServer([]byte(resp), t)
defer s.Close()
s.Status = http.StatusBadRequest

query := &QueryUsersRequest{
ReturnUserInfo: true,
Limit: "1",
SortBy: "USER_EMAIL",
Order: "ASC",
Expression: []*SQLExpression{
{
Email: "[email protected]",
},
},
}

result, err := s.Client.QueryUsers(context.Background(), query)
if result != nil || err == nil {
t.Fatalf("QueryUsers() = (%v, %v); want = (nil, error)", result, err)
}
}

func TestMakeExportedUser(t *testing.T) {
queryResponse := &userQueryResponse{
UID: "testuser",
Expand Down
Loading