Skip to content

Commit

Permalink
MG-2330 - Fix non-admin users search with identity (absmach#2331)
Browse files Browse the repository at this point in the history
Signed-off-by: nyagamunene <[email protected]>
  • Loading branch information
nyagamunene authored Jul 22, 2024
1 parent c4c037f commit c398908
Show file tree
Hide file tree
Showing 22 changed files with 752 additions and 40 deletions.
26 changes: 26 additions & 0 deletions api/openapi/users.yml
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,32 @@ paths:
"500":
$ref: "#/components/responses/ServiceError"

/users/search:
get:
operationId: searchUsers
summary: Search users
description: |
Search users by name and identity.
tags:
- Users
parameters:
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
- $ref: "#/components/parameters/UserName"
- $ref: "#/components/parameters/UserIdentity"
- $ref: "#/components/parameters/UserID"
security:
- bearerAuth: []
responses:
"200":
$ref: "#/components/responses/UserPageRes"
"400":
description: Failed due to malformed query parameters.
"401":
description: Missing or invalid access token provided.
"500":
$ref: "#/components/responses/ServiceError"

/password/reset-request:
post:
operationId: requestPasswordReset
Expand Down
47 changes: 46 additions & 1 deletion cli/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ package cli

import (
"encoding/json"
"fmt"
"net/url"
"strconv"

mgclients "github.com/absmach/magistrala/pkg/clients"
mgxsdk "github.com/absmach/magistrala/pkg/sdk/go"
Expand Down Expand Up @@ -463,12 +466,54 @@ var cmdUsers = []cobra.Command{
logJSONCmd(*cmd, users)
},
},

{
Use: "search <query> <user_auth_token>",
Short: "Search users",
Long: "Search users by query\n" +
"Usage:\n" +
"\tmagistrala-cli users search <query> <user_auth_token>\n",
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsageCmd(*cmd, cmd.Use)
return
}

values, err := url.ParseQuery(args[0])
if err != nil {
logErrorCmd(*cmd, fmt.Errorf("failed to parse query: %s", err))
}

pm := mgxsdk.PageMetadata{
Offset: Offset,
Limit: Limit,
Name: values.Get("name"),
ID: values.Get("id"),
}

if off, err := strconv.Atoi(values.Get("offset")); err == nil {
pm.Offset = uint64(off)
}

if lim, err := strconv.Atoi(values.Get("limit")); err == nil {
pm.Limit = uint64(lim)
}

users, err := sdk.SearchUsers(pm, args[1])
if err != nil {
logErrorCmd(*cmd, err)
return
}

logJSONCmd(*cmd, users)
},
},
}

