diff --git a/changelog/unreleased/user-provider-memory.md b/changelog/unreleased/user-provider-memory.md new file mode 100644 index 0000000000..6391fe9136 --- /dev/null +++ b/changelog/unreleased/user-provider-memory.md @@ -0,0 +1,5 @@ +Enhancement: in memory user provider + +We added an in memory implementation for the user provider that reads the users from the mapstructure passed in. + +https://github.com/cs3org/reva/pull/2781 diff --git a/pkg/user/manager/demo/demo.go b/pkg/user/manager/demo/demo.go index 5a429fa002..edadeeb970 100644 --- a/pkg/user/manager/demo/demo.go +++ b/pkg/user/manager/demo/demo.go @@ -86,6 +86,8 @@ func extractClaim(u *userpb.User, claim string) (string, error) { return u.Mail, nil case "username": return u.Username, nil + case "userid": + return u.Id.OpaqueId, nil case "uid": if u.UidNumber != 0 { return strconv.FormatInt(u.UidNumber, 10), nil diff --git a/pkg/user/manager/loader/loader.go b/pkg/user/manager/loader/loader.go index d14d04edc8..ec538b093b 100644 --- a/pkg/user/manager/loader/loader.go +++ b/pkg/user/manager/loader/loader.go @@ -23,6 +23,7 @@ import ( _ "github.com/cs3org/reva/v2/pkg/user/manager/demo" _ "github.com/cs3org/reva/v2/pkg/user/manager/json" _ "github.com/cs3org/reva/v2/pkg/user/manager/ldap" + _ "github.com/cs3org/reva/v2/pkg/user/manager/memory" _ "github.com/cs3org/reva/v2/pkg/user/manager/nextcloud" _ "github.com/cs3org/reva/v2/pkg/user/manager/owncloudsql" // Add your own here diff --git a/pkg/user/manager/memory/memory.go b/pkg/user/manager/memory/memory.go new file mode 100644 index 0000000000..6b9bffa5e1 --- /dev/null +++ b/pkg/user/manager/memory/memory.go @@ -0,0 +1,183 @@ +// Copyright 2018-2021 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 memory + +import ( + "context" + "strconv" + "strings" + + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/cs3org/reva/v2/pkg/errtypes" + "github.com/cs3org/reva/v2/pkg/user" + "github.com/cs3org/reva/v2/pkg/user/manager/registry" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" +) + +func init() { + registry.Register("memory", New) +} + +type config struct { + // Users holds a map with userid and user + Users map[string]*User `mapstructure:"users"` +} + +// User holds a user but uses _ in mapstructure names +type User struct { + ID *userpb.UserId `mapstructure:"id" json:"id"` + Username string `mapstructure:"username" json:"username"` + Mail string `mapstructure:"mail" json:"mail"` + MailVerified bool `mapstructure:"mail_verified" json:"mail_verified"` + DisplayName string `mapstructure:"display_name" json:"display_name"` + Groups []string `mapstructure:"groups" json:"groups"` + UIDNumber int64 `mapstructure:"uid_number" json:"uid_number"` + GIDNumber int64 `mapstructure:"gid_number" json:"gid_number"` + Opaque *typespb.Opaque `mapstructure:"opaque" json:"opaque"` +} + +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 + } + return c, nil +} + +type manager struct { + catalog map[string]*User +} + +// New returns a new user manager. +func New(m map[string]interface{}) (user.Manager, error) { + mgr := &manager{} + err := mgr.Configure(m) + return mgr, err +} + +func (m *manager) Configure(ml map[string]interface{}) error { + c, err := parseConfig(ml) + if err != nil { + return err + } + m.catalog = c.Users + return nil +} + +func (m *manager) GetUser(ctx context.Context, uid *userpb.UserId, skipFetchingGroups bool) (*userpb.User, error) { + if user, ok := m.catalog[uid.OpaqueId]; ok { + if uid.Idp == "" || user.ID.Idp == uid.Idp { + u := *user + if skipFetchingGroups { + u.Groups = nil + } + return &userpb.User{ + Id: u.ID, + Username: u.Username, + Mail: u.Mail, + DisplayName: u.DisplayName, + MailVerified: u.MailVerified, + Groups: u.Groups, + Opaque: u.Opaque, + UidNumber: u.UIDNumber, + GidNumber: u.GIDNumber, + }, nil + } + } + return nil, errtypes.NotFound(uid.OpaqueId) +} + +func (m *manager) GetUserByClaim(ctx context.Context, claim, value string, skipFetchingGroups bool) (*userpb.User, error) { + for _, u := range m.catalog { + if userClaim, err := extractClaim(u, claim); err == nil && value == userClaim { + user := &userpb.User{ + Id: u.ID, + Username: u.Username, + Mail: u.Mail, + DisplayName: u.DisplayName, + MailVerified: u.MailVerified, + Groups: u.Groups, + Opaque: u.Opaque, + UidNumber: u.UIDNumber, + GidNumber: u.GIDNumber, + } + if skipFetchingGroups { + user.Groups = nil + } + return user, nil + } + } + return nil, errtypes.NotFound(value) +} + +func extractClaim(u *User, claim string) (string, error) { + switch claim { + case "mail": + return u.Mail, nil + case "username": + return u.Username, nil + case "userid": + return u.ID.OpaqueId, nil + case "uid": + if u.UIDNumber != 0 { + return strconv.FormatInt(u.UIDNumber, 10), nil + } + } + return "", errors.New("memory: invalid field") +} + +// TODO(jfd) compare sub? +func userContains(u *User, query string) bool { + return strings.Contains(u.Username, query) || strings.Contains(u.DisplayName, query) || strings.Contains(u.Mail, query) || strings.Contains(u.ID.OpaqueId, query) +} + +func (m *manager) FindUsers(ctx context.Context, query string, skipFetchingGroups bool) ([]*userpb.User, error) { + users := []*userpb.User{} + for _, u := range m.catalog { + if userContains(u, query) { + user := &userpb.User{ + Id: u.ID, + Username: u.Username, + Mail: u.Mail, + DisplayName: u.DisplayName, + MailVerified: u.MailVerified, + Groups: u.Groups, + Opaque: u.Opaque, + UidNumber: u.UIDNumber, + GidNumber: u.GIDNumber, + } + if skipFetchingGroups { + user.Groups = nil + } + users = append(users, user) + } + } + return users, nil +} + +func (m *manager) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]string, error) { + user, err := m.GetUser(ctx, uid, false) + if err != nil { + return nil, err + } + return user.Groups, nil +} diff --git a/pkg/user/manager/memory/memory_test.go b/pkg/user/manager/memory/memory_test.go new file mode 100644 index 0000000000..ed1e0a4d5f --- /dev/null +++ b/pkg/user/manager/memory/memory_test.go @@ -0,0 +1,124 @@ +// Copyright 2018-2021 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 memory + +import ( + "context" + "reflect" + "testing" + + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + "github.com/cs3org/reva/v2/pkg/errtypes" +) + +var ctx = context.Background() + +func TestUserManager(t *testing.T) { + // get manager + manager, _ := New(map[string]interface{}{ + "users": map[string]interface{}{ + "4c510ada-c86b-4815-8820-42cdf82c3d51": map[string]interface{}{ + "id": map[string]interface{}{ + "opaqueId": "4c510ada-c86b-4815-8820-42cdf82c3d51", + "idp": "http://localhost:9998", + "type": 1, // user.UserType_USER_TYPE_PRIMARY + }, + "uid_number": 123, + "gid_number": 987, + "username": "einstein", + "mail": "einstein@example.org", + "display_name": "Albert Einstein", + "groups": []string{"sailing-lovers", "violin-haters", "physics-lovers"}, + }, + }, + }) + + // setup test data + uidEinstein := &userpb.UserId{Idp: "http://localhost:9998", OpaqueId: "4c510ada-c86b-4815-8820-42cdf82c3d51", Type: userpb.UserType_USER_TYPE_PRIMARY} + userEinstein := &userpb.User{ + Id: uidEinstein, + Username: "einstein", + Groups: []string{"sailing-lovers", "violin-haters", "physics-lovers"}, + Mail: "einstein@example.org", + DisplayName: "Albert Einstein", + UidNumber: 123, + GidNumber: 987, + } + userEinsteinWithoutGroups := &userpb.User{ + Id: uidEinstein, + Username: "einstein", + Mail: "einstein@example.org", + DisplayName: "Albert Einstein", + UidNumber: 123, + GidNumber: 987, + } + + uidFake := &userpb.UserId{Idp: "nonesense", OpaqueId: "fakeUser"} + groupsEinstein := []string{"sailing-lovers", "violin-haters", "physics-lovers"} + + // positive test GetUserByClaim by uid + resUserByUID, _ := manager.GetUserByClaim(ctx, "uid", "123", false) + if !reflect.DeepEqual(resUserByUID, userEinstein) { + t.Fatalf("user differs: expected=%v got=%v", userEinstein, resUserByUID) + } + + // negative test GetUserByClaim by uid + expectedErr := errtypes.NotFound("789") + _, err := manager.GetUserByClaim(ctx, "uid", "789", false) + if !reflect.DeepEqual(err, expectedErr) { + t.Fatalf("user not found error differs: expected='%v' got='%v'", expectedErr, err) + } + + // positive test GetUserByClaim by mail + resUserByEmail, _ := manager.GetUserByClaim(ctx, "mail", "einstein@example.org", false) + if !reflect.DeepEqual(resUserByEmail, userEinstein) { + t.Fatalf("user differs: expected=%v got=%v", userEinstein, resUserByEmail) + } + + // positive test GetUserByClaim by uid without groups + resUserByUIDWithoutGroups, _ := manager.GetUserByClaim(ctx, "uid", "123", true) + if !reflect.DeepEqual(resUserByUIDWithoutGroups, userEinsteinWithoutGroups) { + t.Fatalf("user differs: expected=%v got=%v", userEinsteinWithoutGroups, resUserByUIDWithoutGroups) + } + + // positive test GetUserGroups + resGroups, _ := manager.GetUserGroups(ctx, uidEinstein) + if !reflect.DeepEqual(resGroups, groupsEinstein) { + t.Fatalf("groups differ: expected=%v got=%v", groupsEinstein, resGroups) + } + + // negative test GetUserGroups + expectedErr = errtypes.NotFound(uidFake.OpaqueId) + _, err = manager.GetUserGroups(ctx, uidFake) + if !reflect.DeepEqual(err, expectedErr) { + t.Fatalf("user not found error differs: expected='%v' got='%v'", expectedErr, err) + } + + // test FindUsers + resUser, _ := manager.FindUsers(ctx, "einstein", false) + if !reflect.DeepEqual(resUser, []*userpb.User{userEinstein}) { + t.Fatalf("user differs: expected=%v got=%v", []*userpb.User{userEinstein}, resUser) + } + + // negative test FindUsers + resUsers, _ := manager.FindUsers(ctx, "notARealUser", false) + if len(resUsers) > 0 { + t.Fatalf("user not in group: expected=%v got=%v", []*userpb.User{}, resUsers) + } +}