mirror of
https://github.com/absmach/magistrala.git
synced 2026-06-23 04:10:28 +00:00
MG-2330 - Fix non-admin users search with identity (#2331)
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
This commit is contained in:
@@ -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
|
||||
|
||||
+46
-1
@@ -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"
|
||||
@@ -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"`,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -2050,6 +2050,36 @@ func (_m *SDK) RevokeCert(thingID string, token string) (time.Time, errors.SDKEr
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SearchUsers provides a mock function with given fields: pm, token
|
||||
func (_m *SDK) SearchUsers(pm sdk.PageMetadata, token string) (sdk.UsersPage, errors.SDKError) {
|
||||
ret := _m.Called(pm, token)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SearchUsers")
|
||||
}
|
||||
|
||||
var r0 sdk.UsersPage
|
||||
var r1 errors.SDKError
|
||||
if rf, ok := ret.Get(0).(func(sdk.PageMetadata, string) (sdk.UsersPage, errors.SDKError)); ok {
|
||||
return rf(pm, token)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(sdk.PageMetadata, string) sdk.UsersPage); ok {
|
||||
r0 = rf(pm, token)
|
||||
} else {
|
||||
r0 = ret.Get(0).(sdk.UsersPage)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(sdk.PageMetadata, string) errors.SDKError); ok {
|
||||
r1 = rf(pm, token)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(errors.SDKError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SendInvitation provides a mock function with given fields: invitation, token
|
||||
func (_m *SDK) SendInvitation(invitation sdk.Invitation, token string) error {
|
||||
ret := _m.Called(invitation, token)
|
||||
|
||||
@@ -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,
|
||||
@@ -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)
|
||||
|
||||
@@ -656,6 +656,125 @@ func TestListClients(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchUsers(t *testing.T) {
|
||||
us, svc, _ := newUsersServer()
|
||||
defer us.Close()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
page mgclients.Page
|
||||
status int
|
||||
query string
|
||||
listUsersResponse mgclients.ClientsPage
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "search users with valid token",
|
||||
token: validToken,
|
||||
status: http.StatusOK,
|
||||
query: "name=clientname",
|
||||
listUsersResponse: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: 1,
|
||||
},
|
||||
Clients: []mgclients.Client{client},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "search users with empty token",
|
||||
token: "",
|
||||
query: "name=clientname",
|
||||
status: http.StatusUnauthorized,
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "search users with invalid token",
|
||||
token: inValidToken,
|
||||
query: "name=clientname",
|
||||
status: http.StatusUnauthorized,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "search users with offset",
|
||||
token: validToken,
|
||||
listUsersResponse: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Offset: 1,
|
||||
Total: 1,
|
||||
},
|
||||
Clients: []mgclients.Client{client},
|
||||
},
|
||||
query: "name=clientname&offset=1",
|
||||
status: http.StatusOK,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "search users with invalid offset",
|
||||
token: validToken,
|
||||
query: "name=clientname&offset=invalid",
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrValidation,
|
||||
},
|
||||
{
|
||||
desc: "search users with limit",
|
||||
token: validToken,
|
||||
listUsersResponse: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Limit: 1,
|
||||
Total: 1,
|
||||
},
|
||||
Clients: []mgclients.Client{client},
|
||||
},
|
||||
query: "name=clientname&limit=1",
|
||||
status: http.StatusOK,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "search users with invalid limit",
|
||||
token: validToken,
|
||||
query: "name=clientname&limit=invalid",
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrValidation,
|
||||
},
|
||||
{
|
||||
desc: "search users with empty query",
|
||||
token: validToken,
|
||||
query: "",
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrEmptySearchQuery,
|
||||
},
|
||||
{
|
||||
desc: "search users with invalid length of query",
|
||||
token: validToken,
|
||||
query: "name=a",
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrLenSearchQuery,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: us.Client(),
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/users/search?", us.URL) + tc.query,
|
||||
token: tc.token,
|
||||
}
|
||||
|
||||
svcCall := svc.On("SearchUsers", mock.Anything, tc.token, mock.Anything).Return(
|
||||
mgclients.ClientsPage{
|
||||
Page: tc.listUsersResponse.Page,
|
||||
Clients: tc.listUsersResponse.Clients,
|
||||
},
|
||||
tc.err)
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateClient(t *testing.T) {
|
||||
us, svc, _ := newUsersServer()
|
||||
defer us.Close()
|
||||
|
||||
@@ -104,6 +104,42 @@ func listClientsEndpoint(svc users.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func searchClientsEndpoint(svc users.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(searchClientsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
pm := mgclients.Page{
|
||||
Offset: req.Offset,
|
||||
Limit: req.Limit,
|
||||
Name: req.Name,
|
||||
Id: req.Id,
|
||||
Order: req.Order,
|
||||
Dir: req.Dir,
|
||||
}
|
||||
page, err := svc.SearchUsers(ctx, req.token, pm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := clientsPageRes{
|
||||
pageRes: pageRes{
|
||||
Total: page.Total,
|
||||
Offset: page.Offset,
|
||||
Limit: page.Limit,
|
||||
},
|
||||
Clients: []viewClientRes{},
|
||||
}
|
||||
for _, client := range page.Clients {
|
||||
res.Clients = append(res.Clients, viewClientRes{Client: client})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listMembersByGroupEndpoint(svc users.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listMembersByObjectReq)
|
||||
|
||||
@@ -152,6 +152,27 @@ func (lm *loggingMiddleware) ListClients(ctx context.Context, token string, pm m
|
||||
return lm.svc.ListClients(ctx, token, pm)
|
||||
}
|
||||
|
||||
// SearchUsers logs the search_users request. It logs the page metadata and the time it took to complete the request.
|
||||
func (lm *loggingMiddleware) SearchUsers(ctx context.Context, token string, cp mgclients.Page) (mp mgclients.ClientsPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.Group("page",
|
||||
slog.Uint64("limit", cp.Limit),
|
||||
slog.Uint64("offset", cp.Offset),
|
||||
slog.Uint64("total", mp.Total),
|
||||
),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Search clients failed to complete successfully", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Search clients completed successfully", args...)
|
||||
}(time.Now())
|
||||
return lm.svc.SearchUsers(ctx, token, cp)
|
||||
}
|
||||
|
||||
// UpdateClient logs the update_client request. It logs the client id and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) UpdateClient(ctx context.Context, token string, client mgclients.Client) (c mgclients.Client, err error) {
|
||||
|
||||
@@ -84,6 +84,15 @@ func (ms *metricsMiddleware) ListClients(ctx context.Context, token string, pm m
|
||||
return ms.svc.ListClients(ctx, token, pm)
|
||||
}
|
||||
|
||||
// SearchUsers instruments SearchClients method with metrics.
|
||||
func (ms *metricsMiddleware) SearchUsers(ctx context.Context, token string, pm mgclients.Page) (mp mgclients.ClientsPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "search_users").Add(1)
|
||||
ms.latency.With("method", "search_users").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.SearchUsers(ctx, token, pm)
|
||||
}
|
||||
|
||||
// UpdateClient instruments UpdateClient method with metrics.
|
||||
func (ms *metricsMiddleware) UpdateClient(ctx context.Context, token string, client mgclients.Client) (mgclients.Client, error) {
|
||||
defer func(begin time.Time) {
|
||||
|
||||
@@ -89,6 +89,28 @@ func (req listClientsReq) validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type searchClientsReq struct {
|
||||
token string
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
Name string
|
||||
Id string
|
||||
Order string
|
||||
Dir string
|
||||
}
|
||||
|
||||
func (req searchClientsReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.Name == "" && req.Id == "" {
|
||||
return apiutil.ErrEmptySearchQuery
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listMembersByObjectReq struct {
|
||||
mgclients.Page
|
||||
token string
|
||||
|
||||
@@ -237,6 +237,42 @@ func TestListClientsReqValidate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchClientsReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req searchClientsReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: searchClientsReq{
|
||||
token: valid,
|
||||
Name: name,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
req: searchClientsReq{
|
||||
token: "",
|
||||
Name: name,
|
||||
},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty query",
|
||||
req: searchClientsReq{
|
||||
token: valid,
|
||||
},
|
||||
err: apiutil.ErrEmptySearchQuery,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
err := c.req.validate()
|
||||
assert.Equal(t, c.err, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListMembersByObjectReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
|
||||
@@ -31,6 +31,9 @@ type Service interface {
|
||||
// ListMembers retrieves everything that is assigned to a group/thing identified by objectID.
|
||||
ListMembers(ctx context.Context, token, objectKind, objectID string, pm clients.Page) (clients.MembersPage, error)
|
||||
|
||||
// SearchClients searches for users with provided filters for a valid auth token.
|
||||
SearchUsers(ctx context.Context, token string, pm clients.Page) (clients.ClientsPage, error)
|
||||
|
||||
// UpdateClient updates the client's name and metadata.
|
||||
UpdateClient(ctx context.Context, token string, client clients.Client) (clients.Client, error)
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ const (
|
||||
clientView = clientPrefix + "view"
|
||||
profileView = clientPrefix + "view_profile"
|
||||
clientList = clientPrefix + "list"
|
||||
clientSearch = clientPrefix + "search"
|
||||
clientListByGroup = clientPrefix + "list_by_group"
|
||||
clientIdentify = clientPrefix + "identify"
|
||||
generateResetToken = clientPrefix + "generate_reset_token"
|
||||
@@ -37,6 +38,7 @@ var (
|
||||
_ events.Event = (*viewProfileEvent)(nil)
|
||||
_ events.Event = (*listClientEvent)(nil)
|
||||
_ events.Event = (*listClientByGroupEvent)(nil)
|
||||
_ events.Event = (*searchClientEvent)(nil)
|
||||
_ events.Event = (*identifyClientEvent)(nil)
|
||||
_ events.Event = (*generateResetTokenEvent)(nil)
|
||||
_ events.Event = (*issueTokenEvent)(nil)
|
||||
@@ -307,6 +309,30 @@ func (lcge listClientByGroupEvent) Encode() (map[string]interface{}, error) {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
type searchClientEvent struct {
|
||||
mgclients.Page
|
||||
}
|
||||
|
||||
func (sce searchClientEvent) Encode() (map[string]interface{}, error) {
|
||||
val := map[string]interface{}{
|
||||
"operation": clientSearch,
|
||||
"total": sce.Total,
|
||||
"offset": sce.Offset,
|
||||
"limit": sce.Limit,
|
||||
}
|
||||
if sce.Name != "" {
|
||||
val["name"] = sce.Name
|
||||
}
|
||||
if sce.Identity != "" {
|
||||
val["identity"] = sce.Identity
|
||||
}
|
||||
if sce.Id != "" {
|
||||
val["id"] = sce.Id
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
type identifyClientEvent struct {
|
||||
userID string
|
||||
}
|
||||
|
||||
@@ -160,6 +160,22 @@ func (es *eventStore) ListClients(ctx context.Context, token string, pm mgclient
|
||||
return cp, nil
|
||||
}
|
||||
|
||||
func (es *eventStore) SearchUsers(ctx context.Context, token string, pm mgclients.Page) (mgclients.ClientsPage, error) {
|
||||
cp, err := es.svc.SearchUsers(ctx, token, pm)
|
||||
if err != nil {
|
||||
return cp, err
|
||||
}
|
||||
event := searchClientEvent{
|
||||
pm,
|
||||
}
|
||||
|
||||
if err := es.Publish(ctx, event); err != nil {
|
||||
return cp, err
|
||||
}
|
||||
|
||||
return cp, nil
|
||||
}
|
||||
|
||||
func (es *eventStore) ListMembers(ctx context.Context, token, objectKind, objectID string, pm mgclients.Page) (mgclients.MembersPage, error) {
|
||||
mp, err := es.svc.ListMembers(ctx, token, objectKind, objectID, pm)
|
||||
if err != nil {
|
||||
|
||||
@@ -331,6 +331,34 @@ func (_m *Service) ResetSecret(ctx context.Context, resetToken string, secret st
|
||||
return r0
|
||||
}
|
||||
|
||||
// SearchUsers provides a mock function with given fields: ctx, token, pm
|
||||
func (_m *Service) SearchUsers(ctx context.Context, token string, pm clients.Page) (clients.ClientsPage, error) {
|
||||
ret := _m.Called(ctx, token, pm)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SearchUsers")
|
||||
}
|
||||
|
||||
var r0 clients.ClientsPage
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, clients.Page) (clients.ClientsPage, error)); ok {
|
||||
return rf(ctx, token, pm)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, clients.Page) clients.ClientsPage); ok {
|
||||
r0 = rf(ctx, token, pm)
|
||||
} else {
|
||||
r0 = ret.Get(0).(clients.ClientsPage)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, clients.Page) error); ok {
|
||||
r1 = rf(ctx, token, pm)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SendPasswordReset provides a mock function with given fields: ctx, host, email, user, token
|
||||
func (_m *Service) SendPasswordReset(ctx context.Context, host string, email string, user string, token string) error {
|
||||
ret := _m.Called(ctx, host, email, user, token)
|
||||
|
||||
+25
-11
@@ -180,24 +180,38 @@ func (svc service) ListClients(ctx context.Context, token string, pm mgclients.P
|
||||
if err != nil {
|
||||
return mgclients.ClientsPage{}, err
|
||||
}
|
||||
if err := svc.checkSuperAdmin(ctx, userID); err == nil {
|
||||
pm.Role = mgclients.AllRole
|
||||
pg, err := svc.clients.RetrieveAll(ctx, pm)
|
||||
if err != nil {
|
||||
return mgclients.ClientsPage{}, errors.Wrap(svcerr.ErrViewEntity, err)
|
||||
}
|
||||
return pg, err
|
||||
if err := svc.checkSuperAdmin(ctx, userID); err != nil {
|
||||
return mgclients.ClientsPage{}, err
|
||||
}
|
||||
|
||||
pg, err := svc.clients.SearchClients(ctx, pm)
|
||||
pm.Role = mgclients.AllRole
|
||||
pg, err := svc.clients.RetrieveAll(ctx, pm)
|
||||
if err != nil {
|
||||
return mgclients.ClientsPage{}, errors.Wrap(svcerr.ErrViewEntity, err)
|
||||
}
|
||||
for i, c := range pg.Clients {
|
||||
pg.Clients[i] = mgclients.Client{ID: c.ID, Name: c.Name}
|
||||
return pg, err
|
||||
}
|
||||
|
||||
func (svc service) SearchUsers(ctx context.Context, token string, pm mgclients.Page) (mgclients.ClientsPage, error) {
|
||||
_, err := svc.Identify(ctx, token)
|
||||
if err != nil {
|
||||
return mgclients.ClientsPage{}, err
|
||||
}
|
||||
|
||||
return pg, nil
|
||||
page := mgclients.Page{
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
Name: pm.Name,
|
||||
Id: pm.Id,
|
||||
Role: mgclients.UserRole,
|
||||
}
|
||||
|
||||
cp, err := svc.clients.SearchClients(ctx, page)
|
||||
if err != nil {
|
||||
return mgclients.ClientsPage{}, errors.Wrap(svcerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
return cp, nil
|
||||
}
|
||||
|
||||
func (svc service) UpdateClient(ctx context.Context, token string, cli mgclients.Client) (mgclients.Client, error) {
|
||||
|
||||
+85
-27
@@ -534,7 +534,7 @@ func TestListClients(t *testing.T) {
|
||||
authorizeResponse: &magistrala.AuthorizeRes{Authorized: false},
|
||||
token: validToken,
|
||||
authorizeErr: svcerr.ErrAuthorization,
|
||||
err: nil,
|
||||
err: svcerr.ErrAuthorization,
|
||||
},
|
||||
{
|
||||
desc: "list clients as admin with failed to retrieve clients",
|
||||
@@ -557,29 +557,7 @@ func TestListClients(t *testing.T) {
|
||||
authorizeResponse: &magistrala.AuthorizeRes{Authorized: false},
|
||||
token: validToken,
|
||||
superAdminErr: svcerr.ErrAuthorization,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list clients as normal user successfully",
|
||||
page: mgclients.Page{
|
||||
Total: 1,
|
||||
},
|
||||
identifyResponse: &magistrala.IdentityRes{UserId: client.ID},
|
||||
authorizeResponse: &magistrala.AuthorizeRes{Authorized: false},
|
||||
retrieveAllResponse: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: 1,
|
||||
},
|
||||
Clients: []mgclients.Client{basicClient},
|
||||
},
|
||||
response: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{
|
||||
Total: 1,
|
||||
},
|
||||
Clients: []mgclients.Client{basicClient},
|
||||
},
|
||||
token: validToken,
|
||||
err: nil,
|
||||
err: svcerr.ErrAuthorization,
|
||||
},
|
||||
{
|
||||
desc: "list clients as normal user with failed to retrieve clients",
|
||||
@@ -591,7 +569,7 @@ func TestListClients(t *testing.T) {
|
||||
retrieveAllResponse: mgclients.ClientsPage{},
|
||||
token: validToken,
|
||||
retrieveAllErr: repoerr.ErrNotFound,
|
||||
err: svcerr.ErrViewEntity,
|
||||
err: svcerr.ErrAuthorization,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -600,7 +578,6 @@ func TestListClients(t *testing.T) {
|
||||
authCall1 := auth.On("Authorize", context.Background(), mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr)
|
||||
repoCall := cRepo.On("CheckSuperAdmin", context.Background(), mock.Anything).Return(tc.superAdminErr)
|
||||
repoCall1 := cRepo.On("RetrieveAll", context.Background(), mock.Anything).Return(tc.retrieveAllResponse, tc.retrieveAllErr)
|
||||
repoCall2 := cRepo.On("SearchClients", context.Background(), mock.Anything).Return(tc.response, tc.err)
|
||||
page, err := svc.ListClients(context.Background(), tc.token, tc.page)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.response, page, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, page))
|
||||
@@ -612,7 +589,88 @@ func TestListClients(t *testing.T) {
|
||||
authCall1.Unset()
|
||||
repoCall.Unset()
|
||||
repoCall1.Unset()
|
||||
repoCall2.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchUsers(t *testing.T) {
|
||||
svc, cRepo, auth, _ := newService(true)
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
page mgclients.Page
|
||||
identifyResp *magistrala.IdentityRes
|
||||
authorizeResponse *magistrala.AuthorizeRes
|
||||
response mgclients.ClientsPage
|
||||
responseErr error
|
||||
identifyErr error
|
||||
authorizeErr error
|
||||
checkSuperAdminErr error
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "search clients with valid token",
|
||||
token: validToken,
|
||||
page: mgclients.Page{Offset: 0, Name: "clientname", Limit: 100},
|
||||
response: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{Total: 1, Offset: 0, Limit: 100},
|
||||
Clients: []mgclients.Client{client},
|
||||
},
|
||||
identifyResp: &magistrala.IdentityRes{UserId: client.ID},
|
||||
authorizeResponse: &magistrala.AuthorizeRes{Authorized: true},
|
||||
},
|
||||
{
|
||||
desc: "search clients with invalid token",
|
||||
token: inValidToken,
|
||||
page: mgclients.Page{Offset: 0, Name: "clientname", Limit: 100},
|
||||
response: mgclients.ClientsPage{},
|
||||
responseErr: svcerr.ErrAuthentication,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "search clients with id",
|
||||
token: validToken,
|
||||
page: mgclients.Page{Offset: 0, Id: "d8dd12ef-aa2a-43fe-8ef2-2e4fe514360f", Limit: 100},
|
||||
response: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{Total: 1, Offset: 0, Limit: 100},
|
||||
Clients: []mgclients.Client{client},
|
||||
},
|
||||
identifyResp: &magistrala.IdentityRes{UserId: client.ID},
|
||||
authorizeResponse: &magistrala.AuthorizeRes{Authorized: true},
|
||||
},
|
||||
{
|
||||
desc: "search clients with random name",
|
||||
token: validToken,
|
||||
page: mgclients.Page{Offset: 0, Name: "randomname", Limit: 100},
|
||||
response: mgclients.ClientsPage{
|
||||
Page: mgclients.Page{Total: 0, Offset: 0, Limit: 100},
|
||||
Clients: []mgclients.Client{},
|
||||
},
|
||||
identifyResp: &magistrala.IdentityRes{UserId: client.ID},
|
||||
authorizeResponse: &magistrala.AuthorizeRes{Authorized: true},
|
||||
},
|
||||
{
|
||||
desc: "search clients as a normal user",
|
||||
token: validToken,
|
||||
page: mgclients.Page{Offset: 0, Identity: "clientidentity", Limit: 100},
|
||||
response: mgclients.ClientsPage{},
|
||||
authorizeResponse: &magistrala.AuthorizeRes{Authorized: false},
|
||||
checkSuperAdminErr: svcerr.ErrAuthorization,
|
||||
responseErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
authCall := auth.On("Identify", context.Background(), &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResp, tc.identifyErr)
|
||||
authCall1 := auth.On("Authorize", context.Background(), mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr)
|
||||
repoCall := cRepo.On("CheckSuperAdmin", context.Background(), mock.Anything).Return(tc.checkSuperAdminErr)
|
||||
repoCall1 := cRepo.On("SearchClients", context.Background(), mock.Anything).Return(tc.response, tc.responseErr)
|
||||
page, err := svc.SearchUsers(context.Background(), tc.token, tc.page)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.response, page, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, page))
|
||||
authCall.Unset()
|
||||
authCall1.Unset()
|
||||
repoCall.Unset()
|
||||
repoCall1.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,14 @@ func (tm *tracingMiddleware) ListClients(ctx context.Context, token string, pm m
|
||||
return tm.svc.ListClients(ctx, token, pm)
|
||||
}
|
||||
|
||||
// SearchUsers traces the "SearchUsers" operation of the wrapped clients.Service.
|
||||
func (tm *tracingMiddleware) SearchUsers(ctx context.Context, token string, pm mgclients.Page) (mgclients.ClientsPage, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_search_clients", trace.WithAttributes(attribute.String("token", token)))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.SearchUsers(ctx, token, pm)
|
||||
}
|
||||
|
||||
// UpdateClient traces the "UpdateClient" operation of the wrapped clients.Service.
|
||||
func (tm *tracingMiddleware) UpdateClient(ctx context.Context, token string, cli mgclients.Client) (mgclients.Client, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_update_client_name_and_metadata", trace.WithAttributes(
|
||||
|
||||
Reference in New Issue
Block a user