// NewUsersCmd returns users command.
func NewUsersCmd() *cobra.Command {
cmd := cobra.Command{
Use: "users [create | get | update | token | password | enable | disable | delete | channels | things | groups]",
Use: "users [create | get | update | token | password | enable | disable | delete | channels | things | groups | search]",
Short: "Users management",
Long: `Users management: create accounts and tokens"`,
}
Expand Down
4 changes: 3 additions & 1 deletion internal/api/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) {
errors.Contains(err, apiutil.ErrInvalidEntityType),
errors.Contains(err, apiutil.ErrMissingEntityType),
errors.Contains(err, apiutil.ErrInvalidTimeFormat),
errors.Contains(err, svcerr.ErrSearch):
errors.Contains(err, svcerr.ErrSearch),
errors.Contains(err, apiutil.ErrEmptySearchQuery),
errors.Contains(err, apiutil.ErrLenSearchQuery):
err = unwrap(err)
w.WriteHeader(http.StatusBadRequest)

Expand Down
6 changes: 6 additions & 0 deletions pkg/apiutil/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,10 @@ var (

// ErrInvalidTimeFormat indicates invalid time format i.e not unix time.
ErrInvalidTimeFormat = errors.New("invalid time format use unix time")

// ErrEmptySearchQuery indicates search query should not be empty.
ErrEmptySearchQuery = errors.New("search query must not be empty")

// ErrLenSearchQuery indicates search query length.
ErrLenSearchQuery = errors.New("search query must be at least 3 characters")
)
12 changes: 12 additions & 0 deletions pkg/sdk/go/sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,18 @@ type SDK interface {
// fmt.Println(things)
ListUserThings(userID string, pm PageMetadata, token string) (ThingsPage, errors.SDKError)

// SeachUsers filters users and returns a page result.
//
// example:
// pm := sdk.PageMetadata{
// Offset: 0,
// Limit: 10,
// Name: "John Doe",
// }
// users, _ := sdk.SearchUsers(pm, "token")
// fmt.Println(users)
SearchUsers(pm PageMetadata, token string) (UsersPage, errors.SDKError)

// CreateThing registers new thing and returns its id.
//
// example:
Expand Down
19 changes: 19 additions & 0 deletions pkg/sdk/go/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,25 @@ func (sdk mgSDK) ListUserThings(userID string, pm PageMetadata, token string) (T
return tp, nil
}

func (sdk mgSDK) SearchUsers(pm PageMetadata, token string) (UsersPage, errors.SDKError) {
url, err := sdk.withQueryParams(sdk.usersURL, fmt.Sprintf("%s/search", usersEndpoint), pm)
if err != nil {
return UsersPage{}, errors.NewSDKError(err)
}

_, body, sdkerr := sdk.processRequest(http.MethodGet, url, token, nil, nil, http.StatusOK)
if sdkerr != nil {
return UsersPage{}, sdkerr
}

var cp UsersPage
if err := json.Unmarshal(body, &cp); err != nil {
return UsersPage{}, errors.NewSDKError(err)
}

return cp, nil
}

func (sdk mgSDK) EnableUser(id, token string) (User, errors.SDKError) {
return sdk.changeClientStatus(token, id, enableEndpoint)
}
Expand Down
121 changes: 121 additions & 0 deletions pkg/sdk/go/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,127 @@ func TestListUsers(t *testing.T) {
}
}

func TestSearchClients(t *testing.T) {
ts, svc := setupUsers()
defer ts.Close()

var cls []sdk.User
conf := sdk.Config{
UsersURL: ts.URL,
}
mgsdk := sdk.NewSDK(conf)

for i := 10; i < 100; i++ {
cl := sdk.User{
ID: generateUUID(t),
Name: fmt.Sprintf("client_%d", i),
Credentials: sdk.Credentials{
Identity: fmt.Sprintf("identity_%d", i),
Secret: fmt.Sprintf("password_%d", i),
},
Metadata: sdk.Metadata{"name": fmt.Sprintf("client_%d", i)},
Status: mgclients.EnabledStatus.String(),
}
if i == 50 {
cl.Status = mgclients.DisabledStatus.String()
cl.Tags = []string{"tag1", "tag2"}
}
cls = append(cls, cl)
}

cases := []struct {
desc string
token string
page sdk.PageMetadata
response []sdk.User
searchreturn mgclients.ClientsPage
err errors.SDKError
identifyErr error
}{
{
desc: "search for users",
token: validToken,
err: nil,
page: sdk.PageMetadata{
Offset: offset,
Limit: limit,
Name: "client_10",
},
response: []sdk.User{cls[10]},
searchreturn: mgclients.ClientsPage{
Clients: []mgclients.Client{convertClient(cls[10])},
Page: mgclients.Page{
Total: 1,
Offset: offset,
Limit: limit,
},
},
},
{
desc: "search for users with invalid token",
token: invalidToken,
page: sdk.PageMetadata{
Offset: offset,
Limit: limit,
Name: "client_10",
},
err: errors.NewSDKErrorWithStatus(svcerr.ErrAuthentication, http.StatusUnauthorized),
response: nil,
identifyErr: svcerr.ErrAuthentication,
},
{
desc: "search for users with empty token",
token: "",
page: sdk.PageMetadata{
Offset: offset,
Limit: limit,
Name: "client_10",
},
err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrValidation, apiutil.ErrBearerToken), http.StatusUnauthorized),
response: nil,
identifyErr: svcerr.ErrAuthentication,
},
{
desc: "search for users with empty query",
token: validToken,
page: sdk.PageMetadata{
Offset: offset,
Limit: limit,
Name: "",
},
err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrValidation, apiutil.ErrEmptySearchQuery), http.StatusBadRequest),
},
{
desc: "search for users with invalid length of query",
token: validToken,
page: sdk.PageMetadata{
Offset: offset,
Limit: limit,
Name: "a",
},
err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrLenSearchQuery, apiutil.ErrValidation), http.StatusBadRequest),
},
{
desc: "search for users with invalid limit",
token: validToken,
page: sdk.PageMetadata{
Offset: offset,
Limit: 0,
Name: "client_10",
},
err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrValidation, apiutil.ErrLimitSize), http.StatusBadRequest),
},
}

for _, tc := range cases {
repoCall := svc.On("SearchUsers", mock.Anything, mock.Anything, mock.Anything).Return(tc.searchreturn, tc.err)
page, err := mgsdk.SearchUsers(tc.page, tc.token)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected error %s, got %s", tc.desc, tc.err, err))
assert.Equal(t, tc.response, page.Users, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, page))
repoCall.Unset()
}
}

func TestViewUser(t *testing.T) {
ts, svc := setupUsers()
defer ts.Close()
Expand Down
30 changes: 30 additions & 0 deletions pkg/sdk/mocks/sdk.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 55 additions & 0 deletions users/api/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ func clientsHandler(svc users.Service, r *chi.Mux, logger *slog.Logger, pr *rege
opts...,
), "list_clients").ServeHTTP)

r.Get("/search", otelhttp.NewHandler(kithttp.NewServer(
searchClientsEndpoint(svc),
decodeSearchClients,
api.EncodeResponse,
opts...,
), "search_clients").ServeHTTP)

r.Patch("/secret", otelhttp.NewHandler(kithttp.NewServer(
updateClientSecretEndpoint(svc),
decodeUpdateClientSecret,
Expand Down Expand Up @@ -272,6 +279,54 @@ func decodeListClients(_ context.Context, r *http.Request) (interface{}, error)
return req, nil
}

func decodeSearchClients(_ context.Context, r *http.Request) (interface{}, error) {
o, err := apiutil.ReadNumQuery[uint64](r, api.OffsetKey, api.DefOffset)
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
l, err := apiutil.ReadNumQuery[uint64](r, api.LimitKey, api.DefLimit)
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
n, err := apiutil.ReadStringQuery(r, api.NameKey, "")
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
id, err := apiutil.ReadStringQuery(r, api.IDOrder, "")
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
order, err := apiutil.ReadStringQuery(r, api.OrderKey, api.DefOrder)
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
dir, err := apiutil.ReadStringQuery(r, api.DirKey, api.DefDir)
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}

req := searchClientsReq{
token: apiutil.ExtractBearerToken(r),
Offset: o,
Limit: l,
Name: n,
Id: id,
Order: order,
Dir: dir,
}

for _, field := range []string{req.Name, req.Id} {
if field != "" && len(field) < 3 {
req = searchClientsReq{
token: apiutil.ExtractBearerToken(r),
}
return req, errors.Wrap(apiutil.ErrLenSearchQuery, apiutil.ErrValidation)
}
}

return req, nil
}

func decodeUpdateClient(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
Expand Down
Loading

0 comments on commit c398908

Please sign in to comment.