mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-04-28 14:59:49 -05:00
Merge pull request #2804 from rhafer/graph-ldap
Add (read-only) LDAP backend to Graph users/groups endpoints
This commit is contained in:
@@ -25,6 +25,7 @@ require (
|
||||
github.com/go-chi/chi/v5 v5.0.7
|
||||
github.com/go-chi/cors v1.2.0
|
||||
github.com/go-chi/render v1.0.1
|
||||
github.com/go-ldap/ldap/v3 v3.4.1
|
||||
github.com/go-logr/logr v0.4.0
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
|
||||
github.com/gofrs/uuid v4.2.0+incompatible
|
||||
@@ -132,7 +133,6 @@ require (
|
||||
github.com/go-git/go-billy/v5 v5.3.1 // indirect
|
||||
github.com/go-git/go-git/v5 v5.4.2 // indirect
|
||||
github.com/go-kit/log v0.2.0 // indirect
|
||||
github.com/go-ldap/ldap/v3 v3.4.1 // indirect
|
||||
github.com/go-logfmt/logfmt v0.5.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
||||
|
||||
@@ -52,6 +52,31 @@ type Spaces struct {
|
||||
DefaultQuota string `ocisConfig:"default_quota"`
|
||||
}
|
||||
|
||||
type LDAP struct {
|
||||
URI string `ocisConfig:"uri"`
|
||||
BindDN string `ocisConfig:"bind_dn"`
|
||||
BindPassword string `ocisConfig:"bind_password"`
|
||||
|
||||
UserBaseDN string `ocisConfig:"user_base_dn"`
|
||||
UserSearchScope string `ocisConfig:"user_search_scope"`
|
||||
UserFilter string `ocisConfig:"user_filter"`
|
||||
UserEmailAttribute string `ocisConfig:"user_mail_attribute"`
|
||||
UserDisplayNameAttribute string `ocisConfig:"user_displayname_attribute"`
|
||||
UserNameAttribute string `ocisConfig:"user_name_attribute"`
|
||||
UserIDAttribute string `ocisConfig:"user_id_attribute"`
|
||||
|
||||
GroupBaseDN string `ocisConfig:"group_base_dn"`
|
||||
GroupSearchScope string `ocisConfig:"group_search_scope"`
|
||||
GroupFilter string `ocisConfig:"group_filter"`
|
||||
GroupNameAttribute string `ocisConfig:"group_name_attribute"`
|
||||
GroupIDAttribute string `ocisConfig:"group_id_attribute"`
|
||||
}
|
||||
|
||||
type Identity struct {
|
||||
Backend string `ocisConfig:"backend"`
|
||||
LDAP LDAP `ocisConfig:"ldap"`
|
||||
}
|
||||
|
||||
// Config combines all available configuration parts.
|
||||
type Config struct {
|
||||
*shared.Commons
|
||||
@@ -65,6 +90,7 @@ type Config struct {
|
||||
Reva Reva `ocisConfig:"reva"`
|
||||
TokenManager TokenManager `ocisConfig:"token_manager"`
|
||||
Spaces Spaces `ocisConfig:"spaces"`
|
||||
Identity Identity `ocisConfig:"identity"`
|
||||
|
||||
Context context.Context
|
||||
Supervised bool
|
||||
@@ -103,5 +129,27 @@ func DefaultConfig() *Config {
|
||||
WebDavPath: "/dav/spaces/",
|
||||
DefaultQuota: "1000000000",
|
||||
},
|
||||
Identity: Identity{
|
||||
Backend: "cs3",
|
||||
LDAP: LDAP{
|
||||
URI: "ldap://localhost:9125",
|
||||
BindDN: "",
|
||||
BindPassword: "",
|
||||
UserBaseDN: "ou=users,dc=ocis,dc=test",
|
||||
UserSearchScope: "sub",
|
||||
UserFilter: "(objectClass=posixaccount)",
|
||||
UserEmailAttribute: "mail",
|
||||
UserDisplayNameAttribute: "displayName",
|
||||
UserNameAttribute: "uid",
|
||||
// FIXME: switch this to some more widely available attribute by default
|
||||
// ideally this needs to be constant for the lifetime of a users
|
||||
UserIDAttribute: "ownclouduuid",
|
||||
GroupBaseDN: "ou=groups,dc=ocis,dc=test",
|
||||
GroupSearchScope: "sub",
|
||||
GroupFilter: "(objectclass=groupOfNames)",
|
||||
GroupNameAttribute: "cn",
|
||||
GroupIDAttribute: "cn",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,5 +111,69 @@ func structMappings(cfg *Config) []shared.EnvBinding {
|
||||
EnvVars: []string{"REVA_GATEWAY"},
|
||||
Destination: &cfg.Reva.Address,
|
||||
},
|
||||
{
|
||||
EnvVars: []string{"GRAPH_IDENTITY_BACKEND"},
|
||||
Destination: &cfg.Identity.Backend,
|
||||
},
|
||||
{
|
||||
EnvVars: []string{"GRAPH_LDAP_URI"},
|
||||
Destination: &cfg.Identity.LDAP.URI,
|
||||
},
|
||||
{
|
||||
EnvVars: []string{"GRAPH_LDAP_BIND_DN"},
|
||||
Destination: &cfg.Identity.LDAP.BindDN,
|
||||
},
|
||||
{
|
||||
EnvVars: []string{"GRAPH_LDAP_BIND_PASSWORD"},
|
||||
Destination: &cfg.Identity.LDAP.BindPassword,
|
||||
},
|
||||
{
|
||||
EnvVars: []string{"GRAPH_LDAP_USER_BASE_DN"},
|
||||
Destination: &cfg.Identity.LDAP.UserBaseDN,
|
||||
},
|
||||
{
|
||||
EnvVars: []string{"GRAPH_LDAP_USER_EMAIL_ATTRIBUTE"},
|
||||
Destination: &cfg.Identity.LDAP.UserEmailAttribute,
|
||||
},
|
||||
{
|
||||
EnvVars: []string{"GRAPH_LDAP_USER_DISPLAYNAME_ATTRIBUTE"},
|
||||
Destination: &cfg.Identity.LDAP.UserDisplayNameAttribute,
|
||||
},
|
||||
{
|
||||
EnvVars: []string{"GRAPH_LDAP_USER_NAME_ATTRIBUTE"},
|
||||
Destination: &cfg.Identity.LDAP.UserNameAttribute,
|
||||
},
|
||||
{
|
||||
EnvVars: []string{"GRAPH_LDAP_USER_UID_ATTRIBUTE"},
|
||||
Destination: &cfg.Identity.LDAP.UserIDAttribute,
|
||||
},
|
||||
{
|
||||
EnvVars: []string{"GRAPH_LDAP_USER_FILTER"},
|
||||
Destination: &cfg.Identity.LDAP.UserFilter,
|
||||
},
|
||||
{
|
||||
EnvVars: []string{"GRAPH_LDAP_USER_SCOPE"},
|
||||
Destination: &cfg.Identity.LDAP.UserSearchScope,
|
||||
},
|
||||
{
|
||||
EnvVars: []string{"GRAPH_LDAP_GROUP_BASE_DN"},
|
||||
Destination: &cfg.Identity.LDAP.GroupBaseDN,
|
||||
},
|
||||
{
|
||||
EnvVars: []string{"GRAPH_LDAP_GROUP_SEARCH_SCOPE"},
|
||||
Destination: &cfg.Identity.LDAP.GroupSearchScope,
|
||||
},
|
||||
{
|
||||
EnvVars: []string{"GRAPH_LDAP_GROUP_FILTER"},
|
||||
Destination: &cfg.Identity.LDAP.GroupFilter,
|
||||
},
|
||||
{
|
||||
EnvVars: []string{"GRAPH_LDAP_GROUP_NAME_ATTRIBUTE"},
|
||||
Destination: &cfg.Identity.LDAP.GroupNameAttribute,
|
||||
},
|
||||
{
|
||||
EnvVars: []string{"GRAPH_LDAP_GROUP_ID_ATTRIBUTE"},
|
||||
Destination: &cfg.Identity.LDAP.GroupIDAttribute,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
msgraph "github.com/yaegashi/msgraph.go/beta"
|
||||
)
|
||||
|
||||
type Backend interface {
|
||||
GetUser(ctx context.Context, nameOrId string) (*msgraph.User, error)
|
||||
GetUsers(ctx context.Context, queryParam url.Values) ([]*msgraph.User, error)
|
||||
|
||||
GetGroup(ctx context.Context, nameOrId string) (*msgraph.Group, error)
|
||||
GetGroups(ctx context.Context, queryParam url.Values) ([]*msgraph.Group, error)
|
||||
}
|
||||
|
||||
func CreateUserModelFromCS3(u *cs3.User) *msgraph.User {
|
||||
if u.Id == nil {
|
||||
u.Id = &cs3.UserId{}
|
||||
}
|
||||
return &msgraph.User{
|
||||
DisplayName: &u.DisplayName,
|
||||
Mail: &u.Mail,
|
||||
// TODO u.Groups are those ids or group names?
|
||||
OnPremisesSamAccountName: &u.Username,
|
||||
DirectoryObject: msgraph.DirectoryObject{
|
||||
Entity: msgraph.Entity{
|
||||
ID: &u.Id.OpaqueId,
|
||||
Object: msgraph.Object{
|
||||
AdditionalData: map[string]interface{}{
|
||||
"uidnumber": u.UidNumber,
|
||||
"gidnumber": u.GidNumber,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
cs3group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
|
||||
cs3user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
"github.com/cs3org/reva/pkg/rgrpc/todo/pool"
|
||||
msgraph "github.com/yaegashi/msgraph.go/beta"
|
||||
|
||||
"github.com/owncloud/ocis/graph/pkg/config"
|
||||
"github.com/owncloud/ocis/graph/pkg/service/v0/errorcode"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
)
|
||||
|
||||
type CS3 struct {
|
||||
Config *config.Reva
|
||||
Logger *log.Logger
|
||||
}
|
||||
|
||||
func (i *CS3) GetUser(ctx context.Context, userID string) (*msgraph.User, error) {
|
||||
client, err := pool.GetGatewayServiceClient(i.Config.Address)
|
||||
if err != nil {
|
||||
i.Logger.Error().Err(err).Msg("could not get client")
|
||||
return nil, errorcode.New(errorcode.ServiceNotAvailable, err.Error())
|
||||
}
|
||||
|
||||
res, err := client.GetUserByClaim(ctx, &cs3user.GetUserByClaimRequest{
|
||||
Claim: "userid", // FIXME add consts to reva
|
||||
Value: userID,
|
||||
})
|
||||
|
||||
switch {
|
||||
case err != nil:
|
||||
i.Logger.Error().Err(err).Str("userid", userID).Msg("error sending get user by claim id grpc request")
|
||||
return nil, errorcode.New(errorcode.ServiceNotAvailable, err.Error())
|
||||
case res.Status.Code != cs3rpc.Code_CODE_OK:
|
||||
if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
|
||||
return nil, errorcode.New(errorcode.ItemNotFound, res.Status.Message)
|
||||
}
|
||||
i.Logger.Error().Err(err).Str("userid", userID).Msg("error sending get user by claim id grpc request")
|
||||
return nil, errorcode.New(errorcode.GeneralException, res.Status.Message)
|
||||
}
|
||||
return CreateUserModelFromCS3(res.User), nil
|
||||
}
|
||||
|
||||
func (i *CS3) GetUsers(ctx context.Context, queryParam url.Values) ([]*msgraph.User, error) {
|
||||
client, err := pool.GetGatewayServiceClient(i.Config.Address)
|
||||
if err != nil {
|
||||
i.Logger.Error().Err(err).Msg("could not get client")
|
||||
return nil, errorcode.New(errorcode.ServiceNotAvailable, err.Error())
|
||||
}
|
||||
|
||||
search := queryParam.Get("search")
|
||||
if search == "" {
|
||||
search = queryParam.Get("$search")
|
||||
}
|
||||
|
||||
res, err := client.FindUsers(ctx, &cs3user.FindUsersRequest{
|
||||
// FIXME presence match is currently not implemented, an empty search currently leads to
|
||||
// Unwilling To Perform": Search Error: error parsing filter: (&(objectclass=posixAccount)(|(cn=*)(displayname=*)(mail=*))), error: Present filter match for cn not implemented
|
||||
Filter: search,
|
||||
})
|
||||
switch {
|
||||
case err != nil:
|
||||
i.Logger.Error().Err(err).Str("search", search).Msg("error sending find users grpc request")
|
||||
return nil, errorcode.New(errorcode.ServiceNotAvailable, err.Error())
|
||||
case res.Status.Code != cs3rpc.Code_CODE_OK:
|
||||
if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
|
||||
return nil, errorcode.New(errorcode.ItemNotFound, res.Status.Message)
|
||||
}
|
||||
i.Logger.Error().Err(err).Str("search", search).Msg("error sending find users grpc request")
|
||||
return nil, errorcode.New(errorcode.GeneralException, res.Status.Message)
|
||||
}
|
||||
|
||||
users := make([]*msgraph.User, 0, len(res.Users))
|
||||
|
||||
for _, user := range res.Users {
|
||||
users = append(users, CreateUserModelFromCS3(user))
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (i *CS3) GetGroups(ctx context.Context, queryParam url.Values) ([]*msgraph.Group, error) {
|
||||
client, err := pool.GetGatewayServiceClient(i.Config.Address)
|
||||
if err != nil {
|
||||
i.Logger.Error().Err(err).Msg("could not get client")
|
||||
return nil, errorcode.New(errorcode.ServiceNotAvailable, err.Error())
|
||||
}
|
||||
|
||||
search := queryParam.Get("search")
|
||||
if search == "" {
|
||||
search = queryParam.Get("$search")
|
||||
}
|
||||
|
||||
res, err := client.FindGroups(ctx, &cs3group.FindGroupsRequest{
|
||||
// FIXME presence match is currently not implemented, an empty search currently leads to
|
||||
// Unwilling To Perform": Search Error: error parsing filter: (&(objectclass=posixAccount)(|(cn=*)(displayname=*)(mail=*))), error: Present filter match for cn not implemented
|
||||
Filter: search,
|
||||
})
|
||||
|
||||
switch {
|
||||
case err != nil:
|
||||
i.Logger.Error().Err(err).Str("search", search).Msg("error sending find groups grpc request")
|
||||
return nil, errorcode.New(errorcode.ServiceNotAvailable, err.Error())
|
||||
case res.Status.Code != cs3rpc.Code_CODE_OK:
|
||||
if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
|
||||
return nil, errorcode.New(errorcode.ItemNotFound, res.Status.Message)
|
||||
}
|
||||
i.Logger.Error().Err(err).Str("search", search).Msg("error sending find groups grpc request")
|
||||
return nil, errorcode.New(errorcode.GeneralException, res.Status.Message)
|
||||
}
|
||||
|
||||
groups := make([]*msgraph.Group, 0, len(res.Groups))
|
||||
|
||||
for _, group := range res.Groups {
|
||||
groups = append(groups, createGroupModelFromCS3(group))
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (i *CS3) GetGroup(ctx context.Context, groupID string) (*msgraph.Group, error) {
|
||||
client, err := pool.GetGatewayServiceClient(i.Config.Address)
|
||||
if err != nil {
|
||||
i.Logger.Error().Err(err).Msg("could not get client")
|
||||
return nil, errorcode.New(errorcode.ServiceNotAvailable, err.Error())
|
||||
}
|
||||
|
||||
res, err := client.GetGroupByClaim(ctx, &cs3group.GetGroupByClaimRequest{
|
||||
Claim: "groupid", // FIXME add consts to reva
|
||||
Value: groupID,
|
||||
})
|
||||
|
||||
switch {
|
||||
case err != nil:
|
||||
i.Logger.Error().Err(err).Str("groupid", groupID).Msg("error sending get group by claim id grpc request")
|
||||
return nil, errorcode.New(errorcode.ServiceNotAvailable, err.Error())
|
||||
case res.Status.Code != cs3rpc.Code_CODE_OK:
|
||||
if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
|
||||
return nil, errorcode.New(errorcode.ItemNotFound, res.Status.Message)
|
||||
}
|
||||
i.Logger.Error().Err(err).Str("groupid", groupID).Msg("error sending get group by claim id grpc request")
|
||||
return nil, errorcode.New(errorcode.GeneralException, res.Status.Message)
|
||||
}
|
||||
|
||||
return createGroupModelFromCS3(res.Group), nil
|
||||
}
|
||||
|
||||
func createGroupModelFromCS3(g *cs3group.Group) *msgraph.Group {
|
||||
if g.Id == nil {
|
||||
g.Id = &cs3group.GroupId{}
|
||||
}
|
||||
return &msgraph.Group{
|
||||
DirectoryObject: msgraph.DirectoryObject{
|
||||
Entity: msgraph.Entity{
|
||||
ID: &g.Id.OpaqueId,
|
||||
},
|
||||
},
|
||||
OnPremisesDomainName: &g.Id.Idp,
|
||||
OnPremisesSamAccountName: &g.GroupName,
|
||||
DisplayName: &g.DisplayName,
|
||||
Mail: &g.Mail,
|
||||
// TODO when to fetch and expand memberof, usernames or ids?
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
msgraph "github.com/yaegashi/msgraph.go/beta"
|
||||
|
||||
"github.com/owncloud/ocis/graph/pkg/config"
|
||||
"github.com/owncloud/ocis/graph/pkg/service/v0/errorcode"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
)
|
||||
|
||||
type LDAP struct {
|
||||
userBaseDN string
|
||||
userFilter string
|
||||
userScope int
|
||||
userAttributeMap userAttributeMap
|
||||
|
||||
groupBaseDN string
|
||||
groupFilter string
|
||||
groupScope int
|
||||
groupAttributeMap groupAttributeMap
|
||||
|
||||
logger *log.Logger
|
||||
conn ldap.Client
|
||||
}
|
||||
|
||||
type userAttributeMap struct {
|
||||
displayName string
|
||||
id string
|
||||
mail string
|
||||
userName string
|
||||
}
|
||||
|
||||
type groupAttributeMap struct {
|
||||
name string
|
||||
id string
|
||||
}
|
||||
|
||||
func NewLDAPBackend(lc ldap.Client, config config.LDAP, logger *log.Logger) (*LDAP, error) {
|
||||
if config.UserDisplayNameAttribute == "" || config.UserIDAttribute == "" ||
|
||||
config.UserEmailAttribute == "" || config.UserNameAttribute == "" {
|
||||
return nil, fmt.Errorf("Invalid user attribute mappings")
|
||||
}
|
||||
uam := userAttributeMap{
|
||||
displayName: config.UserDisplayNameAttribute,
|
||||
id: config.UserIDAttribute,
|
||||
mail: config.UserEmailAttribute,
|
||||
userName: config.UserNameAttribute,
|
||||
}
|
||||
|
||||
if config.GroupNameAttribute == "" || config.GroupIDAttribute == "" {
|
||||
return nil, fmt.Errorf("Invalid group attribute mappings")
|
||||
}
|
||||
gam := groupAttributeMap{
|
||||
name: config.GroupNameAttribute,
|
||||
id: config.GroupIDAttribute,
|
||||
}
|
||||
|
||||
var userScope, groupScope int
|
||||
var err error
|
||||
if userScope, err = stringToScope(config.UserSearchScope); err != nil {
|
||||
return nil, fmt.Errorf("Error configuring user scope: %w", err)
|
||||
}
|
||||
|
||||
if groupScope, err = stringToScope(config.GroupSearchScope); err != nil {
|
||||
return nil, fmt.Errorf("Error configuring group scope: %w", err)
|
||||
}
|
||||
|
||||
return &LDAP{
|
||||
userBaseDN: config.UserBaseDN,
|
||||
userFilter: config.UserFilter,
|
||||
userScope: userScope,
|
||||
userAttributeMap: uam,
|
||||
groupBaseDN: config.GroupBaseDN,
|
||||
groupFilter: config.GroupFilter,
|
||||
groupScope: groupScope,
|
||||
groupAttributeMap: gam,
|
||||
logger: logger,
|
||||
conn: lc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (i *LDAP) GetUser(ctx context.Context, userID string) (*msgraph.User, error) {
|
||||
i.logger.Debug().Str("backend", "ldap").Msg("GetUser")
|
||||
userID = ldap.EscapeFilter(userID)
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
i.userBaseDN, i.userScope, ldap.NeverDerefAliases, 1, 0, false,
|
||||
fmt.Sprintf("(&%s(|(%s=%s)(%s=%s)))", i.userFilter, i.userAttributeMap.userName, userID, i.userAttributeMap.id, userID),
|
||||
[]string{
|
||||
i.userAttributeMap.displayName,
|
||||
i.userAttributeMap.id,
|
||||
i.userAttributeMap.mail,
|
||||
i.userAttributeMap.userName,
|
||||
},
|
||||
nil,
|
||||
)
|
||||
i.logger.Debug().Str("backend", "ldap").Msgf("Search %s", i.userBaseDN)
|
||||
res, err := i.conn.Search(searchRequest)
|
||||
|
||||
if err != nil {
|
||||
var errmsg string
|
||||
if lerr, ok := err.(*ldap.Error); ok {
|
||||
if lerr.ResultCode == ldap.LDAPResultSizeLimitExceeded {
|
||||
errmsg = fmt.Sprintf("too many results searching for user '%s'", userID)
|
||||
i.logger.Debug().Str("backend", "ldap").Err(lerr).Msg(errmsg)
|
||||
}
|
||||
}
|
||||
return nil, errorcode.New(errorcode.ItemNotFound, errmsg)
|
||||
}
|
||||
if len(res.Entries) == 0 {
|
||||
return nil, errorcode.New(errorcode.ItemNotFound, "not found")
|
||||
}
|
||||
|
||||
return i.createUserModelFromLDAP(res.Entries[0]), nil
|
||||
}
|
||||
|
||||
func (i *LDAP) GetUsers(ctx context.Context, queryParam url.Values) ([]*msgraph.User, error) {
|
||||
i.logger.Debug().Str("backend", "ldap").Msg("GetUsers")
|
||||
|
||||
search := queryParam.Get("search")
|
||||
if search == "" {
|
||||
search = queryParam.Get("$search")
|
||||
}
|
||||
userFilter := i.userFilter
|
||||
if search != "" {
|
||||
search = ldap.EscapeFilter(search)
|
||||
userFilter = fmt.Sprintf(
|
||||
"(&(%s)(|(%s=%s*)(%s=%s*)(%s=%s*)))",
|
||||
userFilter,
|
||||
i.userAttributeMap.userName, search,
|
||||
i.userAttributeMap.mail, search,
|
||||
i.userAttributeMap.displayName, search,
|
||||
)
|
||||
}
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
i.userBaseDN, i.userScope, ldap.NeverDerefAliases, 0, 0, false,
|
||||
userFilter,
|
||||
[]string{
|
||||
i.userAttributeMap.displayName,
|
||||
i.userAttributeMap.id,
|
||||
i.userAttributeMap.mail,
|
||||
i.userAttributeMap.userName,
|
||||
},
|
||||
nil,
|
||||
)
|
||||
i.logger.Debug().Str("backend", "ldap").Msgf("Search %s", i.userBaseDN)
|
||||
res, err := i.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
return nil, errorcode.New(errorcode.ItemNotFound, err.Error())
|
||||
}
|
||||
|
||||
users := make([]*msgraph.User, 0, len(res.Entries))
|
||||
|
||||
for _, e := range res.Entries {
|
||||
users = append(users, i.createUserModelFromLDAP(e))
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (i *LDAP) GetGroup(ctx context.Context, groupID string) (*msgraph.Group, error) {
|
||||
i.logger.Debug().Str("backend", "ldap").Msg("GetGroup")
|
||||
groupID = ldap.EscapeFilter(groupID)
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
i.groupBaseDN, i.groupScope, ldap.NeverDerefAliases, 1, 0, false,
|
||||
fmt.Sprintf("(&%s(|(%s=%s)(%s=%s)))", i.groupFilter, i.groupAttributeMap.name, groupID, i.groupAttributeMap.id, groupID),
|
||||
[]string{
|
||||
i.groupAttributeMap.name,
|
||||
i.groupAttributeMap.id,
|
||||
},
|
||||
nil,
|
||||
)
|
||||
i.logger.Debug().Str("backend", "ldap").Msgf("Search %s", i.groupBaseDN)
|
||||
res, err := i.conn.Search(searchRequest)
|
||||
|
||||
if err != nil {
|
||||
var errmsg string
|
||||
if lerr, ok := err.(*ldap.Error); ok {
|
||||
if lerr.ResultCode == ldap.LDAPResultSizeLimitExceeded {
|
||||
errmsg = fmt.Sprintf("too many results searching for group '%s'", groupID)
|
||||
i.logger.Debug().Str("backend", "ldap").Err(lerr).Msg(errmsg)
|
||||
}
|
||||
}
|
||||
return nil, errorcode.New(errorcode.ItemNotFound, errmsg)
|
||||
}
|
||||
if len(res.Entries) == 0 {
|
||||
return nil, errorcode.New(errorcode.ItemNotFound, "not found")
|
||||
}
|
||||
|
||||
return i.createGroupModelFromLDAP(res.Entries[0]), nil
|
||||
}
|
||||
|
||||
func (i *LDAP) GetGroups(ctx context.Context, queryParam url.Values) ([]*msgraph.Group, error) {
|
||||
i.logger.Debug().Str("backend", "ldap").Msg("GetGroups")
|
||||
|
||||
search := queryParam.Get("search")
|
||||
if search == "" {
|
||||
search = queryParam.Get("$search")
|
||||
}
|
||||
groupFilter := i.groupFilter
|
||||
if search != "" {
|
||||
search = ldap.EscapeFilter(search)
|
||||
groupFilter = fmt.Sprintf(
|
||||
"(&(%s)(|(%s=%s*)(%s=%s*)))",
|
||||
groupFilter,
|
||||
i.groupAttributeMap.name, search,
|
||||
i.groupAttributeMap.id, search,
|
||||
)
|
||||
}
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
i.groupBaseDN, i.groupScope, ldap.NeverDerefAliases, 0, 0, false,
|
||||
groupFilter,
|
||||
[]string{
|
||||
i.groupAttributeMap.name,
|
||||
i.groupAttributeMap.id,
|
||||
},
|
||||
nil,
|
||||
)
|
||||
i.logger.Debug().Str("backend", "ldap").Str("Base", i.groupBaseDN).Str("filter", groupFilter).Msg("ldap search")
|
||||
res, err := i.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
return nil, errorcode.New(errorcode.ItemNotFound, err.Error())
|
||||
}
|
||||
|
||||
groups := make([]*msgraph.Group, 0, len(res.Entries))
|
||||
|
||||
for _, e := range res.Entries {
|
||||
groups = append(groups, i.createGroupModelFromLDAP(e))
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (i *LDAP) createUserModelFromLDAP(e *ldap.Entry) *msgraph.User {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return &msgraph.User{
|
||||
DisplayName: pointerOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.displayName)),
|
||||
Mail: pointerOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.mail)),
|
||||
OnPremisesSamAccountName: pointerOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.userName)),
|
||||
DirectoryObject: msgraph.DirectoryObject{
|
||||
Entity: msgraph.Entity{
|
||||
ID: pointerOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.id)),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (i *LDAP) createGroupModelFromLDAP(e *ldap.Entry) *msgraph.Group {
|
||||
return &msgraph.Group{
|
||||
DisplayName: pointerOrNil(e.GetEqualFoldAttributeValue(i.groupAttributeMap.name)),
|
||||
OnPremisesSamAccountName: pointerOrNil(e.GetEqualFoldAttributeValue(i.groupAttributeMap.name)),
|
||||
DirectoryObject: msgraph.DirectoryObject{
|
||||
Entity: msgraph.Entity{
|
||||
ID: pointerOrNil(e.GetEqualFoldAttributeValue(i.groupAttributeMap.id)),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
func pointerOrNil(val string) *string {
|
||||
if val == "" {
|
||||
return nil
|
||||
}
|
||||
return &val
|
||||
}
|
||||
|
||||
func stringToScope(scope string) (int, error) {
|
||||
var s int
|
||||
switch scope {
|
||||
case "sub":
|
||||
s = ldap.ScopeWholeSubtree
|
||||
case "one":
|
||||
s = ldap.ScopeSingleLevel
|
||||
case "base":
|
||||
s = ldap.ScopeBaseObject
|
||||
default:
|
||||
return 0, fmt.Errorf("Invalid Scope '%s'", scope)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package ldap
|
||||
|
||||
// LDAP automatic reconnection mechanism, inspired by:
|
||||
// https://gist.github.com/emsearcy/cba3295d1a06d4c432ab4f6173b65e4f#file-ldap_snippet-go
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
)
|
||||
|
||||
type ldapConnection struct {
|
||||
Conn *ldap.Conn
|
||||
Error error
|
||||
}
|
||||
|
||||
// Implements the ldap.CLient interface
|
||||
type ConnWithReconnect struct {
|
||||
conn chan ldapConnection
|
||||
reset chan *ldap.Conn
|
||||
retries int
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
func NewLDAPWithReconnect(logger *log.Logger, ldapURI, bindDN, bindPassword string) ConnWithReconnect {
|
||||
conn := ConnWithReconnect{
|
||||
conn: make(chan ldapConnection),
|
||||
reset: make(chan *ldap.Conn),
|
||||
retries: 1,
|
||||
logger: logger,
|
||||
}
|
||||
go conn.ldapAutoConnect(ldapURI, bindDN, bindPassword)
|
||||
return conn
|
||||
}
|
||||
|
||||
func (c ConnWithReconnect) Search(sr *ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||
conn, err := c.GetConnection()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res *ldap.SearchResult
|
||||
for try := 0; try <= c.retries; try++ {
|
||||
res, err = conn.Search(sr)
|
||||
if !ldap.IsErrorWithCode(err, ldap.ErrorNetwork) {
|
||||
// non network error, return it to the client
|
||||
return res, err
|
||||
}
|
||||
|
||||
c.logger.Debug().Msgf("Network Error. attempt %d", try)
|
||||
conn, err = c.reconnect(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.logger.Debug().Msg("retrying LDAP Search")
|
||||
}
|
||||
// if we get here we reached the maximum retries. So return an error
|
||||
return nil, ldap.NewError(ldap.ErrorNetwork, errors.New("max retries"))
|
||||
}
|
||||
|
||||
func (c ConnWithReconnect) GetConnection() (*ldap.Conn, error) {
|
||||
conn := <-c.conn
|
||||
if conn.Conn != nil && !ldap.IsErrorWithCode(conn.Error, ldap.ErrorNetwork) {
|
||||
c.logger.Debug().Msg("using existing Connection")
|
||||
return conn.Conn, conn.Error
|
||||
}
|
||||
return c.reconnect(conn.Conn)
|
||||
}
|
||||
|
||||
func (c ConnWithReconnect) ldapAutoConnect(ldapURI, bindDN, bindPassword string) {
|
||||
l, err := c.ldapConnect(ldapURI, bindDN, bindPassword)
|
||||
if err != nil {
|
||||
c.logger.Error().Err(err).Msg("autoconnect could not get ldap Connection")
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case resConn := <-c.reset:
|
||||
// Only close the connection and reconnect if the current
|
||||
// connection, matches the one we got via the reset channel.
|
||||
// If they differ we already reconnected
|
||||
if l != nil && l == resConn {
|
||||
c.logger.Debug().Msgf("closing connection %v", &l)
|
||||
l.Close()
|
||||
}
|
||||
if l == resConn || l == nil {
|
||||
c.logger.Debug().Msg("reconnecting to LDAP")
|
||||
l, err = c.ldapConnect(ldapURI, bindDN, bindPassword)
|
||||
} else {
|
||||
c.logger.Debug().Msg("already reconnected")
|
||||
}
|
||||
case c.conn <- ldapConnection{l, err}:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c ConnWithReconnect) ldapConnect(ldapURI, bindDN, bindPassword string) (*ldap.Conn, error) {
|
||||
c.logger.Debug().Msgf("Connecting to %s", ldapURI)
|
||||
l, err := ldap.DialURL(ldapURI)
|
||||
if err != nil {
|
||||
c.logger.Error().Err(err).Msg("could not get ldap Connection")
|
||||
} else {
|
||||
c.logger.Debug().Msg("LDAP Connected")
|
||||
if bindDN != "" {
|
||||
c.logger.Debug().Msgf("Binding as %s", bindDN)
|
||||
err = l.Bind(bindDN, bindPassword)
|
||||
if err != nil {
|
||||
c.logger.Error().Err(err).Msg("Bind failed")
|
||||
l.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return l, err
|
||||
}
|
||||
|
||||
func (c ConnWithReconnect) reconnect(resetConn *ldap.Conn) (*ldap.Conn, error) {
|
||||
c.logger.Debug().Msg("LDAP connection reset")
|
||||
c.reset <- resetConn
|
||||
c.logger.Debug().Msg("Waiting for new connection")
|
||||
result := <-c.conn
|
||||
return result.Conn, result.Error
|
||||
}
|
||||
|
||||
// Remaining methods to fulfill ldap.Client interface
|
||||
|
||||
func (c ConnWithReconnect) Start() {}
|
||||
|
||||
func (c ConnWithReconnect) StartTLS(*tls.Config) error {
|
||||
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ConnWithReconnect) Close() {}
|
||||
|
||||
func (c ConnWithReconnect) IsClosing() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c ConnWithReconnect) SetTimeout(time.Duration) {}
|
||||
|
||||
func (c ConnWithReconnect) Bind(username, password string) error {
|
||||
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ConnWithReconnect) UnauthenticatedBind(username string) error {
|
||||
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ConnWithReconnect) SimpleBind(*ldap.SimpleBindRequest) (*ldap.SimpleBindResult, error) {
|
||||
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ConnWithReconnect) ExternalBind() error {
|
||||
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ConnWithReconnect) Add(*ldap.AddRequest) error {
|
||||
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ConnWithReconnect) Del(*ldap.DelRequest) error {
|
||||
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ConnWithReconnect) Modify(*ldap.ModifyRequest) error {
|
||||
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ConnWithReconnect) ModifyDN(*ldap.ModifyDNRequest) error {
|
||||
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ConnWithReconnect) ModifyWithResult(*ldap.ModifyRequest) (*ldap.ModifyResult, error) {
|
||||
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ConnWithReconnect) Compare(dn, attribute, value string) (bool, error) {
|
||||
return false, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ConnWithReconnect) PasswordModify(*ldap.PasswordModifyRequest) (*ldap.PasswordModifyResult, error) {
|
||||
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ConnWithReconnect) SearchWithPaging(searchRequest *ldap.SearchRequest, pagingSize uint32) (*ldap.SearchResult, error) {
|
||||
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/owncloud/ocis/graph/pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
)
|
||||
|
||||
// ldapMock implements the ldap.Client interfac
|
||||
type ldapMock struct {
|
||||
SearchFunc *searchFunc
|
||||
}
|
||||
|
||||
type searchFunc func(*ldap.SearchRequest) (*ldap.SearchResult, error)
|
||||
|
||||
func getMockedBackend(sf *searchFunc, lc config.LDAP, logger *log.Logger) (*LDAP, error) {
|
||||
// Mock a Sizelimit Error
|
||||
lm := ldapMock{SearchFunc: sf}
|
||||
return NewLDAPBackend(lm, lconfig, logger)
|
||||
}
|
||||
|
||||
var lconfig = config.LDAP{
|
||||
UserBaseDN: "dc=test",
|
||||
UserSearchScope: "sub",
|
||||
UserFilter: "filter",
|
||||
UserDisplayNameAttribute: "displayname",
|
||||
UserIDAttribute: "entryUUID",
|
||||
UserEmailAttribute: "mail",
|
||||
UserNameAttribute: "uid",
|
||||
|
||||
GroupBaseDN: "dc=test",
|
||||
GroupSearchScope: "sub",
|
||||
GroupFilter: "filter",
|
||||
GroupNameAttribute: "cn",
|
||||
GroupIDAttribute: "entryUUID",
|
||||
}
|
||||
|
||||
var userEntry = ldap.NewEntry("uid=user",
|
||||
map[string][]string{
|
||||
"uid": {"user"},
|
||||
"displayname": {"DisplayName"},
|
||||
"mail": {"user@example"},
|
||||
"entryuuid": {"abcd-defg"},
|
||||
})
|
||||
var groupEntry = ldap.NewEntry("cn=group",
|
||||
map[string][]string{
|
||||
"cn": {"group"},
|
||||
"entryuuid": {"abcd-defg"},
|
||||
})
|
||||
|
||||
var logger = log.NewLogger(log.Level("debug"))
|
||||
|
||||
func TestNewLDAPBackend(t *testing.T) {
|
||||
|
||||
l := ldapMock{}
|
||||
|
||||
tc := lconfig
|
||||
tc.UserDisplayNameAttribute = ""
|
||||
if _, err := NewLDAPBackend(l, tc, &logger); err == nil {
|
||||
t.Error("Should fail with incomplete user attr config")
|
||||
}
|
||||
|
||||
tc = lconfig
|
||||
tc.GroupIDAttribute = ""
|
||||
if _, err := NewLDAPBackend(l, tc, &logger); err == nil {
|
||||
t.Errorf("Should fail with incomplete group config")
|
||||
}
|
||||
|
||||
tc = lconfig
|
||||
tc.UserSearchScope = ""
|
||||
if _, err := NewLDAPBackend(l, tc, &logger); err == nil {
|
||||
t.Errorf("Should fail with invalid user search scope")
|
||||
}
|
||||
|
||||
tc = lconfig
|
||||
tc.GroupSearchScope = ""
|
||||
if _, err := NewLDAPBackend(l, tc, &logger); err == nil {
|
||||
t.Errorf("Should fail with invalid group search scope")
|
||||
}
|
||||
|
||||
if _, err := NewLDAPBackend(l, lconfig, &logger); err != nil {
|
||||
t.Errorf("Should fail with invalid group search scope")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestCreateUserModelFromLDAP(t *testing.T) {
|
||||
l := ldapMock{}
|
||||
logger := log.NewLogger(log.Level("debug"))
|
||||
|
||||
b, _ := NewLDAPBackend(l, lconfig, &logger)
|
||||
if user := b.createUserModelFromLDAP(nil); user != nil {
|
||||
t.Errorf("createUserModelFromLDAP should return on nil Entry")
|
||||
}
|
||||
user := b.createUserModelFromLDAP(userEntry)
|
||||
if user == nil {
|
||||
t.Error("Converting a valid LDAP Entry should succeed")
|
||||
} else {
|
||||
if *user.OnPremisesSamAccountName != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.userName) {
|
||||
t.Errorf("Error creating msGraph User from LDAP Entry: %s != %s", *user.OnPremisesSamAccountName, userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.userName))
|
||||
}
|
||||
if *user.Mail != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.mail) {
|
||||
t.Errorf("Error creating msGraph User from LDAP Entry: %s != %s", *user.Mail, userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.mail))
|
||||
}
|
||||
if *user.DisplayName != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.displayName) {
|
||||
t.Errorf("Error creating msGraph User from LDAP Entry: %s != %s", *user.DisplayName, userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.displayName))
|
||||
}
|
||||
if *user.ID != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.id) {
|
||||
t.Errorf("Error creating msGraph User from LDAP Entry: %s != %s", *user.ID, userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUser(t *testing.T) {
|
||||
// Mock a Sizelimit Error
|
||||
var sf searchFunc = func(*ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||
return nil, ldap.NewError(ldap.LDAPResultSizeLimitExceeded, errors.New("mock"))
|
||||
}
|
||||
b, _ := getMockedBackend(&sf, lconfig, &logger)
|
||||
_, err := b.GetUser(context.Background(), "fred")
|
||||
if err == nil || err.Error() != "itemNotFound" {
|
||||
t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
|
||||
}
|
||||
|
||||
// Mock an empty Search Result
|
||||
sf = func(*ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||
return &ldap.SearchResult{}, nil
|
||||
}
|
||||
b, _ = getMockedBackend(&sf, lconfig, &logger)
|
||||
_, err = b.GetUser(context.Background(), "fred")
|
||||
if err == nil || err.Error() != "itemNotFound" {
|
||||
t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
|
||||
}
|
||||
|
||||
// Mock a valid Search Result
|
||||
sf = func(*ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||
return &ldap.SearchResult{
|
||||
Entries: []*ldap.Entry{userEntry},
|
||||
}, nil
|
||||
}
|
||||
b, _ = getMockedBackend(&sf, lconfig, &logger)
|
||||
u, err := b.GetUser(context.Background(), "user")
|
||||
if err != nil {
|
||||
t.Errorf("Expected GetUser to succeed. Got %s", err.Error())
|
||||
} else if *u.ID != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.id) {
|
||||
t.Errorf("Expected GetUser to return a valid user")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUsers(t *testing.T) {
|
||||
var sf searchFunc = func(*ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||
return nil, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("mock"))
|
||||
}
|
||||
b, _ := getMockedBackend(&sf, lconfig, &logger)
|
||||
_, err := b.GetUsers(context.Background(), url.Values{})
|
||||
if err == nil || err.Error() != "itemNotFound" {
|
||||
t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
|
||||
}
|
||||
|
||||
sf = func(*ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||
return &ldap.SearchResult{}, nil
|
||||
}
|
||||
b, _ = getMockedBackend(&sf, lconfig, &logger)
|
||||
g, err := b.GetUsers(context.Background(), url.Values{})
|
||||
if err != nil {
|
||||
t.Errorf("Expected success, got '%s'", err.Error())
|
||||
} else if g == nil || len(g) != 0 {
|
||||
t.Errorf("Expected zero length user slice")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetGroup(t *testing.T) {
|
||||
// Mock a Sizelimit Error
|
||||
var sf searchFunc = func(*ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||
return nil, ldap.NewError(ldap.LDAPResultSizeLimitExceeded, errors.New("mock"))
|
||||
}
|
||||
b, _ := getMockedBackend(&sf, lconfig, &logger)
|
||||
_, err := b.GetGroup(context.Background(), "group")
|
||||
if err == nil || err.Error() != "itemNotFound" {
|
||||
t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
|
||||
}
|
||||
|
||||
// Mock an empty Search Result
|
||||
sf = func(*ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||
return &ldap.SearchResult{}, nil
|
||||
}
|
||||
b, _ = getMockedBackend(&sf, lconfig, &logger)
|
||||
_, err = b.GetGroup(context.Background(), "group")
|
||||
if err == nil || err.Error() != "itemNotFound" {
|
||||
t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
|
||||
}
|
||||
|
||||
// Mock a valid Search Result
|
||||
sf = func(*ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||
return &ldap.SearchResult{
|
||||
Entries: []*ldap.Entry{groupEntry},
|
||||
}, nil
|
||||
}
|
||||
b, _ = getMockedBackend(&sf, lconfig, &logger)
|
||||
g, err := b.GetGroup(context.Background(), "group")
|
||||
if err != nil {
|
||||
t.Errorf("Expected GetGroup to succeed. Got %s", err.Error())
|
||||
} else if *g.ID != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id) {
|
||||
t.Errorf("Expected GetGroup to return a valid group")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetGroups(t *testing.T) {
|
||||
var sf searchFunc = func(*ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||
return nil, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("mock"))
|
||||
}
|
||||
b, _ := getMockedBackend(&sf, lconfig, &logger)
|
||||
_, err := b.GetGroups(context.Background(), url.Values{})
|
||||
if err == nil || err.Error() != "itemNotFound" {
|
||||
t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
|
||||
}
|
||||
|
||||
sf = func(*ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||
return &ldap.SearchResult{}, nil
|
||||
}
|
||||
b, _ = getMockedBackend(&sf, lconfig, &logger)
|
||||
g, err := b.GetGroups(context.Background(), url.Values{})
|
||||
if err != nil {
|
||||
t.Errorf("Expected success, got '%s'", err.Error())
|
||||
} else if g == nil || len(g) != 0 {
|
||||
t.Errorf("Expected zero length user slice")
|
||||
}
|
||||
}
|
||||
|
||||
// below here ldap.Client interface method for ldapMock
|
||||
|
||||
func (c ldapMock) Start() {}
|
||||
|
||||
func (c ldapMock) StartTLS(*tls.Config) error {
|
||||
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ldapMock) Close() {}
|
||||
|
||||
func (c ldapMock) IsClosing() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c ldapMock) SetTimeout(time.Duration) {}
|
||||
|
||||
func (c ldapMock) Bind(username, password string) error {
|
||||
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ldapMock) UnauthenticatedBind(username string) error {
|
||||
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ldapMock) SimpleBind(*ldap.SimpleBindRequest) (*ldap.SimpleBindResult, error) {
|
||||
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ldapMock) ExternalBind() error {
|
||||
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ldapMock) Add(*ldap.AddRequest) error {
|
||||
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ldapMock) Del(*ldap.DelRequest) error {
|
||||
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ldapMock) Modify(*ldap.ModifyRequest) error {
|
||||
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ldapMock) ModifyDN(*ldap.ModifyDNRequest) error {
|
||||
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ldapMock) ModifyWithResult(*ldap.ModifyRequest) (*ldap.ModifyResult, error) {
|
||||
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ldapMock) Compare(dn, attribute, value string) (bool, error) {
|
||||
return false, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ldapMock) PasswordModify(*ldap.PasswordModifyRequest) (*ldap.PasswordModifyResult, error) {
|
||||
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (c ldapMock) Search(searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||
if c.SearchFunc != nil {
|
||||
return (*c.SearchFunc)(searchRequest)
|
||||
}
|
||||
|
||||
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
func (c ldapMock) SearchWithPaging(searchRequest *ldap.SearchRequest, pagingSize uint32) (*ldap.SearchResult, error) {
|
||||
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
|
||||
}
|
||||
@@ -12,6 +12,11 @@ import (
|
||||
// ErrorCode defines code as used in MS Graph - see https://docs.microsoft.com/en-us/graph/errors?context=graph%2Fapi%2F1.0&view=graph-rest-1.0
|
||||
type ErrorCode int
|
||||
|
||||
type Error struct {
|
||||
errorCode ErrorCode
|
||||
msg string
|
||||
}
|
||||
|
||||
const (
|
||||
// AccessDenied defines the error if the caller doesn't have permission to perform the action.
|
||||
AccessDenied ErrorCode = iota
|
||||
@@ -66,6 +71,13 @@ var errorCodes = [...]string{
|
||||
"unauthenticated",
|
||||
}
|
||||
|
||||
func New(e ErrorCode, msg string) Error {
|
||||
return Error{
|
||||
errorCode: e,
|
||||
msg: msg,
|
||||
}
|
||||
}
|
||||
|
||||
// Render writes an Graph ErrorObject to the response writer
|
||||
func (e ErrorCode) Render(w http.ResponseWriter, r *http.Request, status int, msg string) {
|
||||
innererror := map[string]interface{}{
|
||||
@@ -85,6 +97,18 @@ func (e ErrorCode) Render(w http.ResponseWriter, r *http.Request, status int, ms
|
||||
render.JSON(w, r, resp)
|
||||
}
|
||||
|
||||
func (e Error) Render(w http.ResponseWriter, r *http.Request) {
|
||||
status := http.StatusInternalServerError
|
||||
if e.errorCode == ItemNotFound {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
e.errorCode.Render(w, r, status, e.msg)
|
||||
}
|
||||
|
||||
func (e ErrorCode) String() string {
|
||||
return errorCodes[e]
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
return errorCodes[e.errorCode]
|
||||
}
|
||||
|
||||
@@ -7,14 +7,16 @@ import (
|
||||
"github.com/cs3org/reva/pkg/rgrpc/todo/pool"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/owncloud/ocis/graph/pkg/config"
|
||||
"github.com/owncloud/ocis/graph/pkg/identity"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
)
|
||||
|
||||
// Graph defines implements the business logic for Service.
|
||||
type Graph struct {
|
||||
config *config.Config
|
||||
mux *chi.Mux
|
||||
logger *log.Logger
|
||||
config *config.Config
|
||||
mux *chi.Mux
|
||||
logger *log.Logger
|
||||
identityBackend identity.Backend
|
||||
}
|
||||
|
||||
// ServeHTTP implements the Service interface.
|
||||
@@ -27,15 +29,6 @@ func (g Graph) GetClient() (gateway.GatewayAPIClient, error) {
|
||||
return pool.GetGatewayServiceClient(g.config.Reva.Address)
|
||||
}
|
||||
|
||||
// The key type is unexported to prevent collisions with context keys defined in
|
||||
// other packages.
|
||||
type key int
|
||||
|
||||
const (
|
||||
userKey key = iota
|
||||
groupKey
|
||||
)
|
||||
|
||||
type listResponse struct {
|
||||
Value interface{} `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
+24
-103
@@ -1,129 +1,50 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
cs3 "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
|
||||
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
"github.com/owncloud/ocis/graph/pkg/service/v0/errorcode"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
|
||||
//msgraph "github.com/owncloud/open-graph-api-go" // FIXME add groups to open graph, needs OnPremisesSamAccountName and OnPremisesDomainName
|
||||
msgraph "github.com/yaegashi/msgraph.go/v1.0"
|
||||
)
|
||||
|
||||
// GroupCtx middleware is used to load an User object from
|
||||
// the URL parameters passed through as the request. In case
|
||||
// the User could not be found, we stop here and return a 404.
|
||||
func (g Graph) GroupCtx(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
groupID := chi.URLParam(r, "groupID")
|
||||
if groupID == "" {
|
||||
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing group id")
|
||||
return
|
||||
}
|
||||
|
||||
client, err := g.GetClient()
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("could not get client")
|
||||
errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
res, err := client.GetGroupByClaim(r.Context(), &cs3.GetGroupByClaimRequest{
|
||||
Claim: "groupid", // FIXME add consts to reva
|
||||
Value: groupID,
|
||||
})
|
||||
|
||||
switch {
|
||||
case err != nil:
|
||||
g.logger.Error().Err(err).Str("groupid", groupID).Msg("error sending get group by claim id grpc request")
|
||||
errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
case res.Status.Code != cs3rpc.Code_CODE_OK:
|
||||
if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
|
||||
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.Status.Message)
|
||||
return
|
||||
}
|
||||
g.logger.Error().Err(err).Str("groupid", groupID).Msg("error sending get group by claim id grpc request")
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), groupKey, res.Group)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// GetGroups implements the Service interface.
|
||||
func (g Graph) GetGroups(w http.ResponseWriter, r *http.Request) {
|
||||
client, err := g.GetClient()
|
||||
groups, err := g.identityBackend.GetGroups(r.Context(), r.URL.Query())
|
||||
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("could not get client")
|
||||
errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
search := r.URL.Query().Get("search")
|
||||
if search == "" {
|
||||
search = r.URL.Query().Get("$search")
|
||||
}
|
||||
|
||||
res, err := client.FindGroups(r.Context(), &cs3.FindGroupsRequest{
|
||||
// FIXME presence match is currently not implemented, an empty search currently leads to
|
||||
// Unwilling To Perform": Search Error: error parsing filter: (&(objectclass=posixAccount)(|(cn=*)(displayname=*)(mail=*))), error: Present filter match for cn not implemented
|
||||
Filter: search,
|
||||
})
|
||||
switch {
|
||||
case err != nil:
|
||||
g.logger.Error().Err(err).Str("search", search).Msg("error sending find groups grpc request")
|
||||
errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
case res.Status.Code != cs3rpc.Code_CODE_OK:
|
||||
if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
|
||||
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.Status.Message)
|
||||
return
|
||||
var errcode errorcode.Error
|
||||
if errors.As(err, &errcode) {
|
||||
errcode.Render(w, r)
|
||||
} else {
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
g.logger.Error().Err(err).Str("search", search).Msg("error sending find groups grpc request")
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message)
|
||||
return
|
||||
}
|
||||
|
||||
groups := make([]*msgraph.Group, 0, len(res.Groups))
|
||||
|
||||
for _, group := range res.Groups {
|
||||
groups = append(groups, createGroupModelFromCS3(group))
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(w, r, &listResponse{Value: groups})
|
||||
}
|
||||
|
||||
// GetGroup implements the Service interface.
|
||||
func (g Graph) GetGroup(w http.ResponseWriter, r *http.Request) {
|
||||
group := r.Context().Value(groupKey).(*cs3.Group)
|
||||
groupID := chi.URLParam(r, "groupID")
|
||||
if groupID == "" {
|
||||
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing group id")
|
||||
return
|
||||
}
|
||||
|
||||
group, err := g.identityBackend.GetGroup(r.Context(), groupID)
|
||||
if err != nil {
|
||||
var errcode errorcode.Error
|
||||
if errors.As(err, &errcode) {
|
||||
errcode.Render(w, r)
|
||||
} else {
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(w, r, createGroupModelFromCS3(group))
|
||||
}
|
||||
|
||||
func createGroupModelFromCS3(g *cs3.Group) *msgraph.Group {
|
||||
if g.Id == nil {
|
||||
g.Id = &cs3.GroupId{}
|
||||
}
|
||||
return &msgraph.Group{
|
||||
DirectoryObject: msgraph.DirectoryObject{
|
||||
Entity: msgraph.Entity{
|
||||
ID: &g.Id.OpaqueId,
|
||||
},
|
||||
},
|
||||
OnPremisesDomainName: &g.Id.Idp,
|
||||
OnPremisesSamAccountName: &g.GroupName,
|
||||
DisplayName: &g.DisplayName,
|
||||
Mail: &g.Mail,
|
||||
// TODO when to fetch and expand memberof, usernames or ids?
|
||||
}
|
||||
render.JSON(w, r, group)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
|
||||
"github.com/owncloud/ocis/graph/pkg/identity"
|
||||
"github.com/owncloud/ocis/graph/pkg/identity/ldap"
|
||||
"github.com/owncloud/ocis/ocis-pkg/account"
|
||||
opkgm "github.com/owncloud/ocis/ocis-pkg/middleware"
|
||||
)
|
||||
@@ -24,10 +27,34 @@ func NewService(opts ...Option) Service {
|
||||
m := chi.NewMux()
|
||||
m.Use(options.Middleware...)
|
||||
|
||||
var backend identity.Backend
|
||||
switch options.Config.Identity.Backend {
|
||||
case "cs3":
|
||||
backend = &identity.CS3{
|
||||
Config: &options.Config.Reva,
|
||||
Logger: &options.Logger,
|
||||
}
|
||||
case "ldap":
|
||||
var err error
|
||||
conn := ldap.NewLDAPWithReconnect(&options.Logger,
|
||||
options.Config.Identity.LDAP.URI,
|
||||
options.Config.Identity.LDAP.BindDN,
|
||||
options.Config.Identity.LDAP.BindPassword,
|
||||
)
|
||||
if backend, err = identity.NewLDAPBackend(conn, options.Config.Identity.LDAP, &options.Logger); err != nil {
|
||||
options.Logger.Error().Msgf("Error initializing LDAP Backend: '%s'", err)
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
options.Logger.Error().Msgf("Unknown Identity Backend: '%s'", options.Config.Identity.Backend)
|
||||
return nil
|
||||
}
|
||||
|
||||
svc := Graph{
|
||||
config: options.Config,
|
||||
mux: m,
|
||||
logger: &options.Logger,
|
||||
config: options.Config,
|
||||
mux: m,
|
||||
logger: &options.Logger,
|
||||
identityBackend: backend,
|
||||
}
|
||||
|
||||
m.Route(options.Config.HTTP.Root, func(r chi.Router) {
|
||||
@@ -41,14 +68,12 @@ func NewService(opts ...Option) Service {
|
||||
r.Route("/users", func(r chi.Router) {
|
||||
r.Get("/", svc.GetUsers)
|
||||
r.Route("/{userID}", func(r chi.Router) {
|
||||
r.Use(svc.UserCtx)
|
||||
r.Get("/", svc.GetUser)
|
||||
})
|
||||
})
|
||||
r.Route("/groups", func(r chi.Router) {
|
||||
r.Get("/", svc.GetGroups)
|
||||
r.Route("/{groupID}", func(r chi.Router) {
|
||||
r.Use(svc.GroupCtx)
|
||||
r.Get("/", svc.GetGroup)
|
||||
})
|
||||
})
|
||||
|
||||
+26
-112
@@ -1,65 +1,17 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
revactx "github.com/cs3org/reva/pkg/ctx"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/owncloud/ocis/graph/pkg/identity"
|
||||
"github.com/owncloud/ocis/graph/pkg/service/v0/errorcode"
|
||||
|
||||
//msgraph "github.com/owncloud/open-graph-api-go" // FIXME needs OnPremisesSamAccountName, OnPremisesDomainName and AdditionalData
|
||||
msgraph "github.com/yaegashi/msgraph.go/v1.0"
|
||||
)
|
||||
|
||||
// UserCtx middleware is used to load an User object from
|
||||
// the URL parameters passed through as the request. In case
|
||||
// the User could not be found, we stop here and return a 404.
|
||||
// TODO use cs3 api to look up user
|
||||
func (g Graph) UserCtx(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
userID := chi.URLParam(r, "userID")
|
||||
if userID == "" {
|
||||
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing user id")
|
||||
return
|
||||
}
|
||||
|
||||
client, err := g.GetClient()
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("could not get client")
|
||||
errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
res, err := client.GetUserByClaim(r.Context(), &cs3.GetUserByClaimRequest{
|
||||
Claim: "userid", // FIXME add consts to reva
|
||||
Value: userID,
|
||||
})
|
||||
|
||||
switch {
|
||||
case err != nil:
|
||||
g.logger.Error().Err(err).Str("userid", userID).Msg("error sending get user by claim id grpc request")
|
||||
errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
case res.Status.Code != cs3rpc.Code_CODE_OK:
|
||||
if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
|
||||
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.Status.Message)
|
||||
return
|
||||
}
|
||||
g.logger.Error().Err(err).Str("userid", userID).Msg("error sending get user by claim id grpc request")
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), userKey, res.User)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// GetMe implements the Service interface.
|
||||
func (g Graph) GetMe(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -72,7 +24,7 @@ func (g Graph) GetMe(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
g.logger.Info().Interface("user", u).Msg("User in /me")
|
||||
|
||||
me := createUserModelFromCS3(u)
|
||||
me := identity.CreateUserModelFromCS3(u)
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(w, r, me)
|
||||
@@ -81,76 +33,38 @@ func (g Graph) GetMe(w http.ResponseWriter, r *http.Request) {
|
||||
// GetUsers implements the Service interface.
|
||||
// TODO use cs3 api to look up user
|
||||
func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
client, err := g.GetClient()
|
||||
users, err := g.identityBackend.GetUsers(r.Context(), r.URL.Query())
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("could not get client")
|
||||
errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
search := r.URL.Query().Get("search")
|
||||
if search == "" {
|
||||
search = r.URL.Query().Get("$search")
|
||||
}
|
||||
|
||||
res, err := client.FindUsers(r.Context(), &cs3.FindUsersRequest{
|
||||
// FIXME presence match is currently not implemented, an empty search currently leads to
|
||||
// Unwilling To Perform": Search Error: error parsing filter: (&(objectclass=posixAccount)(|(cn=*)(displayname=*)(mail=*))), error: Present filter match for cn not implemented
|
||||
Filter: search,
|
||||
})
|
||||
switch {
|
||||
case err != nil:
|
||||
g.logger.Error().Err(err).Str("search", search).Msg("error sending find users grpc request")
|
||||
errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
case res.Status.Code != cs3rpc.Code_CODE_OK:
|
||||
if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
|
||||
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.Status.Message)
|
||||
return
|
||||
var errcode errorcode.Error
|
||||
if errors.As(err, &errcode) {
|
||||
errcode.Render(w, r)
|
||||
} else {
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
g.logger.Error().Err(err).Str("search", search).Msg("error sending find users grpc request")
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message)
|
||||
return
|
||||
}
|
||||
|
||||
users := make([]*msgraph.User, 0, len(res.Users))
|
||||
|
||||
for _, user := range res.Users {
|
||||
users = append(users, createUserModelFromCS3(user))
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(w, r, &listResponse{Value: users})
|
||||
}
|
||||
|
||||
// GetUser implements the Service interface.
|
||||
func (g Graph) GetUser(w http.ResponseWriter, r *http.Request) {
|
||||
user := r.Context().Value(userKey).(*cs3.User)
|
||||
userID := chi.URLParam(r, "userID")
|
||||
if userID == "" {
|
||||
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing user id")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := g.identityBackend.GetUser(r.Context(), userID)
|
||||
|
||||
if err != nil {
|
||||
var errcode errorcode.Error
|
||||
if errors.As(err, &errcode) {
|
||||
errcode.Render(w, r)
|
||||
} else {
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(w, r, createUserModelFromCS3(user))
|
||||
}
|
||||
|
||||
func createUserModelFromCS3(u *cs3.User) *msgraph.User {
|
||||
if u.Id == nil {
|
||||
u.Id = &cs3.UserId{}
|
||||
}
|
||||
return &msgraph.User{
|
||||
DisplayName: &u.DisplayName,
|
||||
Mail: &u.Mail,
|
||||
// TODO u.Groups are those ids or group names?
|
||||
OnPremisesSamAccountName: &u.Username,
|
||||
DirectoryObject: msgraph.DirectoryObject{
|
||||
Entity: msgraph.Entity{
|
||||
ID: &u.Id.OpaqueId,
|
||||
Object: msgraph.Object{
|
||||
AdditionalData: map[string]interface{}{
|
||||
"uidnumber": u.UidNumber,
|
||||
"gidnumber": u.GidNumber,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
render.JSON(w, r, user)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user