Skip to content

Commit

Permalink
JUJU-7073: add filter on group name/id for listGroups (#1426)
Browse files Browse the repository at this point in the history
* add filter on group name/id for listGroups

* improvo godoc

* move fuzzy search to db layer

* refactor foreachgroup to list groups

* update godoc

* update godoc

* rename func arg from filter to pagination

* update godoc

* Trigger Ci

* add uuid fuzzy match
  • Loading branch information
SimoneDutto authored Nov 14, 2024
1 parent 65634b4 commit 8001a25
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 50 deletions.
35 changes: 12 additions & 23 deletions internal/db/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,41 +82,30 @@ func (d *Database) GetGroup(ctx context.Context, group *dbmodel.GroupEntry) (err
return nil
}

// ForEachGroup iterates through every group calling the given function
// for each one. If the given function returns an error the iteration
// will stop immediately and the error will be returned unmodified.
func (d *Database) ForEachGroup(ctx context.Context, limit, offset int, f func(*dbmodel.GroupEntry) error) (err error) {
const op = errors.Op("db.ForEachGroup")
// ListGroups returns a paginated list of groups defined by limit and offset.
// match is used to fuzzy find based on entries' name or uuid using the LIKE operator (ex. LIKE %<match>%).
func (d *Database) ListGroups(ctx context.Context, limit, offset int, match string) (_ []dbmodel.GroupEntry, err error) {
const op = errors.Op("db.ListGroups")
if err := d.ready(); err != nil {
return errors.E(op, err)
return nil, errors.E(op, err)
}

durationObserver := servermon.DurationObserver(servermon.DBQueryDurationHistogram, string(op))
defer durationObserver()
defer servermon.ErrorCounter(servermon.DBQueryErrorCount, &err, string(op))

db := d.DB.WithContext(ctx)
if match != "" {
db = db.Where("name LIKE ? OR uuid LIKE ?", "%"+match+"%", "%"+match+"%")
}
db = db.Order("name asc")
db = db.Limit(limit)
db = db.Offset(offset)
rows, err := db.Model(&dbmodel.GroupEntry{}).Rows()
if err != nil {
return errors.E(op, err)
}
defer rows.Close()
for rows.Next() {
var group dbmodel.GroupEntry
if err := db.ScanRows(rows, &group); err != nil {
return errors.E(op, err)
}
if err := f(&group); err != nil {
return err
}
}
if err := rows.Err(); err != nil {
return errors.E(op, dbError(err))
var groups []dbmodel.GroupEntry
if err := db.Find(&groups).Error; err != nil {
return nil, errors.E(op, dbError(err))
}
return nil
return groups, nil
}

// UpdateGroup updates the group identified by its ID.
Expand Down
34 changes: 24 additions & 10 deletions internal/db/group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,23 +193,37 @@ func (s *dbSuite) TestForEachGroup(c *qt.C) {
_, err := s.Database.AddGroup(context.Background(), fmt.Sprintf("test-group-%d", i))
c.Assert(err, qt.IsNil)
}
firstGroups := []*dbmodel.GroupEntry{}
ctx := context.Background()
err = s.Database.ForEachGroup(ctx, 5, 0, func(ge *dbmodel.GroupEntry) error {
firstGroups = append(firstGroups, ge)
return nil
})
firstGroups, err := s.Database.ListGroups(ctx, 5, 0, "")
c.Assert(err, qt.IsNil)
for i := 0; i < 5; i++ {
c.Assert(firstGroups[i].Name, qt.Equals, fmt.Sprintf("test-group-%d", i))
}
secondGroups := []*dbmodel.GroupEntry{}
err = s.Database.ForEachGroup(ctx, 5, 5, func(ge *dbmodel.GroupEntry) error {
secondGroups = append(secondGroups, ge)
return nil
})
secondGroups, err := s.Database.ListGroups(ctx, 5, 5, "")
c.Assert(err, qt.IsNil)
for i := 0; i < 5; i++ {
c.Assert(secondGroups[i].Name, qt.Equals, fmt.Sprintf("test-group-%d", i+5))
}

matchedGroups, err := s.Database.ListGroups(ctx, 5, 0, "group-1")
c.Assert(err, qt.IsNil)
c.Assert(matchedGroups, qt.HasLen, 1)
c.Assert(matchedGroups[0].Name, qt.Equals, "test-group-1")

matchedGroups, err = s.Database.ListGroups(ctx, 5, 0, "%not-existing%")
c.Assert(err, qt.IsNil)
c.Assert(matchedGroups, qt.HasLen, 0)

tg, err := s.Database.AddGroup(context.Background(), "\\%test-group")
c.Assert(err, qt.IsNil)

matchedGroups, err = s.Database.ListGroups(ctx, 5, 0, "\\%t")
c.Assert(err, qt.IsNil)
c.Assert(matchedGroups, qt.HasLen, 1)
c.Assert(matchedGroups[0].UUID, qt.Equals, tg.UUID)

matchedGroups, err = s.Database.ListGroups(ctx, 5, 0, tg.UUID)
c.Assert(err, qt.IsNil)
c.Assert(matchedGroups, qt.HasLen, 1)
c.Assert(matchedGroups[0].UUID, qt.Equals, tg.UUID)
}
9 changes: 3 additions & 6 deletions internal/jimm/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -790,18 +790,15 @@ func (j *JIMM) RemoveGroup(ctx context.Context, user *openfga.User, name string)
}

// ListGroups returns a list of groups known to JIMM.
func (j *JIMM) ListGroups(ctx context.Context, user *openfga.User, filter pagination.LimitOffsetPagination) ([]dbmodel.GroupEntry, error) {
// `match` will filter the list fuzzy matching group's name or uuid.
func (j *JIMM) ListGroups(ctx context.Context, user *openfga.User, pagination pagination.LimitOffsetPagination, match string) ([]dbmodel.GroupEntry, error) {
const op = errors.Op("jimm.ListGroups")

if !user.JimmAdmin {
return nil, errors.E(op, errors.CodeUnauthorized, "unauthorized")
}

var groups []dbmodel.GroupEntry
err := j.Database.ForEachGroup(ctx, filter.Limit(), filter.Offset(), func(ge *dbmodel.GroupEntry) error {
groups = append(groups, *ge)
return nil
})
groups, err := j.Database.ListGroups(ctx, pagination.Limit(), pagination.Offset(), match)
if err != nil {
return nil, errors.E(op, err)
}
Expand Down
6 changes: 3 additions & 3 deletions internal/jimm/access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1079,8 +1079,8 @@ func TestListGroups(t *testing.T) {
u := openfga.NewUser(&user, ofgaClient)
u.JimmAdmin = true

filter := pagination.NewOffsetFilter(10, 0)
groups, err := j.ListGroups(ctx, u, filter)
pagination := pagination.NewOffsetFilter(10, 0)
groups, err := j.ListGroups(ctx, u, pagination, "")
c.Assert(err, qt.IsNil)
c.Assert(groups, qt.DeepEquals, []dbmodel.GroupEntry{group})

Expand All @@ -1095,7 +1095,7 @@ func TestListGroups(t *testing.T) {
_, err := j.AddGroup(ctx, u, name)
c.Assert(err, qt.IsNil)
}
groups, err = j.ListGroups(ctx, u, filter)
groups, err = j.ListGroups(ctx, u, pagination, "")
c.Assert(err, qt.IsNil)
sort.Slice(groups, func(i, j int) bool {
return groups[i].Name < groups[j].Name
Expand Down
6 changes: 5 additions & 1 deletion internal/jimmhttp/rebac_admin/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ func (s *groupsService) ListGroups(ctx context.Context, params *resources.GetGro
return nil, err
}
page, nextPage, pagination := pagination.CreatePagination(params.Size, params.Page, count)
groups, err := s.jimm.ListGroups(ctx, user, pagination)
match := ""
if params.Filter != nil && *params.Filter != "" {
match = *params.Filter
}
groups, err := s.jimm.ListGroups(ctx, user, pagination, match)
if err != nil {
return nil, err
}
Expand Down
31 changes: 31 additions & 0 deletions internal/jimmhttp/rebac_admin/groups_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,37 @@ func (s *rebacAdminSuite) SetUpTest(c *gc.C) {

var _ = gc.Suite(&rebacAdminSuite{})

func (s rebacAdminSuite) TestListGroupsWithFilterIntegration(c *gc.C) {
ctx := context.Background()
for i := range 10 {
_, err := s.JIMM.AddGroup(ctx, s.AdminUser, fmt.Sprintf("test-group-filter-%d", i))
c.Assert(err, gc.IsNil)
}

ctx = rebac_handlers.ContextWithIdentity(ctx, s.AdminUser)
pageSize := 5
page := 0
params := &resources.GetGroupsParams{Size: &pageSize, Page: &page}
res, err := s.groupSvc.ListGroups(ctx, params)
c.Assert(err, gc.IsNil)
c.Assert(res, gc.Not(gc.IsNil))
c.Assert(res.Meta.Size, gc.Equals, 5)

match := "group-filter-1"
params.Filter = &match
res, err = s.groupSvc.ListGroups(ctx, params)
c.Assert(err, gc.IsNil)
c.Assert(res, gc.Not(gc.IsNil))
c.Assert(len(res.Data), gc.Equals, 1)

match = "group"
params.Filter = &match
res, err = s.groupSvc.ListGroups(ctx, params)
c.Assert(err, gc.IsNil)
c.Assert(res, gc.Not(gc.IsNil))
c.Assert(len(res.Data), gc.Equals, pageSize)
}

func (s rebacAdminSuite) TestGetGroupIdentitiesIntegration(c *gc.C) {
ctx := context.Background()
group, err := s.JIMM.AddGroup(ctx, s.AdminUser, "test-group")
Expand Down
2 changes: 1 addition & 1 deletion internal/jimmhttp/rebac_admin/groups_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func TestListGroups(t *testing.T) {
}
jimm := jimmtest.JIMM{
GroupService: mocks.GroupService{
ListGroups_: func(ctx context.Context, user *openfga.User, filter pagination.LimitOffsetPagination) ([]dbmodel.GroupEntry, error) {
ListGroups_: func(ctx context.Context, user *openfga.User, pagination pagination.LimitOffsetPagination, match string) ([]dbmodel.GroupEntry, error) {
return returnedGroups, listErr
},
CountGroups_: func(ctx context.Context, user *openfga.User) (int, error) {
Expand Down
6 changes: 3 additions & 3 deletions internal/jujuapi/access_control.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type GroupService interface {
CountGroups(ctx context.Context, user *openfga.User) (int, error)
GetGroupByUUID(ctx context.Context, user *openfga.User, uuid string) (*dbmodel.GroupEntry, error)
GetGroupByName(ctx context.Context, user *openfga.User, name string) (*dbmodel.GroupEntry, error)
ListGroups(ctx context.Context, user *openfga.User, filter pagination.LimitOffsetPagination) ([]dbmodel.GroupEntry, error)
ListGroups(ctx context.Context, user *openfga.User, pagination pagination.LimitOffsetPagination, match string) ([]dbmodel.GroupEntry, error)
RenameGroup(ctx context.Context, user *openfga.User, oldName, newName string) error
RemoveGroup(ctx context.Context, user *openfga.User, name string) error
}
Expand Down Expand Up @@ -118,8 +118,8 @@ func (r *controllerRoot) RemoveGroup(ctx context.Context, req apiparams.RemoveGr
func (r *controllerRoot) ListGroups(ctx context.Context, req apiparams.ListGroupsRequest) (apiparams.ListGroupResponse, error) {
const op = errors.Op("jujuapi.ListGroups")

filter := pagination.NewOffsetFilter(req.Limit, req.Offset)
groups, err := r.jimm.ListGroups(ctx, r.user, filter)
pagination := pagination.NewOffsetFilter(req.Limit, req.Offset)
groups, err := r.jimm.ListGroups(ctx, r.user, pagination, "")
if err != nil {
return apiparams.ListGroupResponse{}, errors.E(op, err)
}
Expand Down
6 changes: 3 additions & 3 deletions internal/testutils/jimmtest/mocks/jimm_group_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type GroupService struct {
CountGroups_ func(ctx context.Context, user *openfga.User) (int, error)
GetGroupByUUID_ func(ctx context.Context, user *openfga.User, uuid string) (*dbmodel.GroupEntry, error)
GetGroupByName_ func(ctx context.Context, user *openfga.User, name string) (*dbmodel.GroupEntry, error)
ListGroups_ func(ctx context.Context, user *openfga.User, filter pagination.LimitOffsetPagination) ([]dbmodel.GroupEntry, error)
ListGroups_ func(ctx context.Context, user *openfga.User, pagination pagination.LimitOffsetPagination, match string) ([]dbmodel.GroupEntry, error)
RenameGroup_ func(ctx context.Context, user *openfga.User, oldName, newName string) error
RemoveGroup_ func(ctx context.Context, user *openfga.User, name string) error
}
Expand Down Expand Up @@ -55,11 +55,11 @@ func (j *GroupService) GetGroupByName(ctx context.Context, user *openfga.User, n
return j.GetGroupByName_(ctx, user, name)
}

func (j *GroupService) ListGroups(ctx context.Context, user *openfga.User, filters pagination.LimitOffsetPagination) ([]dbmodel.GroupEntry, error) {
func (j *GroupService) ListGroups(ctx context.Context, user *openfga.User, pagination pagination.LimitOffsetPagination, match string) ([]dbmodel.GroupEntry, error) {
if j.ListGroups_ == nil {
return nil, errors.E(errors.CodeNotImplemented)
}
return j.ListGroups_(ctx, user, filters)
return j.ListGroups_(ctx, user, pagination, match)
}

func (j *GroupService) RemoveGroup(ctx context.Context, user *openfga.User, name string) error {
Expand Down

0 comments on commit 8001a25

Please sign in to comment.