diff --git a/changelog/unreleased/groupprovider.md b/changelog/unreleased/groupprovider.md new file mode 100644 index 0000000000..116a14c704 --- /dev/null +++ b/changelog/unreleased/groupprovider.md @@ -0,0 +1,8 @@ +Enhancement: Add stubs and manager for groupprovider service + +Recently, there was a separation of concerns with regard to users and groups in +CS3APIs. This PR adds the required stubs and drivers for the group manager. + +https://github.com/cs3org/cs3apis/pull/99 +https://github.com/cs3org/cs3apis/pull/102 +https://github.com/cs3org/reva/pull/1358 diff --git a/cmd/revad/runtime/loader.go b/cmd/revad/runtime/loader.go index eab2d41111..acfd433b1a 100644 --- a/cmd/revad/runtime/loader.go +++ b/cmd/revad/runtime/loader.go @@ -30,6 +30,7 @@ import ( _ "github.com/cs3org/reva/pkg/auth/manager/loader" _ "github.com/cs3org/reva/pkg/auth/registry/loader" _ "github.com/cs3org/reva/pkg/cbox/loader" + _ "github.com/cs3org/reva/pkg/group/manager/loader" _ "github.com/cs3org/reva/pkg/metrics/driver/loader" _ "github.com/cs3org/reva/pkg/ocm/invite/manager/loader" _ "github.com/cs3org/reva/pkg/ocm/provider/authorizer/loader" diff --git a/go.mod b/go.mod index 999775a561..1ff7ef2090 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/cheggaaa/pb v1.0.29 github.com/coreos/go-oidc v2.2.1+incompatible github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e - github.com/cs3org/go-cs3apis v0.0.0-20201118090759-87929f5bae21 + github.com/cs3org/go-cs3apis v0.0.0-20210104105209-0d3ecb3453dc github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/eventials/go-tus v0.0.0-20200718001131-45c7ec8f5d59 github.com/go-ldap/ldap/v3 v3.2.4 diff --git a/go.sum b/go.sum index 06937dcab3..de431fa4f1 100644 --- a/go.sum +++ b/go.sum @@ -92,6 +92,10 @@ github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e h1:tqSPWQeueWTKnJVMJff github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e/go.mod h1:XJEZ3/EQuI3BXTp/6DUzFr850vlxq11I6satRtz0YQ4= github.com/cs3org/go-cs3apis v0.0.0-20201118090759-87929f5bae21 h1:mZpylrgnCgSeaZ5EznvHIPIKuaQHMHZDi2wkJtk4M8Y= github.com/cs3org/go-cs3apis v0.0.0-20201118090759-87929f5bae21/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= +github.com/cs3org/go-cs3apis v0.0.0-20201130144553-72667e613e44 h1:bvVU3EesXzpOJu+Sh6bffwCOaF7mW3KxRVaEmDpIw38= +github.com/cs3org/go-cs3apis v0.0.0-20201130144553-72667e613e44/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= +github.com/cs3org/go-cs3apis v0.0.0-20210104105209-0d3ecb3453dc h1:vHFqu+Gb/iOKYFy2KswpwIG3G6zRMudRn+rQ2bg3TPE= +github.com/cs3org/go-cs3apis v0.0.0-20210104105209-0d3ecb3453dc/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/grpc/services/gateway/gateway.go b/internal/grpc/services/gateway/gateway.go index cb6f77a55b..6e4d01fafe 100644 --- a/internal/grpc/services/gateway/gateway.go +++ b/internal/grpc/services/gateway/gateway.go @@ -52,6 +52,7 @@ type config struct { OCMProviderAuthorizerEndpoint string `mapstructure:"ocmproviderauthorizersvc"` OCMCoreEndpoint string `mapstructure:"ocmcoresvc"` UserProviderEndpoint string `mapstructure:"userprovidersvc"` + GroupProviderEndpoint string `mapstructure:"groupprovidersvc"` DataTxEndpoint string `mapstructure:"datatx"` DataGatewayEndpoint string `mapstructure:"datagateway"` CommitShareToStorageGrant bool `mapstructure:"commit_share_to_storage_grant"` @@ -92,6 +93,7 @@ func (c *config) init() { c.OCMProviderAuthorizerEndpoint = sharedconf.GetGatewaySVC(c.OCMProviderAuthorizerEndpoint) c.OCMCoreEndpoint = sharedconf.GetGatewaySVC(c.OCMCoreEndpoint) c.UserProviderEndpoint = sharedconf.GetGatewaySVC(c.UserProviderEndpoint) + c.GroupProviderEndpoint = sharedconf.GetGatewaySVC(c.GroupProviderEndpoint) c.DataTxEndpoint = sharedconf.GetGatewaySVC(c.DataTxEndpoint) c.DataGatewayEndpoint = sharedconf.GetDataGateway(c.DataGatewayEndpoint) diff --git a/internal/grpc/services/gateway/groupprovider.go b/internal/grpc/services/gateway/groupprovider.go new file mode 100644 index 0000000000..638837a71a --- /dev/null +++ b/internal/grpc/services/gateway/groupprovider.go @@ -0,0 +1,108 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package gateway + +import ( + "context" + + group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" + "github.com/cs3org/reva/pkg/rgrpc/status" + "github.com/cs3org/reva/pkg/rgrpc/todo/pool" + "github.com/pkg/errors" +) + +func (s *svc) GetGroup(ctx context.Context, req *group.GetGroupRequest) (*group.GetGroupResponse, error) { + c, err := pool.GetGroupProviderServiceClient(s.c.GroupProviderEndpoint) + if err != nil { + return &group.GetGroupResponse{ + Status: status.NewInternal(ctx, err, "error getting auth client"), + }, nil + } + + res, err := c.GetGroup(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling GetGroup") + } + + return res, nil +} + +func (s *svc) GetGroupByClaim(ctx context.Context, req *group.GetGroupByClaimRequest) (*group.GetGroupByClaimResponse, error) { + c, err := pool.GetGroupProviderServiceClient(s.c.GroupProviderEndpoint) + if err != nil { + return &group.GetGroupByClaimResponse{ + Status: status.NewInternal(ctx, err, "error getting auth client"), + }, nil + } + + res, err := c.GetGroupByClaim(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling GetGroupByClaim") + } + + return res, nil +} + +func (s *svc) FindGroups(ctx context.Context, req *group.FindGroupsRequest) (*group.FindGroupsResponse, error) { + c, err := pool.GetGroupProviderServiceClient(s.c.GroupProviderEndpoint) + if err != nil { + return &group.FindGroupsResponse{ + Status: status.NewInternal(ctx, err, "error getting auth client"), + }, nil + } + + res, err := c.FindGroups(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling FindGroups") + } + + return res, nil +} + +func (s *svc) GetMembers(ctx context.Context, req *group.GetMembersRequest) (*group.GetMembersResponse, error) { + c, err := pool.GetGroupProviderServiceClient(s.c.GroupProviderEndpoint) + if err != nil { + return &group.GetMembersResponse{ + Status: status.NewInternal(ctx, err, "error getting auth client"), + }, nil + } + + res, err := c.GetMembers(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling GetMembers") + } + + return res, nil +} + +func (s *svc) HasMember(ctx context.Context, req *group.HasMemberRequest) (*group.HasMemberResponse, error) { + c, err := pool.GetGroupProviderServiceClient(s.c.GroupProviderEndpoint) + if err != nil { + return &group.HasMemberResponse{ + Status: status.NewInternal(ctx, err, "error getting auth client"), + }, nil + } + + res, err := c.HasMember(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling HasMember") + } + + return res, nil +} diff --git a/internal/grpc/services/gateway/userprovider.go b/internal/grpc/services/gateway/userprovider.go index 1cac65b75e..f2ef2db92f 100644 --- a/internal/grpc/services/gateway/userprovider.go +++ b/internal/grpc/services/gateway/userprovider.go @@ -75,22 +75,6 @@ func (s *svc) FindUsers(ctx context.Context, req *user.FindUsersRequest) (*user. return res, nil } -func (s *svc) FindGroups(ctx context.Context, req *user.FindGroupsRequest) (*user.FindGroupsResponse, error) { - c, err := pool.GetUserProviderServiceClient(s.c.UserProviderEndpoint) - if err != nil { - return &user.FindGroupsResponse{ - Status: status.NewInternal(ctx, err, "error getting auth client"), - }, nil - } - - res, err := c.FindGroups(ctx, req) - if err != nil { - return nil, errors.Wrap(err, "gateway: error calling FindGroups") - } - - return res, nil -} - func (s *svc) GetUserGroups(ctx context.Context, req *user.GetUserGroupsRequest) (*user.GetUserGroupsResponse, error) { c, err := pool.GetUserProviderServiceClient(s.c.UserProviderEndpoint) if err != nil { @@ -106,19 +90,3 @@ func (s *svc) GetUserGroups(ctx context.Context, req *user.GetUserGroupsRequest) return res, nil } - -func (s *svc) IsInGroup(ctx context.Context, req *user.IsInGroupRequest) (*user.IsInGroupResponse, error) { - c, err := pool.GetUserProviderServiceClient(s.c.UserProviderEndpoint) - if err != nil { - return &user.IsInGroupResponse{ - Status: status.NewInternal(ctx, err, "error getting auth client"), - }, nil - } - - res, err := c.IsInGroup(ctx, req) - if err != nil { - return nil, errors.Wrap(err, "gateway: error calling IsInGroup") - } - - return res, nil -} diff --git a/internal/grpc/services/groupprovider/groupprovider.go b/internal/grpc/services/groupprovider/groupprovider.go new file mode 100644 index 0000000000..20e6f386fd --- /dev/null +++ b/internal/grpc/services/groupprovider/groupprovider.go @@ -0,0 +1,174 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package groupprovider + +import ( + "context" + "fmt" + + grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" + "github.com/cs3org/reva/pkg/group" + "github.com/cs3org/reva/pkg/group/manager/registry" + "github.com/cs3org/reva/pkg/rgrpc" + "github.com/cs3org/reva/pkg/rgrpc/status" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" + "google.golang.org/grpc" +) + +func init() { + rgrpc.Register("groupprovider", New) +} + +type config struct { + Driver string `mapstructure:"driver"` + Drivers map[string]map[string]interface{} `mapstructure:"drivers"` +} + +func (c *config) init() { + if c.Driver == "" { + c.Driver = "json" + } +} + +func parseConfig(m map[string]interface{}) (*config, error) { + c := &config{} + if err := mapstructure.Decode(m, c); err != nil { + err = errors.Wrap(err, "error decoding conf") + return nil, err + } + c.init() + return c, nil +} + +func getDriver(c *config) (group.Manager, error) { + if f, ok := registry.NewFuncs[c.Driver]; ok { + return f(c.Drivers[c.Driver]) + } + + return nil, fmt.Errorf("driver %s not found for group manager", c.Driver) +} + +// New returns a new GroupProviderServiceServer. +func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) { + c, err := parseConfig(m) + if err != nil { + return nil, err + } + + groupManager, err := getDriver(c) + if err != nil { + return nil, err + } + + svc := &service{groupmgr: groupManager} + + return svc, nil +} + +type service struct { + groupmgr group.Manager +} + +func (s *service) Close() error { + return nil +} + +func (s *service) UnprotectedEndpoints() []string { + return []string{} +} + +func (s *service) Register(ss *grpc.Server) { + grouppb.RegisterGroupAPIServer(ss, s) +} + +func (s *service) GetGroup(ctx context.Context, req *grouppb.GetGroupRequest) (*grouppb.GetGroupResponse, error) { + group, err := s.groupmgr.GetGroup(ctx, req.GroupId) + if err != nil { + err = errors.Wrap(err, "groupprovidersvc: error getting group") + return &grouppb.GetGroupResponse{ + Status: status.NewInternal(ctx, err, "error getting group"), + }, nil + } + + return &grouppb.GetGroupResponse{ + Status: status.NewOK(ctx), + Group: group, + }, nil +} + +func (s *service) GetGroupByClaim(ctx context.Context, req *grouppb.GetGroupByClaimRequest) (*grouppb.GetGroupByClaimResponse, error) { + group, err := s.groupmgr.GetGroupByClaim(ctx, req.Claim, req.Value) + if err != nil { + err = errors.Wrap(err, "groupprovidersvc: error getting group by claim") + return &grouppb.GetGroupByClaimResponse{ + Status: status.NewInternal(ctx, err, "error getting group by claim"), + }, nil + } + + return &grouppb.GetGroupByClaimResponse{ + Status: status.NewOK(ctx), + Group: group, + }, nil +} + +func (s *service) FindGroups(ctx context.Context, req *grouppb.FindGroupsRequest) (*grouppb.FindGroupsResponse, error) { + groups, err := s.groupmgr.FindGroups(ctx, req.Filter) + if err != nil { + err = errors.Wrap(err, "groupprovidersvc: error finding groups") + return &grouppb.FindGroupsResponse{ + Status: status.NewInternal(ctx, err, "error finding groups"), + }, nil + } + + return &grouppb.FindGroupsResponse{ + Status: status.NewOK(ctx), + Groups: groups, + }, nil +} + +func (s *service) GetMembers(ctx context.Context, req *grouppb.GetMembersRequest) (*grouppb.GetMembersResponse, error) { + members, err := s.groupmgr.GetMembers(ctx, req.GroupId) + if err != nil { + err = errors.Wrap(err, "groupprovidersvc: error getting group members") + return &grouppb.GetMembersResponse{ + Status: status.NewInternal(ctx, err, "error getting group members"), + }, nil + } + + return &grouppb.GetMembersResponse{ + Status: status.NewOK(ctx), + Members: members, + }, nil +} + +func (s *service) HasMember(ctx context.Context, req *grouppb.HasMemberRequest) (*grouppb.HasMemberResponse, error) { + ok, err := s.groupmgr.HasMember(ctx, req.GroupId, req.UserId) + if err != nil { + err = errors.Wrap(err, "groupprovidersvc: error checking for group member") + return &grouppb.HasMemberResponse{ + Status: status.NewInternal(ctx, err, "error checking for group member"), + }, nil + } + + return &grouppb.HasMemberResponse{ + Status: status.NewOK(ctx), + Ok: ok, + }, nil +} diff --git a/internal/grpc/services/loader/loader.go b/internal/grpc/services/loader/loader.go index 6e0f071115..4d4bcfe7be 100644 --- a/internal/grpc/services/loader/loader.go +++ b/internal/grpc/services/loader/loader.go @@ -26,6 +26,7 @@ import ( _ "github.com/cs3org/reva/internal/grpc/services/authregistry" _ "github.com/cs3org/reva/internal/grpc/services/datatx" _ "github.com/cs3org/reva/internal/grpc/services/gateway" + _ "github.com/cs3org/reva/internal/grpc/services/groupprovider" _ "github.com/cs3org/reva/internal/grpc/services/helloworld" _ "github.com/cs3org/reva/internal/grpc/services/ocmcore" _ "github.com/cs3org/reva/internal/grpc/services/ocminvitemanager" diff --git a/internal/grpc/services/userprovider/userprovider.go b/internal/grpc/services/userprovider/userprovider.go index 5ca4d5ab57..5012fef7a5 100644 --- a/internal/grpc/services/userprovider/userprovider.go +++ b/internal/grpc/services/userprovider/userprovider.go @@ -151,23 +151,6 @@ func (s *service) FindUsers(ctx context.Context, req *userpb.FindUsersRequest) ( return res, nil } -func (s *service) FindGroups(ctx context.Context, req *userpb.FindGroupsRequest) (*userpb.FindGroupsResponse, error) { - groups, err := s.usermgr.FindGroups(ctx, req.Filter) - if err != nil { - err = errors.Wrap(err, "userprovidersvc: error finding groups") - res := &userpb.FindGroupsResponse{ - Status: status.NewInternal(ctx, err, "error finding groups"), - } - return res, nil - } - - res := &userpb.FindGroupsResponse{ - Status: status.NewOK(ctx), - Groups: groups, - } - return res, nil -} - func (s *service) GetUserGroups(ctx context.Context, req *userpb.GetUserGroupsRequest) (*userpb.GetUserGroupsResponse, error) { groups, err := s.usermgr.GetUserGroups(ctx, req.UserId) if err != nil { @@ -184,21 +167,3 @@ func (s *service) GetUserGroups(ctx context.Context, req *userpb.GetUserGroupsRe } return res, nil } - -func (s *service) IsInGroup(ctx context.Context, req *userpb.IsInGroupRequest) (*userpb.IsInGroupResponse, error) { - ok, err := s.usermgr.IsInGroup(ctx, req.UserId, req.Group) - if err != nil { - err = errors.Wrap(err, "userprovidersvc: error checking if user belongs to group") - res := &userpb.IsInGroupResponse{ - Status: status.NewInternal(ctx, err, "error checking if user belongs to group"), - } - return res, nil - } - - res := &userpb.IsInGroupResponse{ - Status: status.NewOK(ctx), - Ok: ok, - } - - return res, nil -} diff --git a/pkg/cbox/user/rest/rest.go b/pkg/cbox/user/rest/rest.go index c022095ce1..822beed7ec 100644 --- a/pkg/cbox/user/rest/rest.go +++ b/pkg/cbox/user/rest/rest.go @@ -431,30 +431,6 @@ func (m *manager) FindUsers(ctx context.Context, query string) ([]*userpb.User, return userSlice, nil } -func (m *manager) FindGroups(ctx context.Context, query string) ([]string, error) { - url := fmt.Sprintf("%s/Group?filter=groupIdentifier:contains:%s", m.conf.APIBaseURL, query) - responseData, err := m.sendAPIRequest(ctx, url) - if err != nil { - return nil, err - } - - groupSet := make(map[string]bool) - for _, g := range responseData { - group, ok := g.(map[string]interface{}) - if !ok { - return nil, errors.New("rest: error in type assertion") - } - groupSet[group["groupIdentifier"].(string)] = true - } - - groups := []string{} - for k := range groupSet { - groups = append(groups, k) - } - - return groups, nil -} - func (m *manager) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]string, error) { groups, err := m.fetchCachedUserGroups(uid) diff --git a/pkg/group/group.go b/pkg/group/group.go new file mode 100644 index 0000000000..f27766cbb3 --- /dev/null +++ b/pkg/group/group.go @@ -0,0 +1,35 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package group + +import ( + "context" + + grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" +) + +// Manager is the interface to implement to manipulate groups. +type Manager interface { + GetGroup(ctx context.Context, gid *grouppb.GroupId) (*grouppb.Group, error) + GetGroupByClaim(ctx context.Context, claim, value string) (*grouppb.Group, error) + FindGroups(ctx context.Context, query string) ([]*grouppb.Group, error) + GetMembers(ctx context.Context, gid *grouppb.GroupId) ([]*userpb.UserId, error) + HasMember(ctx context.Context, gid *grouppb.GroupId, uid *userpb.UserId) (bool, error) +} diff --git a/pkg/group/manager/json/json.go b/pkg/group/manager/json/json.go new file mode 100644 index 0000000000..7b91a853a4 --- /dev/null +++ b/pkg/group/manager/json/json.go @@ -0,0 +1,157 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package json + +import ( + "context" + "encoding/json" + "io/ioutil" + "strconv" + "strings" + + grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/group" + "github.com/cs3org/reva/pkg/group/manager/registry" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" +) + +func init() { + registry.Register("json", New) +} + +type manager struct { + groups []*grouppb.Group +} + +type config struct { + // Groups holds a path to a file containing json conforming to the Groups struct + Groups string `mapstructure:"groups"` +} + +func (c *config) init() { + if c.Groups == "" { + c.Groups = "/etc/revad/groups.json" + } +} + +func parseConfig(m map[string]interface{}) (*config, error) { + c := &config{} + if err := mapstructure.Decode(m, c); err != nil { + err = errors.Wrap(err, "error decoding conf") + return nil, err + } + c.init() + return c, nil +} + +// New returns a group manager implementation that reads a json file to provide group metadata. +func New(m map[string]interface{}) (group.Manager, error) { + c, err := parseConfig(m) + if err != nil { + return nil, err + } + + f, err := ioutil.ReadFile(c.Groups) + if err != nil { + return nil, err + } + + groups := []*grouppb.Group{} + + err = json.Unmarshal(f, &groups) + if err != nil { + return nil, err + } + + return &manager{ + groups: groups, + }, nil +} + +func (m *manager) GetGroup(ctx context.Context, gid *grouppb.GroupId) (*grouppb.Group, error) { + for _, g := range m.groups { + if g.Id.GetOpaqueId() == gid.OpaqueId || g.GroupName == gid.OpaqueId { + return g, nil + } + } + return nil, errtypes.NotFound(gid.OpaqueId) +} + +func (m *manager) GetGroupByClaim(ctx context.Context, claim, value string) (*grouppb.Group, error) { + for _, g := range m.groups { + if groupClaim, err := extractClaim(g, claim); err == nil && value == groupClaim { + return g, nil + } + } + return nil, errtypes.NotFound(value) +} + +func extractClaim(g *grouppb.Group, claim string) (string, error) { + switch claim { + case "group_name": + return g.GroupName, nil + case "gid_number": + return strconv.FormatInt(g.GidNumber, 10), nil + case "display_name": + return g.DisplayName, nil + case "mail": + return g.Mail, nil + } + return "", errors.New("json: invalid field") +} + +func (m *manager) FindGroups(ctx context.Context, query string) ([]*grouppb.Group, error) { + groups := []*grouppb.Group{} + for _, g := range m.groups { + if groupContains(g, query) { + groups = append(groups, g) + } + } + return groups, nil +} + +func groupContains(g *grouppb.Group, query string) bool { + return strings.Contains(g.GroupName, query) || strings.Contains(g.DisplayName, query) || strings.Contains(g.Mail, query) || strings.Contains(g.Id.OpaqueId, query) +} + +func (m *manager) GetMembers(ctx context.Context, gid *grouppb.GroupId) ([]*userpb.UserId, error) { + for _, g := range m.groups { + if g.Id.GetOpaqueId() == gid.OpaqueId || g.GroupName == gid.OpaqueId { + return g.Members, nil + } + } + return nil, errtypes.NotFound(gid.OpaqueId) +} + +func (m *manager) HasMember(ctx context.Context, gid *grouppb.GroupId, uid *userpb.UserId) (bool, error) { + members, err := m.GetMembers(ctx, gid) + if err != nil { + return false, err + } + + for _, u := range members { + if u.OpaqueId == uid.OpaqueId && u.Idp == uid.Idp { + return true, nil + } + } + return false, nil +} diff --git a/pkg/group/manager/json/json_test.go b/pkg/group/manager/json/json_test.go new file mode 100644 index 0000000000..581fb4d8d2 --- /dev/null +++ b/pkg/group/manager/json/json_test.go @@ -0,0 +1,159 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package json + +import ( + "context" + "io/ioutil" + "os" + "reflect" + "testing" + + grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + "github.com/cs3org/reva/pkg/errtypes" +) + +var ctx = context.Background() + +func TestUserManager(t *testing.T) { + // add tempdir + tempdir, err := ioutil.TempDir("", "json_test") + if err != nil { + t.Fatalf("error while create temp dir: %v", err) + } + defer os.RemoveAll(tempdir) + + // corrupt json object with user meta data + userJSON := `[{` + + // get file handler for temporary file + file, err := ioutil.TempFile(tempdir, "json_test") + if err != nil { + t.Fatalf("error while open temp file: %v", err) + } + + // write json object to tempdir + _, err = file.WriteString(userJSON) + if err != nil { + t.Fatalf("error while writing temp file: %v", err) + } + + // get manager + input := map[string]interface{}{ + "groups": file.Name(), + } + _, err = New(input) + if err == nil { + t.Fatalf("no error (but we expected one) while get manager") + } + + // clean up + os.Remove(file.Name()) + + // json object with user meta data + userJSON = `[{"id":{"opaque_id":"sailing-lovers"},"group_name":"sailing-lovers","mail":"sailing-lovers@example.org","display_name":"Sailing Lovers","gid_number":1234,"members":[{"idp":"localhost","opaque_id":"einstein"},{"idp":"localhost","opaque_id":"marie"}]}]` + + // get file handler for temporary file + file, err = ioutil.TempFile(tempdir, "json_test") + if err != nil { + t.Fatalf("error while open temp file: %v", err) + } + defer os.Remove(file.Name()) + + // write json object to tempdir + _, err = file.WriteString(userJSON) + if err != nil { + t.Fatalf("error while writing temp file: %v", err) + } + + // get manager - positive test + input = map[string]interface{}{ + "groups": file.Name(), + } + manager, _ := New(input) + + // setup test data + gid := &grouppb.GroupId{OpaqueId: "sailing-lovers"} + uidEinstein := &userpb.UserId{Idp: "localhost", OpaqueId: "einstein"} + uidMarie := &userpb.UserId{Idp: "localhost", OpaqueId: "marie"} + members := []*userpb.UserId{uidEinstein, uidMarie} + group := &grouppb.Group{ + Id: gid, + GroupName: "sailing-lovers", + Mail: "sailing-lovers@example.org", + GidNumber: 1234, + DisplayName: "Sailing Lovers", + Members: members, + } + groupFake := &grouppb.GroupId{OpaqueId: "fake-group"} + + // positive test GetGroup + resGroup, _ := manager.GetGroup(ctx, gid) + if !reflect.DeepEqual(resGroup, group) { + t.Fatalf("group differs: expected=%v got=%v", group, resGroup) + } + + // negative test GetGroup + expectedErr := errtypes.NotFound(groupFake.OpaqueId) + _, err = manager.GetGroup(ctx, groupFake) + if !reflect.DeepEqual(err, expectedErr) { + t.Fatalf("group not found error differ: expected='%v' got='%v'", expectedErr, err) + } + + // positive test GetGroupByClaim by mail + resGroupByEmail, _ := manager.GetGroupByClaim(ctx, "mail", "sailing-lovers@example.org") + if !reflect.DeepEqual(resGroupByEmail, group) { + t.Fatalf("group differs: expected=%v got=%v", group, resGroupByEmail) + } + + // negative test GetGroupByClaim by mail + expectedErr = errtypes.NotFound("abc@example.com") + _, err = manager.GetGroupByClaim(ctx, "mail", "abc@example.com") + if !reflect.DeepEqual(err, expectedErr) { + t.Fatalf("group not found error differs: expected='%v' got='%v'", expectedErr, err) + } + + // test GetMembers + resMembers, _ := manager.GetMembers(ctx, gid) + if !reflect.DeepEqual(resMembers, members) { + t.Fatalf("members differ: expected=%v got=%v", members, resMembers) + } + + // positive test HasMember + resMember, _ := manager.HasMember(ctx, gid, uidMarie) + if resMember != true { + t.Fatalf("result differs: expected=%v got=%v", true, resMember) + } + + // negative test HasMember + resMemberNegative, _ := manager.HasMember(ctx, gid, &userpb.UserId{Idp: "localhost", OpaqueId: "fake-user"}) + if resMemberNegative != false { + t.Fatalf("result differs: expected=%v got=%v", false, resMemberNegative) + } + + // test FindGroups + resFind, _ := manager.FindGroups(ctx, "sail") + if len(resFind) != 1 { + t.Fatalf("too many groups found: expected=%d got=%d", 1, len(resFind)) + } + if !reflect.DeepEqual(resFind[0].GroupName, "sailing-lovers") { + t.Fatalf("group differ: expected=%v got=%v", "sailing-lovers", resFind[0].GroupName) + } +} diff --git a/pkg/group/manager/loader/loader.go b/pkg/group/manager/loader/loader.go new file mode 100644 index 0000000000..18c485ab76 --- /dev/null +++ b/pkg/group/manager/loader/loader.go @@ -0,0 +1,25 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package loader + +import ( + // Load core group manager drivers. + _ "github.com/cs3org/reva/pkg/group/manager/json" + // Add your own here +) diff --git a/pkg/group/manager/registry/registry.go b/pkg/group/manager/registry/registry.go new file mode 100644 index 0000000000..c3b77ad455 --- /dev/null +++ b/pkg/group/manager/registry/registry.go @@ -0,0 +1,34 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package registry + +import "github.com/cs3org/reva/pkg/group" + +// NewFunc is the function that group managers +// should register at init time. +type NewFunc func(map[string]interface{}) (group.Manager, error) + +// NewFuncs is a map containing all the registered group managers. +var NewFuncs = map[string]NewFunc{} + +// Register registers a new group manager new function. +// Not safe for concurrent use. Safe for use from package init. +func Register(name string, f NewFunc) { + NewFuncs[name] = f +} diff --git a/pkg/rgrpc/todo/pool/pool.go b/pkg/rgrpc/todo/pool/pool.go index 56d3764a42..1f7c47ddb7 100644 --- a/pkg/rgrpc/todo/pool/pool.go +++ b/pkg/rgrpc/todo/pool/pool.go @@ -26,6 +26,7 @@ import ( authprovider "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" authregistry "github.com/cs3org/go-cs3apis/cs3/auth/registry/v1beta1" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1" invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1" @@ -72,6 +73,7 @@ var ( storageRegistries = newProvider() gatewayProviders = newProvider() userProviders = newProvider() + groupProviders = newProvider() dataTxs = newProvider() ) @@ -126,6 +128,25 @@ func GetUserProviderServiceClient(endpoint string) (user.UserAPIClient, error) { return v, nil } +// GetGroupProviderServiceClient returns a GroupProviderServiceClient. +func GetGroupProviderServiceClient(endpoint string) (group.GroupAPIClient, error) { + groupProviders.m.Lock() + defer groupProviders.m.Unlock() + + if val, ok := groupProviders.conn[endpoint]; ok { + return val.(group.GroupAPIClient), nil + } + + conn, err := NewConn(endpoint) + if err != nil { + return nil, err + } + + v := group.NewGroupAPIClient(conn) + groupProviders.conn[endpoint] = v + return v, nil +} + // GetStorageProviderServiceClient returns a StorageProviderServiceClient. func GetStorageProviderServiceClient(endpoint string) (storageprovider.ProviderAPIClient, error) { storageProviders.m.Lock() diff --git a/pkg/user/manager/demo/demo.go b/pkg/user/manager/demo/demo.go index 9ef648e6e1..1fc1f6b812 100644 --- a/pkg/user/manager/demo/demo.go +++ b/pkg/user/manager/demo/demo.go @@ -95,23 +95,6 @@ func (m *manager) FindUsers(ctx context.Context, query string) ([]*userpb.User, return users, nil } -func (m *manager) FindGroups(ctx context.Context, query string) ([]string, error) { - groupSet := make(map[string]bool) - for _, u := range m.catalog { - for _, g := range u.Groups { - if strings.Contains(g, query) { - groupSet[g] = true - } - } - } - - groups := []string{} - for k := range groupSet { - groups = append(groups, k) - } - return groups, nil -} - func (m *manager) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]string, error) { user, err := m.GetUser(ctx, uid) if err != nil { @@ -120,20 +103,6 @@ func (m *manager) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]stri return user.Groups, nil } -func (m *manager) IsInGroup(ctx context.Context, uid *userpb.UserId, group string) (bool, error) { - user, err := m.GetUser(ctx, uid) - if err != nil { - return false, err - } - - for _, g := range user.Groups { - if group == g { - return true, nil - } - } - return false, nil -} - func getUsers() map[string]*userpb.User { return map[string]*userpb.User{ "4c510ada-c86b-4815-8820-42cdf82c3d51": &userpb.User{ diff --git a/pkg/user/manager/demo/demo_test.go b/pkg/user/manager/demo/demo_test.go index 82966dcc3c..e199378e36 100644 --- a/pkg/user/manager/demo/demo_test.go +++ b/pkg/user/manager/demo/demo_test.go @@ -95,35 +95,4 @@ func TestUserManager(t *testing.T) { if len(resUsers) > 0 { t.Fatalf("user not in group: expected=%v got=%v", []*userpb.User{}, resUsers) } - - // test FindGroups - resFindGroups, _ := manager.FindGroups(ctx, "violin") - if len(resFindGroups) != 1 { - t.Fatalf("too many groups found: expected=%d got=%+v", 1, resFindGroups) - } - if resFindGroups[0] != "violin-haters" { - t.Fatalf("group differs: expected=%v got=%v", "violin-haters", resFindGroups[0]) - } - - // positive test IsInGroup - resInGroup, _ := manager.IsInGroup(ctx, uidEinstein, "physics-lovers") - if !resInGroup { - t.Fatalf("user not in group: expected=%v got=%v", true, false) - } - - // negative test IsInGroup with wrong group - resInGroup, _ = manager.IsInGroup(ctx, uidEinstein, "notARealGroup") - if resInGroup { - t.Fatalf("user not in group: expected=%v got=%v", true, false) - } - - // negative test IsInGroup with wrong user - expectedErr = errtypes.NotFound(uidFake.OpaqueId) - resInGroup, err = manager.IsInGroup(ctx, uidFake, "physics-lovers") - if !reflect.DeepEqual(err, expectedErr) { - t.Fatalf("user not in group error differs: expected='%v' got='%v'", expectedErr, err) - } - if resInGroup { - t.Fatalf("user not in group bool differs: expected='%v' got='%v'", false, resInGroup) - } } diff --git a/pkg/user/manager/json/json.go b/pkg/user/manager/json/json.go index 5a972127aa..338ecf6ded 100644 --- a/pkg/user/manager/json/json.go +++ b/pkg/user/manager/json/json.go @@ -42,7 +42,7 @@ type manager struct { } type config struct { - // Users holds a path to a file containing json conforming the Users struct + // Users holds a path to a file containing json conforming to the Users struct Users string `mapstructure:"users"` } @@ -137,23 +137,6 @@ func (m *manager) FindUsers(ctx context.Context, query string) ([]*userpb.User, return users, nil } -func (m *manager) FindGroups(ctx context.Context, query string) ([]string, error) { - groupSet := make(map[string]bool) - for _, u := range m.users { - for _, g := range u.Groups { - if strings.Contains(g, query) { - groupSet[g] = true - } - } - } - - groups := []string{} - for k := range groupSet { - groups = append(groups, k) - } - return groups, nil -} - func (m *manager) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]string, error) { user, err := m.GetUser(ctx, uid) if err != nil { @@ -161,17 +144,3 @@ func (m *manager) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]stri } return user.Groups, nil } - -func (m *manager) IsInGroup(ctx context.Context, uid *userpb.UserId, group string) (bool, error) { - user, err := m.GetUser(ctx, uid) - if err != nil { - return false, err - } - - for _, g := range user.Groups { - if group == g { - return true, nil - } - } - return false, nil -} diff --git a/pkg/user/manager/json/json_test.go b/pkg/user/manager/json/json_test.go index 0c59592fea..9ec03cf9e2 100644 --- a/pkg/user/manager/json/json_test.go +++ b/pkg/user/manager/json/json_test.go @@ -134,35 +134,4 @@ func TestUserManager(t *testing.T) { if !reflect.DeepEqual(resUser[0].Username, "einstein") { t.Fatalf("user differ: expected=%v got=%v", "einstein", resUser[0].Username) } - - // test FindGroups - resFindGroups, _ := manager.FindGroups(ctx, "violin") - if len(resFindGroups) != 1 { - t.Fatalf("too many groups found: expected=%d got=%+v", 1, resFindGroups) - } - if resFindGroups[0] != "violin-haters" { - t.Fatalf("group differs: expected=%v got=%v", "violin-haters", resFindGroups[0]) - } - - // positive test IsInGroup - resInGroup, _ := manager.IsInGroup(ctx, uidEinstein, "physics-lovers") - if !resInGroup { - t.Fatalf("user not in group: expected=%v got=%v", true, false) - } - - // negative test IsInGroup with wrong group - resInGroup, _ = manager.IsInGroup(ctx, uidEinstein, "notARealGroup") - if resInGroup { - t.Fatalf("user not in group: expected=%v got=%v", true, false) - } - - // negative test IsInGroup with wrong user - expectedErr = errtypes.NotFound(userFake.OpaqueId) - resInGroup, err = manager.IsInGroup(ctx, userFake, "physics-lovers") - if !reflect.DeepEqual(err, expectedErr) { - t.Fatalf("user not in group error differ: expected='%v' got='%v'", expectedErr, err) - } - if resInGroup { - t.Fatalf("user not in group bool differ: expected='%v' got='%v'", false, resInGroup) - } } diff --git a/pkg/user/manager/ldap/ldap.go b/pkg/user/manager/ldap/ldap.go index 9e41e3d67d..7c5ec5ee10 100644 --- a/pkg/user/manager/ldap/ldap.go +++ b/pkg/user/manager/ldap/ldap.go @@ -344,10 +344,6 @@ func (m *manager) FindUsers(ctx context.Context, query string) ([]*userpb.User, return users, nil } -func (m *manager) FindGroups(ctx context.Context, query string) ([]string, error) { - return nil, errtypes.NotSupported("ldap: FindGroups is not supported") -} - func (m *manager) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]string, error) { l, err := ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", m.c.Hostname, m.c.Port), &tls.Config{InsecureSkipVerify: true}) if err != nil { @@ -387,22 +383,6 @@ func (m *manager) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]stri return groups, nil } -func (m *manager) IsInGroup(ctx context.Context, uid *userpb.UserId, group string) (bool, error) { - // TODO implement with dedicated ldap query - groups, err := m.GetUserGroups(ctx, uid) - if err != nil { - return false, err - } - - for _, g := range groups { - if g == group { - return true, nil - } - } - - return false, nil -} - func (m *manager) getUserFilter(uid *userpb.UserId) string { b := bytes.Buffer{} if err := m.userfilter.Execute(&b, uid); err != nil { diff --git a/pkg/user/user.go b/pkg/user/user.go index 5c592dff85..af26a64a1d 100644 --- a/pkg/user/user.go +++ b/pkg/user/user.go @@ -67,7 +67,5 @@ type Manager interface { GetUser(ctx context.Context, uid *userpb.UserId) (*userpb.User, error) GetUserByClaim(ctx context.Context, claim, value string) (*userpb.User, error) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]string, error) - IsInGroup(ctx context.Context, uid *userpb.UserId, group string) (bool, error) FindUsers(ctx context.Context, query string) ([]*userpb.User, error) - FindGroups(ctx context.Context, query string) ([]string, error) }