SMQ-2718 - Add actions on view entities response in groups (#2750)

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
This commit is contained in:
Steve Munene
2025-03-20 14:04:39 +03:00
committed by GitHub
parent b0493e03af
commit f6b3938f96
26 changed files with 644 additions and 229 deletions
+1 -1
View File
@@ -221,7 +221,7 @@ func TestGetClientssCmd(t *testing.T) {
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("Clients", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.page, tc.sdkErr)
sdkCall1 := sdkMock.On("Client", mock.Anything, mock.Anything, mock.Anything).Return(tc.client, tc.sdkErr)
sdkCall1 := sdkMock.On("Client", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.client, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{getCmd}, tc.args...)...)
+9
View File
@@ -219,6 +219,15 @@ func main() {
"",
"Subscription contact query parameter",
)
rootCmd.PersistentFlags().BoolVarP(
&sdkConf.Roles,
"roles",
"R",
false,
"Adds option to display roles for entities",
)
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
+9 -2
View File
@@ -69,9 +69,16 @@ func DecodeGroupUpdate(_ context.Context, r *http.Request) (interface{}, error)
}
func DecodeGroupRequest(_ context.Context, r *http.Request) (interface{}, error) {
req := groupReq{
id: chi.URLParam(r, "groupID"),
roles, err := apiutil.ReadBoolQuery(r, api.RolesKey, false)
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
req := groupReq{
id: chi.URLParam(r, "groupID"),
roles: roles,
}
return req, nil
}
+7 -2
View File
@@ -236,6 +236,7 @@ func TestViewGroupEndpoint(t *testing.T) {
token string
id string
domainID string
roles bool
session smqauthn.Session
svcResp groups.Group
svcErr error
@@ -248,6 +249,7 @@ func TestViewGroupEndpoint(t *testing.T) {
desc: "view group successfully",
token: validToken,
domainID: validID,
roles: false,
id: validID,
svcResp: validGroupResp,
svcErr: nil,
@@ -260,6 +262,7 @@ func TestViewGroupEndpoint(t *testing.T) {
token: invalidToken,
session: smqauthn.Session{},
domainID: validID,
roles: false,
id: validID,
svcResp: validGroupResp,
svcErr: nil,
@@ -272,6 +275,7 @@ func TestViewGroupEndpoint(t *testing.T) {
token: "",
session: smqauthn.Session{},
domainID: validID,
roles: false,
id: validID,
status: http.StatusUnauthorized,
err: apiutil.ErrBearerToken,
@@ -288,6 +292,7 @@ func TestViewGroupEndpoint(t *testing.T) {
token: validToken,
id: validID,
domainID: validID,
roles: false,
svcResp: validGroupResp,
svcErr: svcerr.ErrAuthorization,
status: http.StatusForbidden,
@@ -300,14 +305,14 @@ func TestViewGroupEndpoint(t *testing.T) {
req := testRequest{
client: gs.Client(),
method: http.MethodGet,
url: fmt.Sprintf("%s/%s/groups/%s", gs.URL, tc.domainID, tc.id),
url: fmt.Sprintf("%s/%s/groups/%s?roles=%v", gs.URL, tc.domainID, tc.id, tc.roles),
token: tc.token,
}
if tc.token == validToken {
tc.session = smqauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}
}
authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authnErr)
svcCall := svc.On("ViewGroup", mock.Anything, tc.session, tc.id).Return(tc.svcResp, tc.svcErr)
svcCall := svc.On("ViewGroup", mock.Anything, tc.session, tc.id, tc.roles).Return(tc.svcResp, tc.svcErr)
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
var errRes respBody
+1 -1
View File
@@ -48,7 +48,7 @@ func ViewGroupEndpoint(svc groups.Service) endpoint.Endpoint {
return viewGroupRes{}, svcerr.ErrAuthentication
}
group, err := svc.ViewGroup(ctx, session, req.id)
group, err := svc.ViewGroup(ctx, session, req.id, req.roles)
if err != nil {
return viewGroupRes{}, err
}
+2 -1
View File
@@ -56,7 +56,8 @@ func (req listGroupsReq) validate() error {
}
type groupReq struct {
id string
id string
roles bool
}
func (req groupReq) validate() error {
+2 -2
View File
@@ -80,8 +80,8 @@ func (es eventStore) UpdateGroup(ctx context.Context, session authn.Session, gro
return group, nil
}
func (es eventStore) ViewGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) {
group, err := es.svc.ViewGroup(ctx, session, id)
func (es eventStore) ViewGroup(ctx context.Context, session authn.Session, id string, withRoles bool) (groups.Group, error) {
group, err := es.svc.ViewGroup(ctx, session, id, withRoles)
if err != nil {
return group, err
}
+26 -22
View File
@@ -25,27 +25,29 @@ type Metadata map[string]interface{}
// Path in a tree consisting of group IDs
// Paths are unique per domain.
type Group struct {
ID string `json:"id"`
Domain string `json:"domain_id,omitempty"`
Parent string `json:"parent_id,omitempty"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Metadata Metadata `json:"metadata,omitempty"`
Level int `json:"level,omitempty"`
Path string `json:"path,omitempty"`
Children []*Group `json:"children,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
UpdatedBy string `json:"updated_by,omitempty"`
Status Status `json:"status"`
RoleID string `json:"role_id,omitempty"`
RoleName string `json:"role_name,omitempty"`
Actions []string `json:"actions,omitempty"`
AccessType string `json:"access_type,omitempty"`
AccessProviderId string `json:"access_provider_id,omitempty"`
AccessProviderRoleId string `json:"access_provider_role_id,omitempty"`
AccessProviderRoleName string `json:"access_provider_role_name,omitempty"`
AccessProviderRoleActions []string `json:"access_provider_role_actions,omitempty"`
ID string `json:"id"`
Domain string `json:"domain_id,omitempty"`
Parent string `json:"parent_id,omitempty"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Metadata Metadata `json:"metadata,omitempty"`
Level int `json:"level,omitempty"`
Path string `json:"path,omitempty"`
Children []*Group `json:"children,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
UpdatedBy string `json:"updated_by,omitempty"`
Status Status `json:"status"`
RoleID string `json:"role_id,omitempty"`
RoleName string `json:"role_name,omitempty"`
Actions []string `json:"actions,omitempty"`
AccessType string `json:"access_type,omitempty"`
AccessProviderId string `json:"access_provider_id,omitempty"`
AccessProviderRoleId string `json:"access_provider_role_id,omitempty"`
AccessProviderRoleName string `json:"access_provider_role_name,omitempty"`
AccessProviderRoleActions []string `json:"access_provider_role_actions,omitempty"`
MemberId string `json:"member_id,omitempty"`
Roles []roles.MemberRoleActions `json:"roles,omitempty"`
}
type Member struct {
@@ -96,6 +98,8 @@ type Repository interface {
RetrieveByIDAndUser(ctx context.Context, domainID, userID, groupID string) (Group, error)
RetrieveByIDWithRoles(ctx context.Context, groupID, memberID string) (Group, error)
// RetrieveAll retrieves all groups.
RetrieveAll(ctx context.Context, pm PageMeta) (Page, error)
@@ -140,7 +144,7 @@ type Service interface {
UpdateGroup(ctx context.Context, session authn.Session, g Group) (Group, error)
// ViewGroup retrieves data about the group identified by ID.
ViewGroup(ctx context.Context, session authn.Session, id string) (Group, error)
ViewGroup(ctx context.Context, session authn.Session, id string, withRoles bool) (Group, error)
// ListGroups retrieves groups for given filters.
ListGroups(ctx context.Context, session authn.Session, pm PageMeta) (Page, error)
+2 -2
View File
@@ -150,7 +150,7 @@ func (am *authorizationMiddleware) UpdateGroup(ctx context.Context, session auth
return am.svc.UpdateGroup(ctx, session, g)
}
func (am *authorizationMiddleware) ViewGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) {
func (am *authorizationMiddleware) ViewGroup(ctx context.Context, session authn.Session, id string, withRoles bool) (groups.Group, error) {
if session.Type == authn.PersonalAccessToken {
if err := am.authz.AuthorizePAT(ctx, smqauthz.PatReq{
UserID: session.UserID,
@@ -175,7 +175,7 @@ func (am *authorizationMiddleware) ViewGroup(ctx context.Context, session authn.
return groups.Group{}, errors.Wrap(errView, err)
}
return am.svc.ViewGroup(ctx, session, id)
return am.svc.ViewGroup(ctx, session, id, withRoles)
}
func (am *authorizationMiddleware) ListGroups(ctx context.Context, session authn.Session, gm groups.PageMeta) (groups.Page, error) {
+3 -2
View File
@@ -77,7 +77,7 @@ func (lm *loggingMiddleware) UpdateGroup(ctx context.Context, session authn.Sess
// ViewGroup logs the view_group request. It logs the group name, id and the time it took to complete the request.
// If the request fails, it logs the error.
func (lm *loggingMiddleware) ViewGroup(ctx context.Context, session authn.Session, id string) (g groups.Group, err error) {
func (lm *loggingMiddleware) ViewGroup(ctx context.Context, session authn.Session, id string, withRoles bool) (g groups.Group, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
@@ -86,6 +86,7 @@ func (lm *loggingMiddleware) ViewGroup(ctx context.Context, session authn.Sessio
slog.Group("group",
slog.String("id", g.ID),
slog.String("name", g.Name),
slog.Bool("with_roles", withRoles),
),
}
if err != nil {
@@ -95,7 +96,7 @@ func (lm *loggingMiddleware) ViewGroup(ctx context.Context, session authn.Sessio
}
lm.logger.Info("View group completed successfully", args...)
}(time.Now())
return lm.svc.ViewGroup(ctx, session, id)
return lm.svc.ViewGroup(ctx, session, id, withRoles)
}
// ListGroups logs the list_groups request. It logs the page metadata and the time it took to complete the request.
+2 -2
View File
@@ -53,12 +53,12 @@ func (ms *metricsMiddleware) UpdateGroup(ctx context.Context, session authn.Sess
}
// ViewGroup instruments ViewGroup method with metrics.
func (ms *metricsMiddleware) ViewGroup(ctx context.Context, session authn.Session, id string) (g groups.Group, err error) {
func (ms *metricsMiddleware) ViewGroup(ctx context.Context, session authn.Session, id string, withRoles bool) (g groups.Group, err error) {
defer func(begin time.Time) {
ms.counter.With("method", "view_group").Add(1)
ms.latency.With("method", "view_group").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ViewGroup(ctx, session, id)
return ms.svc.ViewGroup(ctx, session, id, withRoles)
}
// ListGroups instruments ListGroups method with metrics.
+28
View File
@@ -334,6 +334,34 @@ func (_m *Repository) RetrieveByIDAndUser(ctx context.Context, domainID string,
return r0, r1
}
// RetrieveByIDWithRoles provides a mock function with given fields: ctx, groupID, memberID
func (_m *Repository) RetrieveByIDWithRoles(ctx context.Context, groupID string, memberID string) (groups.Group, error) {
ret := _m.Called(ctx, groupID, memberID)
if len(ret) == 0 {
panic("no return value specified for RetrieveByIDWithRoles")
}
var r0 groups.Group
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) (groups.Group, error)); ok {
return rf(ctx, groupID, memberID)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string) groups.Group); ok {
r0 = rf(ctx, groupID, memberID)
} else {
r0 = ret.Get(0).(groups.Group)
}
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = rf(ctx, groupID, memberID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RetrieveByIDs provides a mock function with given fields: ctx, pm, ids
func (_m *Repository) RetrieveByIDs(ctx context.Context, pm groups.PageMeta, ids ...string) (groups.Page, error) {
ret := _m.Called(ctx, pm, ids)
+9 -9
View File
@@ -832,9 +832,9 @@ func (_m *Service) UpdateRoleName(ctx context.Context, session authn.Session, en
return r0, r1
}
// ViewGroup provides a mock function with given fields: ctx, session, id
func (_m *Service) ViewGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) {
ret := _m.Called(ctx, session, id)
// ViewGroup provides a mock function with given fields: ctx, session, id, withRoles
func (_m *Service) ViewGroup(ctx context.Context, session authn.Session, id string, withRoles bool) (groups.Group, error) {
ret := _m.Called(ctx, session, id, withRoles)
if len(ret) == 0 {
panic("no return value specified for ViewGroup")
@@ -842,17 +842,17 @@ func (_m *Service) ViewGroup(ctx context.Context, session authn.Session, id stri
var r0 groups.Group
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) (groups.Group, error)); ok {
return rf(ctx, session, id)
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, bool) (groups.Group, error)); ok {
return rf(ctx, session, id, withRoles)
}
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) groups.Group); ok {
r0 = rf(ctx, session, id)
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, bool) groups.Group); ok {
r0 = rf(ctx, session, id, withRoles)
} else {
r0 = ret.Get(0).(groups.Group)
}
if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok {
r1 = rf(ctx, session, id)
if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, bool) error); ok {
r1 = rf(ctx, session, id, withRoles)
} else {
r1 = ret.Error(1)
}
+189 -20
View File
@@ -16,6 +16,7 @@ import (
repoerr "github.com/absmach/supermq/pkg/errors/repository"
"github.com/absmach/supermq/pkg/policies"
"github.com/absmach/supermq/pkg/postgres"
"github.com/absmach/supermq/pkg/roles"
rolesPostgres "github.com/absmach/supermq/pkg/roles/repo/postgres"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
@@ -165,6 +166,164 @@ func (repo groupRepository) RetrieveByID(ctx context.Context, id string) (groups
return toGroup(dbg)
}
func (repo groupRepository) RetrieveByIDWithRoles(ctx context.Context, id, memberID string) (groups.Group, error) {
query := `
WITH selected_group AS (
SELECT
g.id,
g.parent_id,
g.domain_id,
g.path AS parent_group_path
FROM
groups g
WHERE
g.id = :id
LIMIT 1
),
selected_group_roles AS (
SELECT
sg.id AS group_id,
grm.member_id AS member_id,
gr.id AS role_id,
gr.name AS role_name,
jsonb_agg(DISTINCT gra.action) AS actions,
g.path AS access_provider_path,
gr.entity_id AS access_provider_id,
CASE
WHEN gr.entity_id = sg.id THEN 'direct'
WHEN gr.entity_id = sg.parent_id THEN 'direct_group'
ELSE 'indirect_group'
END AS access_type
FROM
groups g
JOIN
groups_roles gr ON gr.entity_id = g.id
JOIN
groups_role_members grm ON gr.id = grm.role_id
JOIN
groups_role_actions gra ON gr.id = gra.role_id
JOIN
selected_group sg ON g.path @> sg.parent_group_path
WHERE
grm.member_id = :member_id
AND (
(gr.entity_id = sg.id)
OR (gr.entity_id <> sg.id AND gra.action LIKE 'subgroup%%')
)
GROUP BY
sg.id, gr.entity_id, gr.id, gr.name, g.path, grm.member_id, sg.parent_id
),
selected_domain_roles AS (
SELECT
sg.id AS group_id,
drm.member_id AS member_id,
dr.id AS role_id,
dr.name AS role_name,
jsonb_agg(DISTINCT all_actions.action) AS actions,
''::::ltree access_provider_path,
'domain' AS access_type,
dr.entity_id AS access_provider_id
FROM
domains d
JOIN
selected_group sg ON sg.domain_id = d.id
JOIN
domains_roles dr ON dr.entity_id = d.id
JOIN
domains_role_members drm ON dr.id = drm.role_id
JOIN
domains_role_actions dra ON dr.id = dra.role_id
JOIN
domains_role_actions all_actions ON dr.id = all_actions.role_id
WHERE
drm.member_id = :member_id
AND dra.action LIKE 'group%%'
GROUP BY
sg.id, dr.entity_id, dr.id, dr.name, drm.member_id
),
all_roles AS (
SELECT
sgr.group_id,
sgr.member_id,
sgr.role_id AS role_id,
sgr.role_name AS role_name,
sgr.actions AS actions,
sgr.access_type AS access_type,
sgr.access_provider_path AS access_provider_path,
sgr.access_provider_id AS access_provider_id
FROM
selected_group_roles sgr
UNION
SELECT
sdr.group_id,
sdr.member_id,
sdr.role_id AS role_id,
sdr.role_name AS role_name,
sdr.actions AS actions,
sdr.access_type AS access_type,
sdr.access_provider_path AS access_provider_path,
sdr.access_provider_id AS access_provider_id
FROM
selected_domain_roles sdr
),
final_roles AS (
SELECT
ar.group_id,
ar.member_id,
jsonb_agg(
jsonb_build_object(
'role_id', ar.role_id,
'role_name', ar.role_name,
'actions', ar.actions,
'access_type', ar.access_type,
'access_provider_path', ar.access_provider_path,
'access_provider_id', ar.access_provider_id
)
) AS roles
FROM all_roles ar
GROUP BY
ar.group_id, ar.member_id
)
SELECT
g.id,
g.parent_id,
g.domain_id,
g.name,
g.description,
g.path,
g.metadata,
g.created_at,
g.updated_at,
g.updated_by,
g.status,
fr.member_id,
fr.roles
FROM groups g
JOIN final_roles fr ON fr.group_id = g.id
`
parameters := map[string]interface{}{
"id": id,
"member_id": memberID,
}
row, err := repo.db.NamedQueryContext(ctx, query, parameters)
if err != nil {
return groups.Group{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
defer row.Close()
dbg := dbGroup{}
if !row.Next() {
return groups.Group{}, errors.Wrap(repoerr.ErrNotFound, err)
}
if err := row.StructScan(&dbg); err != nil {
return groups.Group{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
return toGroup(dbg)
}
func (repo groupRepository) RetrieveByIDAndUser(ctx context.Context, domainID, userID, groupID string) (groups.Group, error) {
baseQuery := repo.userGroupsBaseQuery(domainID, userID)
@@ -934,26 +1093,28 @@ func buildQuery(gm groups.PageMeta, ids ...string) string {
}
type dbGroup struct {
ID string `db:"id"`
ParentID *string `db:"parent_id,omitempty"`
DomainID string `db:"domain_id,omitempty"`
Name string `db:"name"`
Description string `db:"description,omitempty"`
Level int `db:"level"`
Path string `db:"path,omitempty"`
Metadata []byte `db:"metadata,omitempty"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt sql.NullTime `db:"updated_at,omitempty"`
UpdatedBy *string `db:"updated_by,omitempty"`
Status groups.Status `db:"status"`
RoleID string `db:"role_id"`
RoleName string `db:"role_name"`
Actions pq.StringArray `db:"actions"`
AccessType string `db:"access_type"`
AccessProviderId string `db:"access_provider_id"`
AccessProviderRoleId string `db:"access_provider_role_id"`
AccessProviderRoleName string `db:"access_provider_role_name"`
AccessProviderRoleActions pq.StringArray `db:"access_provider_role_actions"`
ID string `db:"id"`
ParentID *string `db:"parent_id,omitempty"`
DomainID string `db:"domain_id,omitempty"`
Name string `db:"name"`
Description string `db:"description,omitempty"`
Level int `db:"level"`
Path string `db:"path,omitempty"`
Metadata []byte `db:"metadata,omitempty"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt sql.NullTime `db:"updated_at,omitempty"`
UpdatedBy *string `db:"updated_by,omitempty"`
Status groups.Status `db:"status"`
RoleID string `db:"role_id"`
RoleName string `db:"role_name"`
Actions pq.StringArray `db:"actions"`
AccessType string `db:"access_type"`
AccessProviderId string `db:"access_provider_id"`
AccessProviderRoleId string `db:"access_provider_role_id"`
AccessProviderRoleName string `db:"access_provider_role_name"`
AccessProviderRoleActions pq.StringArray `db:"access_provider_role_actions"`
MemberID string `db:"member_id,omitempty"`
Roles json.RawMessage `db:"roles,omitempty"`
}
func toDBGroup(g groups.Group) (dbGroup, error) {
@@ -1012,6 +1173,13 @@ func toGroup(g dbGroup) (groups.Group, error) {
updatedBy = *g.UpdatedBy
}
var roles []roles.MemberRoleActions
if g.Roles != nil {
if err := json.Unmarshal(g.Roles, &roles); err != nil {
return groups.Group{}, errors.Wrap(errors.ErrMalformedEntity, err)
}
}
return groups.Group{
ID: g.ID,
Name: g.Name,
@@ -1033,6 +1201,7 @@ func toGroup(g dbGroup) (groups.Group, error) {
AccessProviderRoleId: g.AccessProviderRoleId,
AccessProviderRoleName: g.AccessProviderRoleName,
AccessProviderRoleActions: g.AccessProviderRoleActions,
Roles: roles,
}, nil
}
+9 -2
View File
@@ -105,8 +105,15 @@ func (svc service) CreateGroup(ctx context.Context, session smqauthn.Session, g
return saved, nrps, nil
}
func (svc service) ViewGroup(ctx context.Context, session smqauthn.Session, id string) (Group, error) {
group, err := svc.repo.RetrieveByIDAndUser(ctx, session.DomainID, session.UserID, id)
func (svc service) ViewGroup(ctx context.Context, session smqauthn.Session, id string, withRoles bool) (Group, error) {
var group Group
var err error
switch withRoles {
case true:
group, err = svc.repo.RetrieveByIDWithRoles(ctx, id, session.UserID)
default:
group, err = svc.repo.RetrieveByID(ctx, id)
}
if err != nil {
return Group{}, errors.Wrap(svcerr.ErrViewEntity, err)
}
+55 -20
View File
@@ -43,6 +43,22 @@ var (
},
Status: groups.EnabledStatus,
}
validGroupWithRoles = groups.Group{
ID: testsutil.GenerateUUID(&testing.T{}),
Name: namegen.Generate(),
Description: namegen.Generate(),
Metadata: map[string]interface{}{
"key": "value",
},
Status: groups.EnabledStatus,
Roles: []roles.MemberRoleActions{
{
RoleID: "test-id",
RoleName: "test-name",
AccessType: "direct",
},
},
}
parentGroupID = testsutil.GenerateUUID(&testing.T{})
childGroupID = testsutil.GenerateUUID(&testing.T{})
childGroup = groups.Group{
@@ -226,39 +242,58 @@ func TestViewGroup(t *testing.T) {
svc := newService(t)
cases := []struct {
desc string
session smqauthn.Session
id string
repoResp groups.Group
repoErr error
err error
desc string
session smqauthn.Session
id string
withRoles bool
repoResp groups.Group
repoErr error
err error
}{
{
desc: "view group successfully",
id: validGroup.ID,
session: validSession,
repoResp: validGroup,
desc: "view group successfully",
id: validGroup.ID,
session: validSession,
withRoles: false,
repoResp: validGroup,
},
{
desc: "view group with failed to retrieve",
id: testsutil.GenerateUUID(t),
session: validSession,
repoErr: repoerr.ErrNotFound,
err: svcerr.ErrViewEntity,
desc: "view group successfully with roles",
id: validGroupWithRoles.ID,
session: validSession,
withRoles: true,
repoResp: validGroupWithRoles,
},
{
desc: "view group with failed to retrieve",
id: testsutil.GenerateUUID(t),
session: validSession,
withRoles: false,
repoErr: repoerr.ErrNotFound,
err: svcerr.ErrViewEntity,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
repoCall := repo.On("RetrieveByIDAndUser", context.Background(), tc.session.DomainID, tc.session.UserID, tc.id).Return(tc.repoResp, tc.repoErr)
got, err := svc.ViewGroup(context.Background(), validSession, tc.id)
repoCall := repo.On("RetrieveByID", context.Background(), tc.id).Return(tc.repoResp, tc.repoErr)
repoCall1 := repo.On("RetrieveByIDWithRoles", context.Background(), tc.id, tc.session.UserID).Return(tc.repoResp, tc.repoErr)
got, err := svc.ViewGroup(context.Background(), validSession, tc.id, tc.withRoles)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err))
if err == nil {
assert.Equal(t, tc.repoResp, got)
ok := repo.AssertCalled(t, "RetrieveByIDAndUser", context.Background(), tc.session.DomainID, tc.session.UserID, tc.id)
assert.True(t, ok, fmt.Sprintf("RetrieveByIDAndUser was not called on %s", tc.desc))
switch tc.withRoles {
case true:
assert.Equal(t, tc.repoResp, got)
ok := repo.AssertCalled(t, "RetrieveByIDWithRoles", context.Background(), tc.id, tc.session.UserID)
assert.True(t, ok, fmt.Sprintf("RetrieveByIDWithRoles was not called on %s", tc.desc))
default:
assert.Equal(t, tc.repoResp, got)
ok := repo.AssertCalled(t, "RetrieveByID", context.Background(), tc.id)
assert.True(t, ok, fmt.Sprintf("RetrieveByID was not called on %s", tc.desc))
}
}
repoCall.Unset()
repoCall1.Unset()
})
}
}
+3 -3
View File
@@ -38,11 +38,11 @@ func (tm *tracingMiddleware) CreateGroup(ctx context.Context, session authn.Sess
}
// ViewGroup traces the "ViewGroup" operation of the wrapped groups.Service.
func (tm *tracingMiddleware) ViewGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) {
ctx, span := tracing.StartSpan(ctx, tm.tracer, "svc_view_group", trace.WithAttributes(attribute.String("id", id)))
func (tm *tracingMiddleware) ViewGroup(ctx context.Context, session authn.Session, id string, withRoles bool) (groups.Group, error) {
ctx, span := tracing.StartSpan(ctx, tm.tracer, "svc_view_group", trace.WithAttributes(attribute.String("id", id), attribute.Bool("with_roles", withRoles)))
defer span.End()
return tm.svc.ViewGroup(ctx, session, id)
return tm.svc.ViewGroup(ctx, session, id, withRoles)
}
// ListGroups traces the "ListGroups" operation of the wrapped groups.Service.
+13 -11
View File
@@ -11,6 +11,7 @@ import (
apiutil "github.com/absmach/supermq/api/http/util"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/supermq/pkg/roles"
)
const (
@@ -20,17 +21,18 @@ const (
// Channel represents supermq channel.
type Channel struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Tags []string `json:"tags,omitempty"`
ParentGroup string `json:"parent_group_id,omitempty"`
DomainID string `json:"domain_id,omitempty"`
Metadata Metadata `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
UpdatedBy string `json:"updated_by,omitempty"`
Status string `json:"status,omitempty"`
Permissions []string `json:"permissions,omitempty"`
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Tags []string `json:"tags,omitempty"`
ParentGroup string `json:"parent_group_id,omitempty"`
DomainID string `json:"domain_id,omitempty"`
Metadata Metadata `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
UpdatedBy string `json:"updated_by,omitempty"`
Status string `json:"status,omitempty"`
Permissions []string `json:"permissions,omitempty"`
Roles []roles.MemberRoleActions `json:"roles,omitempty"`
}
func (sdk mgSDK) CreateChannel(c Channel, domainID, token string) (Channel, errors.SDKError) {
+41 -3
View File
@@ -627,11 +627,18 @@ func TestViewChannel(t *testing.T) {
}
mgsdk := sdk.NewSDK(conf)
channelResRoles := sdk.Config{
ChannelsURL: ts.URL,
Roles: true,
}
mgsdkRoles := sdk.NewSDK(channelResRoles)
cases := []struct {
desc string
domainID string
token string
session smqauthn.Session
withRoles bool
channelID string
svcRes channels.Channel
svcErr error
@@ -643,6 +650,18 @@ func TestViewChannel(t *testing.T) {
desc: "view channel successfully",
domainID: domainID,
token: validToken,
withRoles: false,
channelID: channelRes.ID,
svcRes: channelRes,
svcErr: nil,
response: channel,
err: nil,
},
{
desc: "view channel successfully with roles",
domainID: domainID,
token: validToken,
withRoles: true,
channelID: channelRes.ID,
svcRes: channelRes,
svcErr: nil,
@@ -653,6 +672,7 @@ func TestViewChannel(t *testing.T) {
desc: "view channel with invalid token",
domainID: domainID,
token: invalidToken,
withRoles: false,
channelID: channelRes.ID,
svcRes: channels.Channel{},
authenticateErr: svcerr.ErrAuthentication,
@@ -663,6 +683,7 @@ func TestViewChannel(t *testing.T) {
desc: "view channel with empty token",
domainID: domainID,
token: "",
withRoles: false,
channelID: channelRes.ID,
svcRes: channels.Channel{},
svcErr: nil,
@@ -673,6 +694,7 @@ func TestViewChannel(t *testing.T) {
desc: "view channel for wrong id",
domainID: domainID,
token: validToken,
withRoles: false,
channelID: wrongID,
svcRes: channels.Channel{},
svcErr: svcerr.ErrViewEntity,
@@ -683,6 +705,7 @@ func TestViewChannel(t *testing.T) {
desc: "view channel with empty channel id",
domainID: domainID,
token: validToken,
withRoles: false,
channelID: "",
svcRes: channels.Channel{},
svcErr: nil,
@@ -693,6 +716,7 @@ func TestViewChannel(t *testing.T) {
desc: "view channel with service response that can't be unmarshalled",
domainID: domainID,
token: validToken,
withRoles: false,
channelID: channelRes.ID,
svcRes: channels.Channel{
ID: generateUUID(t),
@@ -712,12 +736,25 @@ func TestViewChannel(t *testing.T) {
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
}
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
svcCall := gsvc.On("ViewChannel", mock.Anything, tc.session, tc.channelID, false).Return(tc.svcRes, tc.svcErr)
resp, err := mgsdk.Channel(tc.channelID, tc.domainID, tc.token)
svcCall := gsvc.On("ViewChannel", mock.Anything, tc.session, tc.channelID, tc.withRoles).Return(tc.svcRes, tc.svcErr)
var resp sdk.Channel
var err error
switch tc.withRoles {
case true:
resp, err = mgsdkRoles.Channel(tc.channelID, tc.domainID, tc.token)
default:
resp, err = mgsdk.Channel(tc.channelID, tc.domainID, tc.token)
}
assert.Equal(t, tc.err, err)
assert.Equal(t, tc.response, resp)
if tc.withRoles {
assert.Equal(t, resp.Roles, validRoles, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, validRoles, resp.Roles))
}
if tc.err == nil {
ok := svcCall.Parent.AssertCalled(t, "ViewChannel", mock.Anything, tc.session, tc.channelID, false)
ok := svcCall.Parent.AssertCalled(t, "ViewChannel", mock.Anything, tc.session, tc.channelID, tc.withRoles)
assert.True(t, ok)
}
svcCall.Unset()
@@ -2083,6 +2120,7 @@ func generateTestChannel(t *testing.T) sdk.Channel {
CreatedAt: createdAt,
UpdatedAt: updatedAt,
Status: channels.EnabledStatus.String(),
Roles: validRoles,
}
return ch
}
+14 -12
View File
@@ -11,6 +11,7 @@ import (
apiutil "github.com/absmach/supermq/api/http/util"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/supermq/pkg/roles"
)
const (
@@ -25,18 +26,19 @@ const (
// Client represents supermq client.
type Client struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Tags []string `json:"tags,omitempty"`
DomainID string `json:"domain_id,omitempty"`
ParentGroup string `json:"parent_group_id,omitempty"`
Credentials ClientCredentials `json:"credentials"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
UpdatedBy string `json:"updated_by,omitempty"`
Status string `json:"status,omitempty"`
Permissions []string `json:"permissions,omitempty"`
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Tags []string `json:"tags,omitempty"`
DomainID string `json:"domain_id,omitempty"`
ParentGroup string `json:"parent_group_id,omitempty"`
Credentials ClientCredentials `json:"credentials"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
UpdatedBy string `json:"updated_by,omitempty"`
Status string `json:"status,omitempty"`
Permissions []string `json:"permissions,omitempty"`
Roles []roles.MemberRoleActions `json:"roles,omitempty"`
}
type ClientCredentials struct {
+91 -50
View File
@@ -45,7 +45,7 @@ func TestCreateClient(t *testing.T) {
ts, tsvc, auth := setupClients()
defer ts.Close()
client := generateTestClient(t)
client := generateTestClient(t, false)
createClientReq := sdk.Client{
Name: client.Name,
Tags: client.Tags,
@@ -213,7 +213,7 @@ func TestCreateClients(t *testing.T) {
sdkClients := []sdk.Client{}
for i := 0; i < 3; i++ {
client := generateTestClient(t)
client := generateTestClient(t, false)
sdkClients = append(sdkClients, client)
}
@@ -324,7 +324,7 @@ func TestListClients(t *testing.T) {
var sdkClients []sdk.Client
for i := 10; i < 100; i++ {
c := generateTestClient(t)
c := generateTestClient(t, false)
if i == 50 {
c.Status = clients.DisabledStatus.String()
c.Tags = []string{"tag1", "tag2"}
@@ -589,17 +589,25 @@ func TestViewClient(t *testing.T) {
ts, tsvc, auth := setupClients()
defer ts.Close()
sdkClient := generateTestClient(t)
sdkClient := generateTestClient(t, false)
sdkClientWithRoles := generateTestClient(t, true)
conf := sdk.Config{
ClientsURL: ts.URL,
}
mgsdk := sdk.NewSDK(conf)
confRoles := sdk.Config{
ClientsURL: ts.URL,
Roles: true,
}
mgsdkRoles := sdk.NewSDK(confRoles)
cases := []struct {
desc string
domainID string
token string
session smqauthn.Session
withRoles bool
clientID string
svcRes clients.Client
svcErr error
@@ -608,19 +616,32 @@ func TestViewClient(t *testing.T) {
err errors.SDKError
}{
{
desc: "view client successfully",
domainID: domainID,
token: validToken,
clientID: sdkClient.ID,
svcRes: convertClient(sdkClient),
svcErr: nil,
response: sdkClient,
err: nil,
desc: "view client successfully",
domainID: domainID,
token: validToken,
withRoles: false,
clientID: sdkClient.ID,
svcRes: convertClient(sdkClient),
svcErr: nil,
response: sdkClient,
err: nil,
},
{
desc: "view client successfully with roles",
domainID: domainID,
token: validToken,
withRoles: true,
clientID: sdkClientWithRoles.ID,
svcRes: convertClient(sdkClientWithRoles),
svcErr: nil,
response: sdkClientWithRoles,
err: nil,
},
{
desc: "view client with an invalid token",
domainID: domainID,
token: invalidToken,
withRoles: false,
clientID: sdkClient.ID,
svcRes: clients.Client{},
authenticateErr: svcerr.ErrAuthorization,
@@ -628,40 +649,44 @@ func TestViewClient(t *testing.T) {
err: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
},
{
desc: "view client with empty token",
domainID: domainID,
token: "",
clientID: sdkClient.ID,
svcRes: clients.Client{},
svcErr: nil,
response: sdk.Client{},
err: errors.NewSDKErrorWithStatus(apiutil.ErrBearerToken, http.StatusUnauthorized),
desc: "view client with empty token",
domainID: domainID,
token: "",
withRoles: false,
clientID: sdkClient.ID,
svcRes: clients.Client{},
svcErr: nil,
response: sdk.Client{},
err: errors.NewSDKErrorWithStatus(apiutil.ErrBearerToken, http.StatusUnauthorized),
},
{
desc: "view client with an invalid client id",
domainID: domainID,
token: validToken,
clientID: wrongID,
svcRes: clients.Client{},
svcErr: svcerr.ErrViewEntity,
response: sdk.Client{},
err: errors.NewSDKErrorWithStatus(svcerr.ErrViewEntity, http.StatusBadRequest),
desc: "view client with an invalid client id",
domainID: domainID,
token: validToken,
withRoles: false,
clientID: wrongID,
svcRes: clients.Client{},
svcErr: svcerr.ErrViewEntity,
response: sdk.Client{},
err: errors.NewSDKErrorWithStatus(svcerr.ErrViewEntity, http.StatusBadRequest),
},
{
desc: "view client with empty client id",
domainID: domainID,
token: validToken,
clientID: "",
svcRes: clients.Client{},
svcErr: nil,
response: sdk.Client{},
err: errors.NewSDKError(apiutil.ErrMissingID),
desc: "view client with empty client id",
domainID: domainID,
token: validToken,
withRoles: false,
clientID: "",
svcRes: clients.Client{},
svcErr: nil,
response: sdk.Client{},
err: errors.NewSDKError(apiutil.ErrMissingID),
},
{
desc: "view client with response that can't be unmarshalled",
domainID: domainID,
token: validToken,
clientID: sdkClient.ID,
desc: "view client with response that can't be unmarshalled",
domainID: domainID,
token: validToken,
withRoles: false,
clientID: sdkClient.ID,
svcRes: clients.Client{
Name: sdkClient.Name,
Tags: sdkClient.Tags,
@@ -681,12 +706,23 @@ func TestViewClient(t *testing.T) {
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
}
authCall := auth.On("Authenticate", mock.Anything, mock.Anything).Return(tc.session, tc.authenticateErr)
svcCall := tsvc.On("View", mock.Anything, tc.session, tc.clientID, false).Return(tc.svcRes, tc.svcErr)
resp, err := mgsdk.Client(tc.clientID, tc.domainID, tc.token)
svcCall := tsvc.On("View", mock.Anything, tc.session, tc.clientID, tc.withRoles).Return(tc.svcRes, tc.svcErr)
var resp sdk.Client
var err error
switch tc.withRoles {
case true:
resp, err = mgsdkRoles.Client(tc.clientID, tc.domainID, tc.token)
default:
resp, err = mgsdk.Client(tc.clientID, tc.domainID, tc.token)
}
assert.Equal(t, tc.err, err)
assert.Equal(t, tc.response, resp)
if tc.withRoles {
assert.Equal(t, resp.Roles, validRoles, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, validRoles, resp.Roles))
}
if tc.err == nil {
ok := svcCall.Parent.AssertCalled(t, "View", mock.Anything, tc.session, tc.clientID, false)
ok := svcCall.Parent.AssertCalled(t, "View", mock.Anything, tc.session, tc.clientID, tc.withRoles)
assert.True(t, ok)
}
svcCall.Unset()
@@ -699,7 +735,7 @@ func TestUpdateClient(t *testing.T) {
ts, tsvc, auth := setupClients()
defer ts.Close()
sdkClient := generateTestClient(t)
sdkClient := generateTestClient(t, false)
updatedClient := sdkClient
updatedClient.Name = "newName"
updatedClient.Metadata = map[string]interface{}{
@@ -857,7 +893,7 @@ func TestUpdateClientTags(t *testing.T) {
ts, tsvc, auth := setupClients()
defer ts.Close()
sdkClient := generateTestClient(t)
sdkClient := generateTestClient(t, false)
updatedClient := sdkClient
updatedClient.Tags = []string{"newTag1", "newTag2"}
updateClientReq := sdk.Client{
@@ -1009,7 +1045,7 @@ func TestUpdateClientSecret(t *testing.T) {
ts, tsvc, auth := setupClients()
defer ts.Close()
sdkClient := generateTestClient(t)
sdkClient := generateTestClient(t, false)
newSecret := generateUUID(t)
updatedClient := sdkClient
updatedClient.Credentials.Secret = newSecret
@@ -1141,7 +1177,7 @@ func TestEnableClient(t *testing.T) {
ts, tsvc, auth := setupClients()
defer ts.Close()
client := generateTestClient(t)
client := generateTestClient(t, false)
enabledClient := client
enabledClient.Status = clients.EnabledStatus.String()
@@ -1244,7 +1280,7 @@ func TestDisableClient(t *testing.T) {
ts, tsvc, auth := setupClients()
defer ts.Close()
client := generateTestClient(t)
client := generateTestClient(t, false)
disabledClient := client
disabledClient.Status = clients.DisabledStatus.String()
@@ -1347,7 +1383,7 @@ func TestDeleteClient(t *testing.T) {
ts, tsvc, auth := setupClients()
defer ts.Close()
client := generateTestClient(t)
client := generateTestClient(t, false)
conf := sdk.Config{
ClientsURL: ts.URL,
@@ -3213,10 +3249,14 @@ func TestListAvailableClientRoleActions(t *testing.T) {
}
}
func generateTestClient(t *testing.T) sdk.Client {
func generateTestClient(t *testing.T, withRoles bool) sdk.Client {
createdAt, err := time.Parse(time.RFC3339, "2023-03-03T00:00:00Z")
assert.Nil(t, err, fmt.Sprintf("unexpected error %s", err))
updatedAt := createdAt
var rl []roles.MemberRoleActions
if withRoles {
rl = validRoles
}
return sdk.Client{
ID: testsutil.GenerateUUID(t),
Name: "clientname",
@@ -3229,5 +3269,6 @@ func generateTestClient(t *testing.T) sdk.Client {
Status: clients.EnabledStatus.String(),
CreatedAt: createdAt,
UpdatedAt: updatedAt,
Roles: rl,
}
}
+19 -2
View File
@@ -345,6 +345,12 @@ func TestViewDomain(t *testing.T) {
mgsdk := sdk.NewSDK(sdkConf)
sdkConfRoles := sdk.Config{
DomainsURL: ds.URL,
Roles: true,
}
mgsdkRoles := sdk.NewSDK(sdkConfRoles)
cases := []struct {
desc string
token string
@@ -371,7 +377,7 @@ func TestViewDomain(t *testing.T) {
desc: "view domain successfully with roles",
token: validToken,
domainID: sdkDomain.ID,
withRoles: false,
withRoles: true,
svcRes: authDomain,
svcErr: nil,
response: sdkDomain,
@@ -441,7 +447,16 @@ func TestViewDomain(t *testing.T) {
}
authCall := authn.On("Authenticate", mock.Anything, mock.Anything).Return(tc.session, tc.authnErr)
svcCall := svc.On("RetrieveDomain", mock.Anything, tc.session, tc.domainID, tc.withRoles).Return(tc.svcRes, tc.svcErr)
resp, err := mgsdk.Domain(tc.domainID, tc.token)
var resp sdk.Domain
var err error
switch tc.withRoles {
case true:
resp, err = mgsdkRoles.Domain(tc.domainID, tc.token)
default:
resp, err = mgsdk.Domain(tc.domainID, tc.token)
}
assert.Equal(t, tc.err, err)
assert.Equal(t, tc.response, resp)
if tc.withRoles {
@@ -2332,6 +2347,7 @@ func generateTestDomain(t *testing.T) (domains.Domain, sdk.Domain) {
CreatedAt: createdAt,
UpdatedBy: ownerID,
UpdatedAt: createdAt,
Roles: validRoles,
}
sd := sdk.Domain{
@@ -2345,6 +2361,7 @@ func generateTestDomain(t *testing.T) (domains.Domain, sdk.Domain) {
CreatedAt: ad.CreatedAt,
UpdatedBy: ad.UpdatedBy,
UpdatedAt: ad.UpdatedAt,
Roles: ad.Roles,
}
return ad, sd
}
+23 -21
View File
@@ -11,6 +11,7 @@ import (
apiutil "github.com/absmach/supermq/api/http/util"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/supermq/pkg/roles"
)
const (
@@ -25,27 +26,28 @@ const (
// Path in a tree consisting of group IDs
// Paths are unique per owner.
type Group struct {
ID string `json:"id,omitempty"`
DomainID string `json:"domain_id,omitempty"`
ParentID string `json:"parent_id,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Metadata Metadata `json:"metadata,omitempty"`
Level int `json:"level,omitempty"`
Path string `json:"path,omitempty"`
Children []*Group `json:"children,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
UpdatedBy string `json:"updated_by,omitempty"`
Status string `json:"status,omitempty"`
RoleID string `json:"role_id,omitempty"`
RoleName string `json:"role_name,omitempty"`
Actions []string `json:"actions,omitempty"`
AccessType string `json:"access_type,omitempty"`
AccessProviderId string `json:"access_provider_id,omitempty"`
AccessProviderRoleId string `json:"access_provider_role_id,omitempty"`
AccessProviderRoleName string `json:"access_provider_role_name,omitempty"`
AccessProviderRoleActions []string `json:"access_provider_role_actions,omitempty"`
ID string `json:"id,omitempty"`
DomainID string `json:"domain_id,omitempty"`
ParentID string `json:"parent_id,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Metadata Metadata `json:"metadata,omitempty"`
Level int `json:"level,omitempty"`
Path string `json:"path,omitempty"`
Children []*Group `json:"children,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
UpdatedBy string `json:"updated_by,omitempty"`
Status string `json:"status,omitempty"`
RoleID string `json:"role_id,omitempty"`
RoleName string `json:"role_name,omitempty"`
Actions []string `json:"actions,omitempty"`
AccessType string `json:"access_type,omitempty"`
AccessProviderId string `json:"access_provider_id,omitempty"`
AccessProviderRoleId string `json:"access_provider_role_id,omitempty"`
AccessProviderRoleName string `json:"access_provider_role_name,omitempty"`
AccessProviderRoleActions []string `json:"access_provider_role_actions,omitempty"`
Roles []roles.MemberRoleActions `json:"roles,omitempty"`
}
func (sdk mgSDK) CreateGroup(g Group, domainID, token string) (Group, errors.SDKError) {
+77 -39
View File
@@ -523,11 +523,18 @@ func TestViewGroup(t *testing.T) {
}
mgsdk := sdk.NewSDK(conf)
confRoles := sdk.Config{
GroupsURL: ts.URL,
Roles: true,
}
mgsdkRoles := sdk.NewSDK(confRoles)
cases := []struct {
desc string
domainID string
token string
session smqauthn.Session
withRoles bool
groupID string
svcRes groups.Group
svcErr error
@@ -536,19 +543,32 @@ func TestViewGroup(t *testing.T) {
err errors.SDKError
}{
{
desc: "view group successfully",
domainID: domainID,
token: validToken,
groupID: group.ID,
svcRes: group,
svcErr: nil,
response: sdkGroup,
err: nil,
desc: "view group successfully",
domainID: domainID,
token: validToken,
withRoles: false,
groupID: group.ID,
svcRes: group,
svcErr: nil,
response: sdkGroup,
err: nil,
},
{
desc: "view group successfully with roles",
domainID: domainID,
token: validToken,
withRoles: true,
groupID: group.ID,
svcRes: group,
svcErr: nil,
response: sdkGroup,
err: nil,
},
{
desc: "view group with invalid token",
domainID: domainID,
token: invalidToken,
withRoles: false,
groupID: group.ID,
svcRes: groups.Group{},
authenticateErr: svcerr.ErrAuthentication,
@@ -556,30 +576,33 @@ func TestViewGroup(t *testing.T) {
err: errors.NewSDKErrorWithStatus(svcerr.ErrAuthentication, http.StatusUnauthorized),
},
{
desc: "view group with empty token",
domainID: domainID,
token: "",
groupID: group.ID,
svcRes: groups.Group{},
svcErr: nil,
response: sdk.Group{},
err: errors.NewSDKErrorWithStatus(apiutil.ErrBearerToken, http.StatusUnauthorized),
desc: "view group with empty token",
domainID: domainID,
token: "",
withRoles: false,
groupID: group.ID,
svcRes: groups.Group{},
svcErr: nil,
response: sdk.Group{},
err: errors.NewSDKErrorWithStatus(apiutil.ErrBearerToken, http.StatusUnauthorized),
},
{
desc: "view group with invalid group id",
domainID: domainID,
token: validToken,
groupID: wrongID,
svcRes: groups.Group{},
svcErr: svcerr.ErrViewEntity,
response: sdk.Group{},
err: errors.NewSDKErrorWithStatus(svcerr.ErrViewEntity, http.StatusBadRequest),
desc: "view group with invalid group id",
domainID: domainID,
token: validToken,
withRoles: false,
groupID: wrongID,
svcRes: groups.Group{},
svcErr: svcerr.ErrViewEntity,
response: sdk.Group{},
err: errors.NewSDKErrorWithStatus(svcerr.ErrViewEntity, http.StatusBadRequest),
},
{
desc: "view group with service response that cannot be unmarshalled",
domainID: domainID,
token: validToken,
groupID: group.ID,
desc: "view group with service response that cannot be unmarshalled",
domainID: domainID,
token: validToken,
withRoles: false,
groupID: group.ID,
svcRes: groups.Group{
ID: group.ID,
Name: "group_1",
@@ -592,14 +615,15 @@ func TestViewGroup(t *testing.T) {
err: errors.NewSDKError(errors.New("unexpected end of JSON input")),
},
{
desc: "view group with empty id",
domainID: domainID,
token: validToken,
groupID: "",
svcRes: groups.Group{},
svcErr: nil,
response: sdk.Group{},
err: errors.NewSDKError(apiutil.ErrMissingID),
desc: "view group with empty id",
domainID: domainID,
token: validToken,
withRoles: false,
groupID: "",
svcRes: groups.Group{},
svcErr: nil,
response: sdk.Group{},
err: errors.NewSDKError(apiutil.ErrMissingID),
},
}
@@ -609,12 +633,25 @@ func TestViewGroup(t *testing.T) {
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
}
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
svcCall := gsvc.On("ViewGroup", mock.Anything, tc.session, tc.groupID).Return(tc.svcRes, tc.svcErr)
resp, err := mgsdk.Group(tc.groupID, tc.domainID, tc.token)
svcCall := gsvc.On("ViewGroup", mock.Anything, tc.session, tc.groupID, tc.withRoles).Return(tc.svcRes, tc.svcErr)
var resp sdk.Group
var err error
switch tc.withRoles {
case true:
resp, err = mgsdkRoles.Group(tc.groupID, tc.domainID, tc.token)
default:
resp, err = mgsdk.Group(tc.groupID, tc.domainID, tc.token)
}
assert.Equal(t, tc.err, err)
assert.Equal(t, tc.response, resp)
if tc.withRoles {
assert.Equal(t, resp.Roles, validRoles, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, validRoles, resp.Roles))
}
if tc.err == nil {
ok := svcCall.Parent.AssertCalled(t, "ViewGroup", mock.Anything, tc.session, tc.groupID)
ok := svcCall.Parent.AssertCalled(t, "ViewGroup", mock.Anything, tc.session, tc.groupID, tc.withRoles)
assert.True(t, ok)
}
svcCall.Unset()
@@ -3631,6 +3668,7 @@ func generateTestGroup(t *testing.T) sdk.Group {
CreatedAt: createdAt,
UpdatedAt: updatedAt,
Status: groups.EnabledStatus.String(),
Roles: validRoles,
}
return gr
}
+6
View File
@@ -1324,6 +1324,7 @@ type mgSDK struct {
msgContentType ContentType
client *http.Client
curlFlag bool
roles bool
}
// Config contains sdk configuration parameters.
@@ -1341,6 +1342,7 @@ type Config struct {
MsgContentType ContentType
TLSVerification bool
CurlFlag bool
Roles bool
}
// NewSDK returns new supermq SDK instance.
@@ -1365,12 +1367,16 @@ func NewSDK(conf Config) SDK {
},
},
curlFlag: conf.CurlFlag,
roles: conf.Roles,
}
}
// processRequest creates and send a new HTTP request, and checks for errors in the HTTP response.
// It then returns the response headers, the response body, and the associated error(s) (if any).
func (sdk mgSDK) processRequest(method, reqUrl, token string, data []byte, headers map[string]string, expectedRespCodes ...int) (http.Header, []byte, errors.SDKError) {
if sdk.roles {
reqUrl = reqUrl + fmt.Sprintf("?roles=%v", true)
}
req, err := http.NewRequest(method, reqUrl, bytes.NewReader(data))
if err != nil {
return make(http.Header), []byte{}, errors.NewSDKError(err)
+3
View File
@@ -128,6 +128,7 @@ func convertGroup(g sdk.Group) groups.Group {
AccessProviderRoleId: g.AccessProviderRoleId,
AccessProviderRoleName: g.AccessProviderRoleName,
AccessProviderRoleActions: g.AccessProviderRoleActions,
Roles: g.Roles,
}
}
@@ -194,6 +195,7 @@ func convertClient(c sdk.Client) clients.Client {
UpdatedAt: c.UpdatedAt,
UpdatedBy: c.UpdatedBy,
Status: status,
Roles: c.Roles,
}
}
@@ -216,6 +218,7 @@ func convertChannel(g sdk.Channel) mgchannels.Channel {
UpdatedAt: g.UpdatedAt,
UpdatedBy: g.UpdatedBy,
Status: status,
Roles: g.Roles,
}
}