Skip to content

Commit

Permalink
in memory user provider (#2781)
Browse files Browse the repository at this point in the history
* in memory user and auth provider

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* add get by userid claim support to memory and demo

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* get rid of in memory auth provider

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* update changelog

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
  • Loading branch information
butonic authored Apr 27, 2022
1 parent 618964e commit bfdfc83
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 0 deletions.
5 changes: 5 additions & 0 deletions changelog/unreleased/user-provider-memory.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions pkg/user/manager/demo/demo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions pkg/user/manager/loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
183 changes: 183 additions & 0 deletions pkg/user/manager/memory/memory.go
Original file line number Diff line number Diff line change
@@ -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
}
124 changes: 124 additions & 0 deletions pkg/user/manager/memory/memory_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit bfdfc83

Please sign in to comment.