mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-04-24 04:58:31 -05:00
Make identity backend configurable for GraphAPI
In order to gradually switch to a new LDAP backend move the existing code talking to CS3 into its own backend.
This commit is contained in:
@@ -52,6 +52,10 @@ type Spaces struct {
|
||||
DefaultQuota string `ocisConfig:"default_quota"`
|
||||
}
|
||||
|
||||
type Identity struct {
|
||||
Backend string `ocisConfig:"backend"`
|
||||
}
|
||||
|
||||
// Config combines all available configuration parts.
|
||||
type Config struct {
|
||||
*shared.Commons
|
||||
@@ -65,6 +69,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 +108,8 @@ func DefaultConfig() *Config {
|
||||
WebDavPath: "/dav/spaces/",
|
||||
DefaultQuota: "1000000000",
|
||||
},
|
||||
Identity: Identity{
|
||||
Backend: "cs3",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,5 +111,9 @@ func structMappings(cfg *Config) []shared.EnvBinding {
|
||||
EnvVars: []string{"REVA_GATEWAY"},
|
||||
Destination: &cfg.Reva.Address,
|
||||
},
|
||||
{
|
||||
EnvVars: []string{"GRAPH_IDENTITY_BACKEND"},
|
||||
Destination: &cfg.Identity.Backend,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
msgraph "github.com/yaegashi/msgraph.go/beta"
|
||||
)
|
||||
|
||||
type Users interface {
|
||||
GetUser(ctx context.Context, nameOrId string) (*msgraph.User, error)
|
||||
GetUsers(ctx context.Context, queryParam url.Values) ([]*msgraph.User, 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,84 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
cs3 "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, &cs3.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, &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:
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
userBackend identity.Users
|
||||
}
|
||||
|
||||
// ServeHTTP implements the Service interface.
|
||||
|
||||
@@ -5,6 +5,7 @@ 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/ocis-pkg/account"
|
||||
opkgm "github.com/owncloud/ocis/ocis-pkg/middleware"
|
||||
)
|
||||
@@ -24,10 +25,22 @@ func NewService(opts ...Option) Service {
|
||||
m := chi.NewMux()
|
||||
m.Use(options.Middleware...)
|
||||
|
||||
var userBackend identity.Users
|
||||
switch options.Config.Identity.Backend {
|
||||
case "cs3":
|
||||
userBackend = &identity.CS3{
|
||||
Config: &options.Config.Reva,
|
||||
Logger: &options.Logger,
|
||||
}
|
||||
default:
|
||||
options.Logger.Error().Msgf("Unknown Identity Backend: '%s'", options.Config.Identity.Backend)
|
||||
}
|
||||
|
||||
svc := Graph{
|
||||
config: options.Config,
|
||||
mux: m,
|
||||
logger: &options.Logger,
|
||||
config: options.Config,
|
||||
mux: m,
|
||||
logger: &options.Logger,
|
||||
userBackend: userBackend,
|
||||
}
|
||||
|
||||
m.Route(options.Config.HTTP.Root, func(r chi.Router) {
|
||||
@@ -41,7 +54,6 @@ 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)
|
||||
})
|
||||
})
|
||||
|
||||
+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.userBackend.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.userBackend.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