diff --git a/docs/api.swagger.yaml b/docs/api.swagger.yaml index 987d73ec56..977f12cc35 100644 --- a/docs/api.swagger.yaml +++ b/docs/api.swagger.yaml @@ -1384,7 +1384,7 @@ paths: { - "resources": ["rn:hydra:warden:groups:"], + "resources": ["rn:hydra:warden:groups"], "actions": ["get"], @@ -1403,8 +1403,8 @@ paths: tags: - warden - groups - summary: Find group IDs by member - operationId: findGroupsByMember + summary: List group IDs + operationId: listGroups security: - oauth2: - hydra.groups @@ -1415,9 +1415,21 @@ paths: description: The id of the member to look up. name: member in: query + - type: integer + format: int64 + x-go-name: Offset + description: The offset from where to start looking if member isn't specified. + name: offset + in: query + - type: integer + format: int64 + x-go-name: Limit + description: The maximum amount of policies returned if member isn't specified. + name: limit + in: query responses: '200': - $ref: '#/responses/findGroupsByMemberResponse' + $ref: '#/responses/listGroupsResponse' '401': $ref: '#/responses/genericError' '403': @@ -2721,7 +2733,7 @@ responses: headers: description: type: string - findGroupsByMemberResponse: + listGroupsResponse: description: A list of groups the member is belonging to schema: type: array diff --git a/warden/group/doc.go b/warden/group/doc.go index 930c154e16..178834cc32 100644 --- a/warden/group/doc.go +++ b/warden/group/doc.go @@ -2,17 +2,25 @@ package group // A list of groups the member is belonging to -// swagger:response findGroupsByMemberResponse -type swaggerFindGroupsByMemberResponse struct { +// swagger:response listGroupsResponse +type swaggerListGroupsResponse struct { // in: body Body []string } -// swagger:parameters findGroupsByMember -type swaggerFindGroupsByMemberParameters struct { +// swagger:parameters listGroups +type swaggerListGroupsParameters struct { // The id of the member to look up. // in: query Member int `json:"member"` + + // The offset from where to start looking if member isn't specified. + // in: query + Offset int `json:"offset"` + + // The maximum amount of policies returned if member isn't specified. + // in: query + Limit int `json:"limit"` } // swagger:parameters getGroup deleteGroup diff --git a/warden/group/handler.go b/warden/group/handler.go index cb28cb7f8b..20ad723180 100644 --- a/warden/group/handler.go +++ b/warden/group/handler.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "strconv" "github.com/julienschmidt/httprouter" "github.com/ory/herodot" @@ -40,15 +41,15 @@ func (h *Handler) SetRoutes(r *httprouter.Router) { r.DELETE(GroupsHandlerPath+"/:id/members", h.RemoveGroupMembers) } -// swagger:route GET /warden/groups warden groups findGroupsByMember +// swagger:route GET /warden/groups warden groups listGroups // -// Find group IDs by member +// List group IDs // // The subject making the request needs to be assigned to a policy containing: // // ``` // { -// "resources": ["rn:hydra:warden:groups:"], +// "resources": ["rn:hydra:warden:groups"], // "actions": ["get"], // "effect": "allow" // } @@ -66,7 +67,7 @@ func (h *Handler) SetRoutes(r *httprouter.Router) { // oauth2: hydra.groups // // Responses: -// 200: findGroupsByMemberResponse +// 200: listGroupsResponse // 401: genericError // 403: genericError // 500: genericError @@ -74,16 +75,43 @@ func (h *Handler) FindGroupNames(w http.ResponseWriter, r *http.Request, _ httpr var ctx = r.Context() var member = r.URL.Query().Get("member") - g, err := h.Manager.FindGroupNames(member) - if err != nil { + accessReq := &firewall.TokenAccessRequest{ + Resource: GroupsResource, + Action: "get", + } + + if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), accessReq, Scope); err != nil { h.H.WriteError(w, r, err) return } - if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &firewall.TokenAccessRequest{ - Resource: fmt.Sprintf(GroupResource, member), - Action: "get", - }, Scope); err != nil { + if member != "" { + g, err := h.Manager.FindGroupNames(member) + + if err != nil { + h.H.WriteError(w, r, err) + return + } + + h.H.Write(w, r, g) + return + } + + limit, err := intFromQuery(r, "limit", 500) + if err != nil { + h.H.WriteError(w, r, errors.WithStack(err)) + return + } + + offset, err := intFromQuery(r, "offset", 0) + if err != nil { + h.H.WriteError(w, r, errors.WithStack(err)) + return + } + + g, err := h.Manager.ListGroups(limit, offset) + + if err != nil { h.H.WriteError(w, r, err) return } @@ -91,6 +119,15 @@ func (h *Handler) FindGroupNames(w http.ResponseWriter, r *http.Request, _ httpr h.H.Write(w, r, g) } +func intFromQuery(r *http.Request, key string, def int64) (int64, error) { + val := r.URL.Query().Get(key) + if val == "" { + return def, nil + } + + return strconv.ParseInt(val, 10, 64) +} + // swagger:route POST /warden/groups warden groups createGroup // // Create a group diff --git a/warden/group/manager.go b/warden/group/manager.go index 6f20a50f6d..9efa422755 100644 --- a/warden/group/manager.go +++ b/warden/group/manager.go @@ -19,5 +19,6 @@ type Manager interface { AddGroupMembers(group string, members []string) error RemoveGroupMembers(group string, members []string) error + ListGroups(limit, offset int64) ([]string, error) FindGroupNames(subject string) ([]string, error) } diff --git a/warden/group/manager_http.go b/warden/group/manager_http.go index 64f8d68ac4..542a853c12 100644 --- a/warden/group/manager_http.go +++ b/warden/group/manager_http.go @@ -1,6 +1,7 @@ package group import ( + "fmt" "net/http" "net/url" @@ -19,6 +20,8 @@ type HTTPManager struct { Dry bool } +var _ Manager = (*HTTPManager)(nil) + func (m *HTTPManager) CreateGroup(g *Group) error { var r = pkg.NewSuperAgent(m.Endpoint.String()) r.Client = m.Client @@ -91,6 +94,19 @@ func (m *HTTPManager) RemoveGroupMembers(group string, members []string) error { return nil } +func (m *HTTPManager) ListGroups(limit, offset int64) ([]string, error) { + var g []string + var r = pkg.NewSuperAgent(m.Endpoint.String() + fmt.Sprintf("?limit=%d&offset=%d", limit, offset)) + r.Client = m.Client + r.Dry = m.Dry + r.FakeTLSTermination = m.FakeTLSTermination + if err := r.Get(&g); err != nil { + return nil, err + } + + return g, nil +} + func (m *HTTPManager) FindGroupNames(subject string) ([]string, error) { var g []string var r = pkg.NewSuperAgent(m.Endpoint.String() + "?member=" + subject) diff --git a/warden/group/manager_memory.go b/warden/group/manager_memory.go index 3320732fd5..2c7931830a 100644 --- a/warden/group/manager_memory.go +++ b/warden/group/manager_memory.go @@ -1,6 +1,7 @@ package group import ( + "sort" "sync" "github.com/pborman/uuid" @@ -18,6 +19,8 @@ type MemoryManager struct { sync.RWMutex } +var _ Manager = (*MemoryManager)(nil) + func (m *MemoryManager) CreateGroup(g *Group) error { if g.ID == "" { g.ID = uuid.New() @@ -76,12 +79,42 @@ func (m *MemoryManager) RemoveGroupMembers(group string, subjects []string) erro return m.CreateGroup(g) } +func (m *MemoryManager) ListGroups(limit, offset int64) ([]string, error) { + if limit <= 0 { + limit = 500 + } + + if offset < 0 { + offset = 0 + } + + res := []string{} + + if offset >= int64(len(m.Groups)) { + return res, nil + } + + for _, g := range m.Groups { + res = append(res, g.ID) + } + + sort.Strings(res) + + res = res[offset:] + + if limit < int64(len(res)) { + res = res[:limit] + } + + return res, nil +} + func (m *MemoryManager) FindGroupNames(subject string) ([]string, error) { if m.Groups == nil { m.Groups = map[string]Group{} } - var res []string + res := []string{} for _, g := range m.Groups { for _, s := range g.Members { if s == subject { diff --git a/warden/group/manager_sql.go b/warden/group/manager_sql.go index 76fc04d5be..8c269255e1 100644 --- a/warden/group/manager_sql.go +++ b/warden/group/manager_sql.go @@ -31,6 +31,8 @@ type SQLManager struct { DB *sqlx.DB } +var _ Manager = (*SQLManager)(nil) + func (s *SQLManager) CreateSchemas() (int, error) { migrate.SetTable("hydra_groups_migration") n, err := migrate.Exec(s.DB.DB, s.DB.DriverName(), migrations, migrate.Up) @@ -123,8 +125,26 @@ func (m *SQLManager) RemoveGroupMembers(group string, subjects []string) error { return nil } +func (m *SQLManager) ListGroups(limit, offset int64) ([]string, error) { + if limit <= 0 { + limit = 500 + } + + if offset < 0 { + offset = 0 + } + + q := []string{} + + if err := m.DB.Select(&q, m.DB.Rebind("SELECT id from hydra_warden_group ORDER BY id LIMIT ? OFFSET ?"), limit, offset); err != nil { + return nil, errors.WithStack(err) + } + + return q, nil +} + func (m *SQLManager) FindGroupNames(subject string) ([]string, error) { - var q []string + q := []string{} if err := m.DB.Select(&q, m.DB.Rebind("SELECT group_id from hydra_warden_group_member WHERE member = ? GROUP BY group_id"), subject); err != nil { return nil, errors.WithStack(err) } diff --git a/warden/group/manager_test.go b/warden/group/manager_test.go index 411d5942ae..478754eda5 100644 --- a/warden/group/manager_test.go +++ b/warden/group/manager_test.go @@ -85,6 +85,8 @@ func connectToPG() { } func TestManagers(t *testing.T) { + t.Parallel() + for k, m := range clientManagers { t.Run(fmt.Sprintf("case=%s", k), TestHelperManagers(m)) } diff --git a/warden/group/manager_test_helper.go b/warden/group/manager_test_helper.go index 99a0cee436..ea473119d1 100644 --- a/warden/group/manager_test_helper.go +++ b/warden/group/manager_test_helper.go @@ -9,7 +9,27 @@ import ( func TestHelperManagers(m Manager) func(t *testing.T) { return func(t *testing.T) { - _, err := m.GetGroup("4321") + ds, err := m.ListGroups(0, 0) + assert.NoError(t, err) + assert.Empty(t, ds) + assert.NotNil(t, ds) + + ds, err = m.ListGroups(-1, 0) + assert.NoError(t, err) + assert.Empty(t, ds) + assert.NotNil(t, ds) + + ds, err = m.ListGroups(0, -1) + assert.NoError(t, err) + assert.Empty(t, ds) + assert.NotNil(t, ds) + + ds, err = m.ListGroups(-1, -1) + assert.NoError(t, err) + assert.Empty(t, ds) + assert.NotNil(t, ds) + + _, err = m.GetGroup("4321") assert.NotNil(t, err) c := &Group{ @@ -17,33 +37,94 @@ func TestHelperManagers(m Manager) func(t *testing.T) { Members: []string{"bar", "foo"}, } assert.NoError(t, m.CreateGroup(c)) + ds, err = m.ListGroups(0, 0) + require.NoError(t, err) + assert.Len(t, ds, 1) + assert.NotNil(t, ds) + + ds, err = m.ListGroups(0, 1) + require.NoError(t, err) + assert.Len(t, ds, 0) + assert.NotNil(t, ds) + assert.NoError(t, m.CreateGroup(&Group{ ID: "2", Members: []string{"foo"}, })) + ds, err = m.ListGroups(0, 0) + require.NoError(t, err) + assert.Len(t, ds, 2) + assert.NotNil(t, ds) + + ds, err = m.ListGroups(0, 1) + require.NoError(t, err) + assert.Len(t, ds, 1) + assert.NotNil(t, ds) + + ds, err = m.ListGroups(0, 2) + require.NoError(t, err) + assert.Len(t, ds, 0) + assert.NotNil(t, ds) + + ds, err = m.ListGroups(1, 0) + require.NoError(t, err) + assert.Len(t, ds, 1) + assert.NotNil(t, ds) + + ds, err = m.ListGroups(2, 0) + require.NoError(t, err) + assert.Len(t, ds, 2) + assert.NotNil(t, ds) + + ds, err = m.ListGroups(1, 1) + require.NoError(t, err) + assert.Len(t, ds, 1) + assert.NotNil(t, ds) + + ds, err = m.ListGroups(0, 2) + require.NoError(t, err) + assert.Len(t, ds, 0) + assert.NotNil(t, ds) + + assert.NoError(t, m.CreateGroup(&Group{ + ID: "3", + Members: []string{"bar"}, + })) + ds, err = m.ListGroups(0, 0) + require.NoError(t, err) + assert.Len(t, ds, 3) + assert.NotNil(t, ds) d, err := m.GetGroup("1") require.NoError(t, err) assert.EqualValues(t, c.Members, d.Members) assert.EqualValues(t, c.ID, d.ID) - ds, err := m.FindGroupNames("foo") + ds, err = m.FindGroupNames("foo") require.NoError(t, err) assert.Len(t, ds, 2) + assert.NotNil(t, ds) assert.NoError(t, m.AddGroupMembers("1", []string{"baz"})) ds, err = m.FindGroupNames("baz") require.NoError(t, err) assert.Len(t, ds, 1) + assert.NotNil(t, ds) assert.NoError(t, m.RemoveGroupMembers("1", []string{"baz"})) ds, err = m.FindGroupNames("baz") require.NoError(t, err) assert.Len(t, ds, 0) + assert.NotNil(t, ds) assert.NoError(t, m.DeleteGroup("1")) _, err = m.GetGroup("1") require.NotNil(t, err) + + ds, err = m.ListGroups(0, 0) + require.NoError(t, err) + assert.Len(t, ds, 2) + assert.NotNil(t, ds) } }